DEV Community

Cover image for ๐Ÿš€ Building a Decentralized Chat App using GUN.js and Svelte
Vedant Chainani
Vedant Chainani

Posted on

๐Ÿš€ Building a Decentralized Chat App using GUN.js and Svelte

The complete code for this tutorial is available here:

GitHub logo Envoy-VC / gun-chat

Decentralized Chat App using GUN.js and Svelte

๐Ÿš€ Live Site: https://gun-chat-zeta.vercel.app/


What is GUN

GUN.jsย aย decentralizedย graphย databaseย forย freedomย fighters.

Graph database applications have grown dramatically during the last few years. Graph databases, for example, are already utilized by Facebook for their social media platform, Stripe for fraudulent transactions, Amazon for product recommendation, and companies all over the world for big data analytics in a variety of sectors and challenges.

Unlike a centralized database that stays on a server maintained by big tech, data in GUN is dispersed between several peers or users using the power of WebRTC. This decentralized database functions exactly like a cloud database from the developerโ€™s perspective; however, it is hosted on a completely peer to peer network. It is not a blockchain, but leverages some of the same cryptographic algorithms, such as Patricia-Merkle trees.

In this post, we'll utilize GUN to build a decentralized chat software that employs user authentication and end-to-end encryption.


Screenshots

Login Page

Chat Section


Step 1 - Setting up the Environment

We will use slimline as our frontend framework, so create a new project directory called 'chat-app' and run command to initialize npm.

npm init -y
Enter fullscreen mode Exit fullscreen mode

to create a svelte app run

npm create vite@latest chat-app --template svelte
Enter fullscreen mode Exit fullscreen mode

Following that, we will require a gun for storage and to install run

npm install gun
Enter fullscreen mode Exit fullscreen mode

Step 2 - Implementing User Authentication

Create a new file called user.js in the project's src subdirectory, where we will implement our user authentication.

Following that, we will import Gun as well as the side libraries known as sea and axe. sea represents security, encryption, and authorization, and it allows for user authentication. axe stands for advanced exchange equation, and it is a different technique to link peers.

import GUN from 'gun';
import 'gun/sea';
import 'gun/axe';
Enter fullscreen mode Exit fullscreen mode

Following that, we will initialize our database and make a database reference to the currently authenticated user.

// Database
export const db = GUN();

// Gun User
export const user = db.user().recall({sessionStorage: true});
Enter fullscreen mode Exit fullscreen mode

We used sessionStorage: true to prevent the user from being logged out while closing and reopening tabs.

Next, we'll need the username, which will be used frequently in the app, so we'll import writable from svelte/store at the start of the file to make the app more responsive.

import { writable } from 'svelte/store';
Enter fullscreen mode Exit fullscreen mode

To obtain the user's alias, we will use the get method.

// Current User's username
export const username = writable('');

user.get('alias').on(v => username.set(v))
Enter fullscreen mode Exit fullscreen mode

We will create a on event function that will update the user's username whenever he or she logs in and out.


Step 3 - Creating Header

Now we'll make a header for our app that will display our username and avatar when we sign up or login in. In the src directory, create a new file called Header.svelte.

We will import the user database and the username from the user.js file into the script tag. We'll also write a signout function that logs the user out and sets the username to an empty string.

<script>
  import { username, user } from './user';

  function signout() {
    user.leave();
    username.set('');
  }
</script>
Enter fullscreen mode Exit fullscreen mode

In the header tag, we will create a conditional block that will display the username along with a unique avatar generated using the DiceBear API if the username is not an empty string.

<header>
<h1>๐Ÿ”ซ Chat App</h1>
  {#if $username}
    <div class="user-bio">

      <span>Hello <strong>{$username}</strong></span>
      <img src={`https://avatars.dicebear.com/api/human/${$username}.svg`} alt="avatar" /> 
    </div>

    <button class="signout-button" on:click={signout}>Sign Out</button>
  {/if}
</header>
Enter fullscreen mode Exit fullscreen mode

Step 4 - Creating Login form

Now we'll design our login and sign up forms, where users may log in or create new accounts. Make a new file called Login.svelte for this purpose.

We will import the user database from the user.js file into the script tag. In addition, we will create state variables for username and password.

Then we'll create two functions: login and signup. In the login function, we will utilize the auth method to validate the user. We will also handle errors and notify the user if any input is incorrect.

We will create a new item in the database using the username and password in the sign up method.

<script>
  import { user } from './user';

  let username;
  let password;

  function login() {
    user.auth(username, password, ({ err }) => err && alert(err));
  }

  function signup() {
    user.create(username, password, ({ err }) => {
      if (err) {
        alert(err);
      } else {
        login();
      }
    });
  }
</script>
Enter fullscreen mode Exit fullscreen mode

Following that, we'll add input fields for the username and password, as well as use the bind:value method in Svelte to update the state variables when the input changes.

<label for="username">Username</label>
<input name="username" bind:value={username} minlength="3" maxlength="16" />

<label for="password">Password</label>
<input name="password" bind:value={password} type="password" />

<button class="login" on:click={login}>Login</button>
<button class="login"  on:click={signup}>Sign Up</button>
Enter fullscreen mode Exit fullscreen mode

Step 5 - Building Chat Component

To begin, we will import various modules and global state variables while developing our chat component, so create a new file called Chat. svelte .

  import Login from './Login.svelte';
  import ChatMessage from './ChatMessage.svelte';
  import { onMount } from 'svelte';
  import { username, user } from './user';
  import debounce from 'lodash.debounce';
  import GUN from 'gun';

Enter fullscreen mode Exit fullscreen mode

Now we'll declare some new state variables named newMessage, which will store the message, the messages array, and some minor scrolling feature variables.

  const db = GUN();

  let newMessage;
  let messages = [];

  let scrollBottom;
  let lastScrollTop;
  let canAutoScroll = true;
  let unreadMessages = false;
Enter fullscreen mode Exit fullscreen mode

Then we'll write some scroll methods that will monitor chat and auto-scroll as new messages arrive.

  function autoScroll() {
    setTimeout(() => scrollBottom?.scrollIntoView({ behavior: 'auto' }), 50);
    unreadMessages = false;
  }

  function watchScroll(e) {
    canAutoScroll = (e.target.scrollTop || Infinity) > lastScrollTop;
    lastScrollTop = e.target.scrollTop;
  }

  $: debouncedWatchScroll = debounce(watchScroll, 1000);
Enter fullscreen mode Exit fullscreen mode

Next, we'll utilize the onMount hook, which will be executed when the component is initialized. It contains a match variable, which functions similarly to a RegEx, and will look for any messages that are less than 3 hours old.

onMount(() => {
    var match = {
      // lexical queries are kind of like a limited RegEx or Glob.
      '.': {
        // property selector
        '>': new Date(+new Date() - 1 * 1000 * 60 * 60 * 3).toISOString(), // find any indexed property larger ~3 hours ago
      },
      '-': 1, // filter in reverse
    };
});
Enter fullscreen mode Exit fullscreen mode

Following that, we will retrieve the chat by using the get method, map on it once, and if any data is received, we will create a new variable that will store the same key that will aid in decrypting the data.

Then we will reformat the data as per our convenience in a new message object with keys - who, what and when. In the who key we will store the username of the sender, in the what key we will store the message after decrypting it using the key and in the when key we will store the timestamp of the message.

If a new message is received, we will perform an if conditional block that will store it in the messages array.

db.get('chat')
      .map(match)
      .once(async (data, id) => {
        if (data) {
          // Key for end-to-end encryption
          const key = '#foo';

          var message = {
            // transform the data
            who: await db.user(data).get('alias'), // a user might lie who they are! So let the user system detect whose data it is.
            what: (await SEA.decrypt(data.what, key)) + '', // force decrypt as text.
            when: GUN.state.is(data, 'what'), // get the internal timestamp for the what property.
          };

          if (message.what) {
            messages = [...messages.slice(-100), message].sort((a, b) => a.when - b.when);
            if (canAutoScroll) {
              autoScroll();
            } else {
              unreadMessages = true;
            }
          }
        }
Enter fullscreen mode Exit fullscreen mode

Now we will construct a new function sendMessage that will allow users to send messages, and then we will insert the message into the database using the put method.

  async function sendMessage() {
    const secret = await SEA.encrypt(newMessage, '#foo');
    const message = user.get('all').set({ what: secret });
    const index = new Date().toISOString();
    db.get('chat').get(index).put(message);
    newMessage = '';
    canAutoScroll = true;
    autoScroll();
  }
Enter fullscreen mode Exit fullscreen mode

We will now create the UI for the chat component, which will loop through all of the messages in the messages array based on the timestamp. In addition, we will create a new input field for users to send messages.

Then we'll add some scrolling elements that will scroll to the bottom when fresh messages arrive.


<div class="container">
  {#if $username}
    <main on:scroll={debouncedWatchScroll}>
      {#each messages as message (message.when)}
        <ChatMessage {message} sender={$username} />
      {/each}

      <div class="dummy" bind:this={scrollBottom} />
    </main>

    <form on:submit|preventDefault={sendMessage}>
      <input type="text" placeholder="Type a message..." bind:value={newMessage} maxlength="100" />

      <button type="submit" disabled={!newMessage}>๐Ÿ’ฅ</button>
    </form>


    {#if !canAutoScroll}
    <div class="scroll-button">
      <button on:click={autoScroll} class:red={unreadMessages}>
        {#if unreadMessages}
          ๐Ÿ’ฌ
        {/if}

        ๐Ÿ‘‡
      </button>
    </div>
   {/if}
  {:else}
    <main>
      <Login />
    </main>
  {/if}
</div>
Enter fullscreen mode Exit fullscreen mode

Step 5 - Create Message Bubble

Now we'll make a chat message bubble, so make a new file called ChatMessage.svelte and paste the following code into it:

<script>
  export let message;
  export let sender;

  const messageClass = message.who === sender ? 'sent' : 'received';

  const avatar = `https://avatars.dicebear.com/api/human/${message.who}.svg`;

  const ts = new Date(message.when);
</script>

<div class={`message ${messageClass}`}>
  <img src={avatar} alt="avatar" />
  <div class="message-text">
    <p>{message.what}</p>

    <time>{ts.toLocaleTimeString()}</time>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Step 6 - Finalizing

We are nearly finished with our app; simply go to App.svelte and import the header and chat components into our app component.

<script>
  import Chat from './Chat.svelte';
  import Header from './Header.svelte';
</script>

<div class="app">
    <Header />
    <Chat />
</div>
Enter fullscreen mode Exit fullscreen mode

Conclusion

First and foremost, congratulations ๐ŸŽ‰๐ŸŽ‰ on making it this far. You have successfully built a decentralized chat app utilizing GUN.js and svelte as the frontend framework.

This article was inspired by a Fireship video:

This article demonstrates what you can build using GUN.js. Feel free to improve on the project by:

  • Adding User to User E2E Encryption
  • Improving the UI

The complete code for this tutorial is available here:

GitHub logo Envoy-VC / gun-chat

Decentralized Chat App using GUN.js and Svelte

Thank you for reading!

If you enjoyed this essay and would like to see more on web3, blockchain, and decentralization, make sure to follow and connect with me.



Top comments (0)