Build a serverless chat app with Svelte and Firebase (PART 3)
Tags: tutorial, webdev, svelte, javascript
Published: 2020-11-06 17:18:40

In the last two parts, we set up the app and configured authentication. Now, let's get to the juicy stuff, Chatting!

We'll be using Firebase Firestore as our database. Firestore is a NoSQL Document database, like MongoDB. It also has a really cool feature of listening to database changes, which allows us to make this chat app without using stuff like [socket.io](https://socket.io).

We created a messages collection in Firestore. This is where we'll be storing our messages. So let's get started!

> Be sure to check the first two parts out before continuing with this one! All code is available on [Github](https://github.com/arnu515/serverless-chat)

## The `chat` route

Let's create a `/chat` route that will only work **if the user is authenticated**.

But first, we have to make sure that our Auth component doesn't allow the user to authenticate if they are already authenticated. This simple line of code will do:

```html
<!-- src/components/Auth.svelte -->

<script lang="ts">
    // ...

    auth.onAuthStateChanged(user => {
        isAuthenticated = !!user;
        if (user) d("auth")
    })

    // ...
</script>

<!-- ... -->
```

The `auth.onAuthStateChanged()` function is called by Firebase everytime the user's auth state changes, i.e. whenever they login and logout.

Let's move on to our `/chat` route.

```html
<!-- src/routes/chat.svelte -->

<script lang="ts">
    import { auth } from "../services/firebase";
    import router from "page";

    interface User {
        email: string, photoURL: string, uid: string
    }

    let user: User | null;

    auth.onAuthStateChanged(u => user = u);

    $: {
        // user === null is explicitly called instead of !user because we need firebase to decide what the user is, and not us, so we dont initialise user up there.
        if (user === null) router.redirect("/auth?action=login&next=%2Fchat");
    }
</script>
```

The funny little `$: ` block you're seeing there is not vanilla javascript. It is svelte magic. The code in that block is called whenever any dependencies (i.e. variables initialised **outside** that block) change. It's like the `useEffect` hook in React.

Now, let's do the UI:

```html
<!-- src/routes/chat.svelte -->

<!-- ... -->

{#if typeof user === "undefined"}
    <p class="w3-center w3-section"><i class="fas fa-spinner w3-spin fa-3x"></i> Loading</p>
{:else}
    {#if user}
        <h1 class="w3-jumbo w3-center">Serverless chat</h1>
        <p class="w3-center">Chatroom</p>
        <p class="w3-center"><button class="w3-button w3-blue" on:click={logout}>Logout</button></p>

        <br>
        <div class="w3-container w3-border w3-border-gray" style="margin: 0 auto; width: 60%; height: 600px; overflow-y: auto;">
            <br>
            {#if messages.length > 0}
                {#each messages as m}
                    <Chat {...m} self={user.uid === m.uid} />
                {/each}
            {:else}
                <p class="w3-center w3-text-gray">Looks like nobody's sent a message. Be the first!</p>
            {/if}
            <!-- Dummy element used to scroll chat -->
            <br id="scroll-to">
        </div>
        <input on:keydown={messageSubmit} type="text" style="margin: 0 auto; width: 60%; margin-top: -1px" placeholder={cooldown ? "3 second cooldown" : "Enter message and press enter"} class="w3-input w3-border w3-border-gray {cooldown && "w3-pale-red"}" id="message-input">
        <br>
    {:else}
        <p class="w3-center w3-section">Not logged in!</p>
    {/if}
{/if}
```

Now, we'll update the javascript inside the svelte component. We'll create all the variables referenced in the HTML.

```html
<!-- src/routes/chat.svelte -->

<script lang="ts">
    import {auth} from "../services/firebase"
    import router from "page";

    interface User {
        email: string, photoURL: string, uid: string
    }

    // new
    interface Message extends User {
        message: string, createdAt: number
    }

    let user: User | null;
    // new
    let messages: Message[] = [];
    let cooldown = false;

    auth.onAuthStateChanged(u => user = u);

    $: {
        if (user === null) router.redirect("/auth?action=login&next=%2Fchat");
    }

    // new
    function messageSubmit(e: KeyboardEvent & {
        currentTarget: EventTarget & HTMLInputElement;
    }) {
    }

    // new
    function logout() {
        if (auth.currentUser) {
            auth.signOut().then(() => {}).catch(e => {
                throw new Error(e)
            });
        }
    }
</script>

<!-- ... -->
```

Now, all but one of the squiggly lines in your code should disappear.

## How a chat message will look like

No, I'm not talking about visual looks, but I'm talking about how a message will be structured in our database. This is the thing im going for:

```ts
{
   // the main content of the message
   message: string,
   // the id of the user who posted the message
   uid: string,
   // the email of the user who posted the message
   email: string,
   // the avatar of the user who posted the message (URL)
   photoURL: string,
   // the timestamp when the message was created
   createdAt: number
}
```

## Chat component

Let's make a component that will render each of the chat messages:

```html
<!-- src/components/Chat.svelte -->
<script lang="ts">
    import md5 from "md5";

    export let photoURL: string;
    export let createdAt: number;
    export let email: string;
    export let message: string;
    export let uid: string;
    // if the message was made by the current user
    export let self = false;
</script>

<div title={`${email} (${uid}) at ${new Date(createdAt)}`} style="display: flex; margin-bottom: 0.5rem; {self && "flex-direction: row-reverse; "}align-items: center;">
    <img src={photoURL || `https://www.gravatar.com/avatar/${md5(email)}?d=mp&s=32&r=g`} style="width: 32px; height: 32px;" class="w3-circle" alt="avatar">
    <span class="{self ? "w3-blue" : "w3-light-gray"} w3-padding w3-round-xxlarge" style="margin: 0 6px">{message}</span>
</div>
```

> Notice in the above code, we're using [Gravatar](https://gravatar.com) to give us the user's avatar if it doesn't exist.

## Syncing with the database in REAL TIME

Now, let's do the juicy part! Let's sync our app to firestore. It is WAY easier than you think. Here's the code we need:

```ts
db.collection("messages").onSnapshot((snapshot) => {
        snapshot.docChanges().forEach(change => {
            if (change.type === "added") {
                messages = [...messages, change.doc.data() as Message]
                setTimeout(() => {if (document.getElementById("scroll-to")) document.getElementById("scroll-to").scrollIntoView({behavior: "smooth"});}, 500)
            }
        })
    })

```

Let's implement this in our app!
```html
<!-- src/routes/chat.svelte -->

<script lang="ts">
    import {auth, db} from "../services/firebase";
    import router from "page";
    import Chat from "../components/Chat.svelte";
    import {onDestroy} from "svelte";

    // ...

    const unsubscribe = db.collection("messages").onSnapshot((snapshot) => {
        snapshot.docChanges().forEach(change => {
            if (change.type === "added") {
                messages = [...messages, change.doc.data() as Message]
                setTimeout(() => {if (document.getElementById("scroll-to")) document.getElementById("scroll-to").scrollIntoView({behavior: "smooth"});}, 500)
            }
        })
    })

    // calling the unsubscribe() method when the component gets destroyed to prevent listening to changes when not needed. Also, bandwith gets saved.
    onDestroy(unsubscribe);
</script>

<!-- ... -->
```

> Lost track? [The Github can help](https://github.com/arnu515/serverless-chat)

## Add chat messages

Let's finish this off by adding functionality to add chat messages. It's very simple. All you have to do, is modify the `messageSubmit()` event handler to add messages in the database. Along with that, I also decided to add functionality to censor bad words using the `bad-words` npm package.

```sh
npm install bad-words
```

And here's what our code finally looks like:
```html
<!-- src/routes/chat.svelte -->

<script lang="ts">
    import {auth, db} from "../services/firebase"
    import router from "page";
    import { onDestroy } from "svelte";
    import Chat from "../components/Chat.svelte";
    import Filter from "bad-words";

    interface User {
        email: string, photoURL: string, uid: string
    }

    interface Message extends User {
        message: string, createdAt: number
    }

    let user: User | null;
    let messages: Message[] = [];
    let cooldown = false;

    auth.onAuthStateChanged(u => user = u);

    $: {
        if (user === null) router.redirect("/auth?action=login&next=%2Fchat");
    }

    const unsubscribe = db.collection("messages").onSnapshot((snapshot) => {
        snapshot.docChanges().forEach(change => {
            if (change.type === "added") {
                messages = [...messages, change.doc.data() as Message]
                setTimeout(() => {if (document.getElementById("scroll-to")) document.getElementById("scroll-to").scrollIntoView({behavior: "smooth"});}, 500)
            }
        })
    })

    function messageSubmit(e: KeyboardEvent & {
        currentTarget: EventTarget & HTMLInputElement;
    }) {
        if (e.key.toLowerCase() !== "enter") return;
        if (cooldown) return;
        const message = (new Filter()).clean(((document.getElementById("message-input") as HTMLInputElement).value || "").trim());
        if (!message) return;

        (document.getElementById("message-input") as HTMLInputElement).value = ""
        cooldown = true;
        setTimeout(() => cooldown = false, 3000)

        db.collection("messages").add({
            message,
            email: user.email,
            photoURL: user.photoURL,
            uid: user.uid,
            createdAt: Date.now()
        })
    }

    onDestroy(unsubscribe)

    function logout() {
        if (auth.currentUser) {
            auth.signOut().then(() => {}).catch(e => {
                throw new Error(e)
            });
        }
    }
</script>

{#if typeof user === "undefined"}
    <p class="w3-center w3-section"><i class="fas fa-spinner w3-spin fa-3x"></i> Loading</p>
{:else}
    {#if user}
        <h1 class="w3-jumbo w3-center">Serverless chat</h1>
        <p class="w3-center">Chatroom</p>
        <p class="w3-center"><button class="w3-button w3-blue" on:click={logout}>Logout</button></p>

        <br>
        <div class="w3-container w3-border w3-border-gray" style="margin: 0 auto; width: 60%; height: 600px; overflow-y: auto;">
            <br>
            {#if messages.length > 0}
                {#each messages as m}
                    <Chat {...m} self={user.uid === m.uid} />
                {/each}
            {:else}
                <p class="w3-center w3-text-gray">Looks like nobody's sent a message. Be the first!</p>
            {/if}
            <!-- Dummy element used to scroll chat -->
            <br id="scroll-to">
        </div>
        <input on:keydown={messageSubmit} type="text" style="margin: 0 auto; width: 60%; margin-top: -1px" placeholder={cooldown ? "3 second cooldown" : "Enter message and press enter"} class="w3-input w3-border w3-border-gray {cooldown && "w3-pale-red"}" id="message-input">
        <br>
    {:else}
        <p class="w3-center w3-section">Not logged in!</p>
    {/if}
{/if}
```

## Test the app

Yay! We're done. Feel free to mess around and send chat messages to your friend. Try using two browser tabs (with different accounts, ofcourse) and see how the chat updates real-time!

That's all for this part! There is one final part, however, and that will teach you how to **deploy** your code. [And here's the link!](https://dev.to/arnu515/build-a-serverless-chat-app-with-svelte-and-firebase-part-4-1m6d)