Build a group chat app with react native and meteor

The stack

I have been a big fan of meteor from its infancy and I am always surprised how it never became as big in the JS community as rails became in the ruby community. There was some hype around it for a short while and it seemed to have died off slowly as most hypes do. However, it’s probably time for meteor to make a comeback with it’s 2.0 release. If you’re not familiar with this framework, I believe this post will be able to get your attention towards it. If you’ve heard about it here and there, I’d urge you to read this post and give it a go along with me and I’m confident that it’s simplicity and inherent power will wow you. We will be going over the baked-in authentication system, realtime data propagation and DDP based architecture of meteor throughout this post. Oh and we will be using typescript instead of plain ol’ JS to build this post.

In this post, we will be building a group messaging app with meteor and mongodb on the server and then react native, expo, gifted chat and magnus ui on the client. Styling react native apps can be a tedious task and I believe utility based styling approach always eases the process of building UI. Also, building a chat UI can be tricky and time consuming so to avoid reinventing the wheel, we will be using the amazing gifted chat package that drops a ready-made chat UI on any react native app.

Before you invest your time and energy into reading this 5K word long tutorial, I think you should see what you’re getting in return. Here’s a video demonstrating the end product: https://youtu.be/4Cx2k7qhXHQ

Dive in

Since it’s a messaging app, let’s call it textr (the name seems to be already taken by another platform that I am not affiliated with in any way, I’m simply using this name as a placeholder for this app. Feel free to use your own).

We will first start with the client side of the app and generate an expo app using the expo-cli. If you don’t have it already, please follow the official guides to install the cli tool on your workstation. Then, run the following commands which will create a couple of folders for you:

mkdir textr && cd $_
expo init textr-rn

It will also ask you to pick a starter template, make sure you choose the Blank (Typescript) template.

Before we look inside the code, let’s install a few npm packages that we know we will be needing very soon:

yarn add @meteorrn/core expo-constants react-native-gifted-chat react-native-magnus color react-native-modal react-native-animatable -S
expo install @react-native-community/netinfo @react-native-async-storage/async-storage

Now we are all set to build our react native app. Open the entire textr Folder with your favorite code editor and dive into the App.tsx file in root of the textr-rn folder and replace the existing code with the following:

import React, {useState} from 'react';
import Constants from "expo-constants";
import {StatusBar} from "expo-status-bar";
import {SafeAreaView, Dimensions} from 'react-native';
import { Div, ThemeProvider } from "react-native-magnus";

import ChatPage from "./chat-page";
import AuthPage from "./auth-page";

const windowHeight = Dimensions.get('window').height;

export default function App() {
    const [user, setUser] = useState(null);
    return (
        <ThemeProvider>
            <StatusBar style="auto" />
            <SafeAreaView>
                <Div
                    flex={1}
                    minH={windowHeight}
                    justifyContent="center"
                    pt={Constants.statusBarHeight}>
                    {
                        user
                            ? <ChatPage user={{ _id: user }} />
                            : <AuthPage setUser={setUser} />
                    }
                </Div>
            </SafeAreaView>
        </ThemeProvider>
    );
}

This is the main entry point of our app and the functionality is divided in two main pages: Authentication and chat page. Before get to those, we are doing some bootstrapping here such as: wrapping the entire app with ThemeProvider from magnus UI so that we can use all it’s utility props, putting the StatusBus on top and then wrapping everything in SafeAreaView so that the app’s components don’t overlap with the status bar area or get cut off by device’s notch.

The main body of the app is wrapped within a Div component which is imported from magnus UI and basically is just a wrapper around react native’s View component that allows us to manipulate it’s look through various utility props. You should read more about these props on the official documentation and once you get the hang of it, these will become second nature. Let me break down the ones we’re using here:

  • pt={Constants.statusBarHeight} ensures that all the components begin below the statusbar area by adding a padding on top.
  • minH={windowHeight}, where windowHeight is computed from the height of the device’s screen, ensures that every page is rendered to fill the entire vertical height of the page.
  • flex={1} and justifyContent="center" we make sure that the content inside is always vertically centered in the middle of the screen.

Finally, we conditionally render the Authpage or the ChatPage components depending on the presence of the user state variable. For now, we are passing the setUser function down to the AuthPage component so that when the user signs up or logs in, the user data is propagated back to this component and the chat page is rendered. However, this will be done slightly differently once we start integrating with the meteor api server.

Authentication page

Separation of concern is very important to keep code maintainable and react hooks make it quite easy to do from early on. We will keep all our logic wrapped in a custom hook file use-auth-form.ts and the presentation layer will be in a file named auth-page.ts. Go ahead and create those 2 files in the root of the app’s folder. Let’s start with the presentation component file and put the following content in it:

import { Div, Text, Button, Input } from 'react-native-magnus';
import React from 'react';
import {KeyboardAvoidingView} from "react-native";

import {useAuthForm} from "./use-auth-form";

export default function AuthPage({ setUser }) {
    const {
        error,
        email,
        form,
        username,
        password,
        callingApi,
        handleLogin,
        handleSignup,
        toggleFormType,
        handleEmailInput,
        handleUsernameInput,
        handlePasswordInput
    } = useAuthForm(setUser);
    return (
        <KeyboardAvoidingView behavior={'padding'}>
            <Div px="lg" bg="white">
                <Text
                    fontSize="4xl"
                    textAlign="center"
                    fontWeight="bold">
                    Textr
                </Text>
                <Div mt="md" pt="xl">
                    <Text fontSize="md" mb="sm">Email</Text>
                    <Input
                        rounded="sm"
                        bg="gray200"
                        value={email}
                        editable={!callingApi}
                        autoCompleteType="email"
                        keyboardType="email-address"
                        borderWidth={0}
                        onChangeText={handleEmailInput}
                    />
                </Div>
                {form === 'signup' && <Div mt="md">
                    <Text fontSize="md" mb="sm">Username</Text>
                    <Input
                        bg="gray200"
                        rounded="sm"
                        value={username}
                        editable={!callingApi}
                        borderWidth={0}
                        onChangeText={handleUsernameInput}
                    />
                </Div>}
                <Div mt="md">
                    <Text fontSize="md" mb="sm">Password</Text>
                    <Input
                        bg="gray200"
                        secureTextEntry
                        rounded="sm"
                        value={password}
                        editable={!callingApi}
                        borderWidth={0}
                        onChangeText={handlePasswordInput}
                    />
                </Div>
                {!!error && (
                    <Text
                        color="red600"
                        py="lg">
                        {error}
                    </Text>
                )}
                <Button
                    block
                    py="lg"
                    mt="xl"
                    bg="gray500"
                    loading={callingApi}
                    onPress={form === 'signup' ? handleSignup : handleLogin}>
                    {form === 'signup' ? 'Sign up' : 'Login'}
                </Button>
                <Div alignItems="center" pt="lg">
                    <Text>
                        {
                            form === 'signup'
                                ? 'Already have an account?'
                                : "Don't have an account?"
                        }
                    </Text>
                    <Button
                        onPress={toggleFormType}
                        bg="transparent"
                        color="gray700"
                        block>

                        {
                            form === 'signup'
                                ? 'Click here to login'
                                : 'Click here to signup'
                        }
                    </Button>
                </Div>
            </Div>
        </KeyboardAvoidingView>
    );
}

Let me first explain what this component does. It will show the sign up form with username, email address and password input fields by default. It will allow the user to switch to the login form that contains only the email and password fields. Based on the form that the user is filling out, the main action button will either trigger a signup or a login call.

All of the logic that handles these behaviors are tucked away in the useAuthForm hook. Let’s take a quick look at that file now:

import {useReducer} from "react";

type AuthInputPayload = {
    field: 'email' | 'password' | 'username',
    value: string,
};

type AuthState = {
    email: string;
    error: string;
    password: string;
    username: string;
    callingApi: boolean;
    form: 'signup' | 'login';
};

type AuthAction =
    | { type: 'callingApi' | 'apiSuccess' | 'toggleForm' }
    | { type: 'apiError', payload: { error: string } }
    | { type: 'input', payload: AuthInputPayload };

export const useAuthForm = (setUser) => {
    const reducer = (state: AuthState, action: AuthAction): AuthState => {
        switch (action.type) {
            case 'input':
                return {...state, [action.payload.field]: action.payload.value};
            case 'callingApi':
                return {...state, callingApi: true, error: ''};
            case 'apiSuccess':
                return {...state, callingApi: false};
            case 'toggleForm':
                return {...state, form: state.form === 'signup' ? 'login' : 'signup'};
            case 'apiError':
                return {...state, callingApi: false, error: action.payload.error};
            default:
                return state;
        }
    }

    const initialState: AuthState = {
        username: '',
        email: '',
        password: '',
        callingApi: false,
        error: '',
        form: 'login'
    };
    const [{
        username,
        email,
        form,
        password,
        callingApi,
        error
    }, dispatch] = useReducer(reducer, initialState);
    const handleInput = (field, value) => dispatch({type: 'input', payload: {field, value}});
    return {
        email,
        username,
        password,
        callingApi,
        error,
        form,
        handleLogin: () => {
            if (!email || !password) {
                dispatch({type: 'apiError', payload: {error: 'Please fill in all the input fields'}});
                return;
            }
            dispatch({type: 'callingApi'});
            //    Implement meteor login here
            setTimeout(() => {
                dispatch({type: 'apiSuccess'});
                setUser({username});
            }, 500);
        },
        handleSignup: () => {
            if (!email || !password || !username) {
                dispatch({type: 'apiError', payload: {error: 'Please fill in all the input fields'}});
                return;
            }
            dispatch({type: 'callingApi'});
            //    Implement meteor login here
            setTimeout(() => {
                dispatch({type: 'apiSuccess'});
                setUser({username});
            }, 500);
        },
        handleUsernameInput: (value) => handleInput('username', value),
        handleEmailInput: (value) => handleInput('email', value),
        handlePasswordInput: (value) => handleInput('password', value),
        toggleFormType: () => dispatch({type: 'toggleForm'}),
    };
};

The entire state of the auth form is described by the AuthState type so let’s take a closer look at it’s structure.

  • 3 string inputs: email, username and password. These are the ones passed to the value prop in the form’s input fields.
  • form property with 2 possible values, ‘signup’ or ‘login’. This is what determines which type of form will be displayed to the user.
  • The error property which will contain any eventual error in the form’s input validation or the server’s response once we get into that.
  • callingApi property which lets the user know that a server request is in progress.

The reducer is equipped to handle 5 actions:

  • input action updates the state with input value from the form fields. Each input action will contain a payload with the name of the field and the value.
  • toggleForm action that switches the form type from signup to login or vice versa.
  • Aptly named callingApi action that sets the value of the callingApi state.
  • apiSuccess and apiError actions that sets callingApi state to false since these actions will only be dispatched when a server request has been completed with either success or an error. With apiError it will also set the error message in the state.

Instead of directly using the dispatch function from useReducer, we are returning custom functions such as handleEmailInput, handleUsernameInput and handlePasswordInput that just dispatch input events behind the scene. Similarly toggleFormType dispatches the toggleForm action.

More meaty functions here are the handleLogin and handleSignup which does some client side validation and dispatches the apiError action if the user presses the main action button without filling out all the required fields. For now, we are just simulating the server request using a setTimeout that dispatches apiSuccess action after 500ms.

Ok, I think that sums up the hook quite well. Let’s get back to the page component and see how this hook is powering everything that we said the component needs to do previously. First thing you will notice is the KeyboardAvoidingView component wrapping the entire page. That’s because the input fields can get covered by the keyboard when focused since we are placing it in the vertical center of the screen.

On top of the form, we create a logo-ish text with the help of some utility props from magnus. Then the 3 input fields are rendered with a label on top and among them, the username field is conditionally rendered only for the signup form mode. We are also disabling all the fields when api call is going out using the editable={!callingApi} prop on Input components. The error message is rendered with red600 color to make it look more like an error message instead of a normal text.

For the main action button, we are using the block prop so that the button takes up the entire width of the container. It triggers either the handleSignup or the handleLogin function when pressed, depending on which form the user filled out. With the help of theloading={callingApi} prop, when a simulated server request is happening, we are showing a nice loading spinner within the button. To quickly switch between the two forms, we have a ghost button at the bottom of the form with some instructional text. With all of that, this is how the page looks:

Now, when you see this page and fill out the fields in signup or login form and press the main action button, the app will try to render the chat page for you which we will be creating in the next section.

Chat Page

The chat page is going to look much more simple until we plug in the communication with the meteor server. Go ahead, open the file and put in the following code in it:

import { GiftedChat } from 'react-native-gifted-chat'
import React, {useEffect, useCallback, useState} from 'react';
import {KeyboardAvoidingView, Platform} from "react-native";

export default function ChatPage({ user }) {
    const [messages, setMessages] = useState([]);

    useEffect(() => {
        setMessages([]);
    }, []);

    const onSend = useCallback((messages = []) => {
        setMessages(previousMessages => GiftedChat.append(previousMessages, messages));
    }, []);

    return (
        <>
            <GiftedChat
                user={user}
                messages={messages}
                renderUsernameOnMessage
                onSend={messages => onSend(messages)}
            />
            { Platform.OS === 'android' && <KeyboardAvoidingView behavior="padding" /> }
        </>
    );
};

We are simply rendering the GiftedChat component imported from the react-native-gifted-chat package and passing it some props. Most of this will change once we integrate the message sending with meteor server.

At this point, you can open the app on your device/emulator and play around with the form and the chat page. Here’s a quick video of how it looks on my device https://www.youtube.com/watch?v=h8QzbrpvwDs

When you’re ready, proceed to the next section where we build the meteor server app.

Authenticate with meteor server app

If you are using meteor for the first time, please follow their official installation guide to equip your workstation with it. If you already have it, you can get started with the following command inside the container textr folder: meteor create --typescript textr-api and this will create a new folder named textr-api where some boilerplate meteor code has been dropped in for you. Open the folder in your favorite code editor and let’s clean up the boilerplate code before we add our own stuff. First, remove all the content inside server/main.tsfile and entirely remove the following files:

imports/api/links.ts
imports/ui/Iello.tsx
imports/ui/Info.tsx

Then open up the imports/ui/App.tsx file and remove the &lt;Hello /> &lt;Info /> components and their associated imports. While meteor works great for monolithic frontend and backend combined applications, in our case, we will be using it only as a server side api app. So everything that’s inside the client folder or imports/ui folder can be disregarded. Now from the terminal, you need to run meteor to fire up the app which will run the app in your machine’s 3000 port.

Now this api app is running on your workstation which is why from within the same machine, you can access it using localhost. However, your device/emulator points to itself when using localhost. So to talk to this app, you will have to use your machine’s local network IP address. A quick google search can guide you through the process of getting that. Here’s one for macOS https://osxdaily.com/2010/08/08/lan-ip-address-mac/ Once you have the local IP of your machine, head back to the expo app and open up the App.tsx file and add these lines:

import {SafeAreaView, Dimensions} from 'react-native';
import { Div, ThemeProvider } from "react-native-magnus";
import { User } from 'react-native-gifted-chat';
import Meteor, {withTracker} from '@meteorrn/core';

import ChatPage from "./chat-page";
import AuthPage from "./auth-page";

const windowHeight = Dimensions.get('window').height;

Meteor.connect("ws://192.168.0.113:3000/websocket");

function App({ user }: {user: User}) {
    return (
        <ThemeProvider>
            <StatusBar style="auto" />
            <SafeAreaView>
                <Div
                    flex={1}
                    minH={windowHeight}
                    justifyContent="center"
                    pt={Constants.statusBarHeight}>
                    {
                        user
                            ? <ChatPage />
                            : <AuthPage />
                    }
                </Div>
            </SafeAreaView>
        </ThemeProvider>
    );
}

export default withTracker(() => ({user: Meteor.user()}))(App);

First, we connect to our meteor app server. Then, we are wrapping our App component with the withTracker Higher Order Component(HOC). This seems very plain but there’s a LOT of magic going on behind the scene with this HOC. Meteor has the concept of reactive data source and these data sources can change anywhere, client or server and meteor will propagate the change in realtime to all it’s listeners. Meteor.user() is one of those reactive data sources. Remember when I said meteor comes with a baked in authentication system? This is part of that. So what this will do is, set a listener on the user data and pass the data as user prop down to the App component. And thanks to it’s reactive nature, whenever user info is changed anywhere, for example, the authentication page, this component will re-render with new user prop. Which is why we can get rid of the local state variable from the App component.

Ok now let’s integrate meteor’s authentication system with our form handler logic. Open up the use-auth-form.ts file, find the handleSignup function’s definition and swap out the setTimeout call with the following:

           Accounts.createUser({username, email, password}, (err) => {
                if (err) {
                    dispatch({type: 'apiError', payload: {error: err.message}});
                    return;
                }
                
                dispatch({ type: 'apiSuccess' });
            });
            /* previous simulation code
            setTimeout(() => {
                dispatch({type: 'apiSuccess'});
                setUser({username});
            }, 500);
            */

The Accounts.createUser call will take the username, email and password and create a user account with these details on our server. By default, meteor comes with an embedded mongodb instance and this data will be saved there. The response callback will contain an error if there was an issue registering the user account, for example, if there’s already an user with the given email or username etc. If the registration is successful, err will be set to null. For new user signup, this is all we need! Amazing, right?

Now, let’s take the handleLogin function and update it with the following code:

Meteor.loginWithPassword(email, password, (err) => {
                if (err) {
                    dispatch({type: 'apiError', payload: {error: err.message}});
                    return;
                }

                dispatch({ type: 'apiSuccess' });
            });           
           /* previous simulation code
            setTimeout(() => {
                dispatch({type: 'apiSuccess'});
                setUser({username});
            }, 500);
            */

Similar to signup, login is also packaged in this one handy loginWithPassword method from Meteor module and all we have to do is, pass the email/username and then the password to verify if a user with this combination exists in the database.

Finally, don’t forget to import these new modules in the hook file. Add this at the top of the file: import Meteor, { Accounts } from '@meteorrn/core';

Notice that we got rid of the injected setUser function in this hook since that has been replaced by the reactive nature of meteor.

This is all we need on the expo app to hook it up with server side authentication. However, there’s one more thing we need to do on the server. Meteor has authentication modules that can plug into a lot of 3rd party services via oAuth, SSO and other established user authentication methods. Not every app needs all of them which is why meteor made those opt-in. To enable password based auth in our meteor app we simply need to run meteor add accounts-password. This is a very powerful package and you should read more about it here.

Once you install that package, the meteor app will restart and from the expo app, you can go ahead and signup for an account from the expo app and it should take you to the chat page.

Realtime messaging with meteor server

With authentication out of the way, we are ready to move to the more fun part of this journey which is, integrating gifted chat with meteor so that messages entered from the app are stored in mongodb and sent to other connected users in realtime.

We start by creating a new director inside imports/api/ and creating a couple of empty files inside of it:

cd text-api/imports && mkdir messages && cd $_ && touch collection.ts && mkdir server && cd $_ && touch methods.ts && touch publications.ts

Now open the file collection.ts and here’s the code we are going to put inside of it:

import { Mongo } from 'meteor/mongo';

export interface Message {
    _id: string;
    text: string;
    userId: string;
    createdAt: Date;
}

export const MessagesCollection = new Mongo.Collection<Message>('messages');

First thing you will notice is the interface definition for every message entry containing an id field which is added to every entry in mongodb collections. Then we have a text field which will contain the actual text that the user sent. The userId field contains the `idof the sender of the message. This will point to the user entry in users collection which is made available to us by the authentication module. Finally, thecreatedAtfield contains the timestamp when the message was sent. Then we instantiate a newMongo.Collectionobject type casting it’s entries to the Message interface. Now, using thisMessagesCollection` object, we can query the database in any way we want. The first usage of this will be to insert a new message into the collection.

In the meteor world, for communicating with the server from a client, we use methods. These are equivalents of an HTTP POST or a PUT request in REST api paradigm. Meteor allows sharing code between server and client but sometimes, there are code that will never have to run on the client. For performance and security purposes, making sure that kind of code is never sent to the client you can put them inside a server/ directory and meteor will make sure that it never publishes this code in the client bundle. Methods are usually that kind of code. We could, technically, insert a message directly from the client and meteor would take care of sending it to the server, saving it in the database but that won’t be very secure, right?

Let’s create our first meteor method that will allow us to insert a message into the messages collection from our react native app. Open up the server/methods.ts file and put the following code in it:

import { check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
import {MessagesCollection} from "/imports/api/messages/collection";

Meteor.methods({
    'messages.add'(text: string) {
        check(this.userId, String);
        return MessagesCollection.insert({
            text,
            userId: <string>this.userId,
            createdAt: new Date(),
        });
    }
});

Each meteor method needs a unique name, just like a route in a REST api. We are calling our method messages.add. I like this pattern of &lt;entity>.&lt;action> for method names which makes it very easy to understand what a method does just from its name. Our method expects 1 parameter, a string containing the text for the message.

First thing the method does is it checks that this.userId is a string. The this.userId here should raise your curiosity a bit. Meteor is smart enough to inject the _id of the logged in user into every method call’s context on the server which makes it extremely easy to run operations behind a security wall within methods. If a non-logged in user triggers this method call, this.userId will be falsy and the check will throw an error preventing the message entry insertion into the database.

Inside the method body, we simply call MessagesCollection.insert and pass an object with the text from the client, a new Date() for createdAt timestamp and this.userId for the userId field. .insert returns the _id of the new entry which is then returned back to the caller of the method on the client.

Now, we need a way to retrieve messages from the database to show on the client. In the REST paradigm, you’d use an HTTP GET request for this. The meteor equivalent of that is a publication but it’s also way more powerful than just an HTTP request. A subscription can publish a subset of the data from your database based on any mongodb query. Then from the client side, you’d subscribe to said publication to get the published data. The real magic in this is that whenever that data changes, on the server, it will publish the new data to all the subscribed clients. In our case, we would publish all the messages in our database and every user who has the app opened will subscribe to that publication. Then any of the users sending new messages will be propagated to all those subscribed users in real time. If you can imagine how complex the above is to achieve in traditional REST API apps, you will be amazed how simple meteor makes this. Open up the file server/publications.ts and put the following code in it:

import { check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
import {MessagesCollection} from "/imports/api/messages/collection";

Meteor.publish('messages', function() {
    check(this.userId, String);

    return [
        MessagesCollection.find(),
        Meteor.users.find({}, {fields: {username: 1}}),
    ];
});

Similar to methods, each publication in meteor requires a unique name. We are calling it just messages. Just like methods, publications also inherit some contextual info from meteor through the “this" scope such as userId and just like our method, we only want logged in users to see the messages. Each publication can return one or multiple mongo cursor/s. In our case, besides messages, we want to return the username of the senders of the messages. For message entries, we want to publish all the fields but for users, we limit the publication to only the username field. This allows us to keep all the email addresses and other info about users private.

Keep in mind that in a real world app, this would not float. We are essentially querying ALL the messages and users from the database which can become super heavy as soon as you have a few users texting everyday with each other. However, scaling meteor apps for a large user base is an entirely different topic of its own so we will not be getting into that in this post.

Now, all we have to do is, make sure our meteor app is aware of the publication and the method by importing these inside the server/main.ts file in the root of our meteor project:

import '../imports/api/messages/server/methods';
import '../imports/api/messages/server/publications';

This is all we need to do on our meteor app. Let’s get back to our client side code now and integrate with all of this. First, create a new file in the root of the expo project named messages.ts and put the following code in it:

import {Mongo} from '@meteorrn/core';
import { IMessage } from 'react-native-gifted-chat';

export interface Message extends IMessage {
    text: string;
    userId: string;
}

export const MessagesCollection = new Mongo.Collection('messages');

With a little bit of webpack magic we could share the meteor app’s code with the expo code but to keep this post on track, we will be duplicating some code instead. Here we are defining the Message interface and creating an instance of the MessagesCollection just like we did in our meteor code. A slight difference in the interface definition is that we are extending the IMessage interface from gifted-chat package and the collection instance in this case will be a minimongo collection.

Then, replace all our previous code from the chat-page.tsx file and put the following code in:

import React, {useCallback} from 'react';
import Meteor, {withTracker} from '@meteorrn/core';
import { GiftedChat, User } from 'react-native-gifted-chat';
import {KeyboardAvoidingView, Platform} from "react-native";
import {Message, MessagesCollection} from "./messages";

type ChatPageProps = {
    user: User,
    messages: Message[]
}

function ChatPage({ user, messages }: ChatPageProps) {
    const onSend = useCallback((allMessages = []) => {
        Meteor.call('messages.add', allMessages.slice(-1)?.[0]?.text);
    }, []);

    return (
        <>
            <GiftedChat
                user={user}
                messages={messages}
                renderUsernameOnMessage
                onSend={messages => onSend(messages)}
            />
            { Platform.OS === 'android' && <KeyboardAvoidingView behavior="padding" /> }
        </>
    );
};

export default withTracker(() => {
    const user = Meteor.user();
    const messagesReady = Meteor.subscribe('messages').ready();
    const messages = MessagesCollection.find({}, {sort: {createdAt: -1}}).fetch().map(message => {
        const user = Meteor.users.findOne(message.userId);
        return {
            ...message,
            user: {name: user.username, _id: user._id}
        };
    });
    return {loading: !messagesReady, messages, user};
})(ChatPage);

We do have some of the previous code in here but there’s a lot more additional code and changes so explaining this from the top would be more helpful. First, we converted our component’s default export into a local component which is then wrapped in the withTracker HOC before being exported.

Inside the withTracker HOC, we are creating a subscription to the messages publication that we just created. The Meteor.subscribe() method returns a handle with a ready() method which reactively returns true when the subscription is ready with all the data from publication. Then, we are querying the Messages collection to find all entries and sorting it in descending order using the createdAt timestamp. Notice the use of the fetch() method after the find()? We didn’t have that on the server side publication, did we? find returns a cursor whereas fetch returns actual results from that cursor in an array which is why on the client, we need to use fetch on the cursor. Since gifted chat expects a user object in every message containing author info but the message entry only contains userId, we map through every message and extend it with the author info. Notice that the author info is retrieved from a findOne query which runs on the Meteor.users collection which we didn’t have to create/define ourselves.

Finally, we return 3 props for our ChatPage component: loading, messages and user. loading refers to the subscription state, if it is ready or not based on which, we could show a fancy loading screen of sort if we wanted to. messages contain all the message entries with author info embedded. user containing the currently logged in user’s info.

Inside the component itself, we only have one function onSend which is passed down to GiftedChat component along with messages and user props inherited from previously discussed HOC. Gifted chat’s onSend event is triggered with all the messages so inside our onSend handler function, we slice the messages to get only the last entry and from that, we extract the text. The text from the message is all we need to pass to our meteor method that adds the message to our database collection so we call the method with that value like this: Meteor.call('messages.add', allMessages.slice(-1)?.[0]?.text);

That’s all folks! You should now have a fully functional group chat app built with expo and meteor.

Take it for a spin

To test it out, you will need at least 1 device and an emulator or 2 physical devices. From both of those, open the app and create a new account. Once you get to the message screen with an input, start sending messages from both apps and see them appear in real time on the other one.

What’s next?

You’ve probably used a bunch of messaging apps so feel free to pick any feature you have seen in any of those apps and start implementing it. Here are a few ideas: \

  1. Push notification: Send push notification to everyone when someone sends a new message. Maybe go one step forward and allow users to turn push notifications on/off.
  2. Stories: A very common feature on every social media and messaging app is Stories. Allow every user to upload a picture and add a row of stories that users can browse through and open. Add an expiration time to the stories so that they disappear after a certain amount of time.
Published 9 Feb 2021

I write a lot of code at my day job, side hustles and for fun.
Foysal Ahamed on Twitter