Building a Web3 Chat App in 1 Day Using Push Protocol

Building a Web3 Chat App in 1 Day Using Push Protocol

Introduction

Web3 has revolutionized how we interact with decentralized applications (dApps), and communication is a crucial aspect of enhancing user experience. In this blog post, we'll guide you through creating a Web3 chat app in just one day, leveraging the power of Push Protocol. Push Protocol is a web3 communication network that facilitates cross-chain notifications and messaging for dApps, wallets, and services.

Illustration showing Push as the missing piece of web3

We'll use a powerful tech stack including Next.js for front-end development, Tailwind CSS for styling, the Push Protocol SDK for communication, React Redux for state management, and Wagmi for wallet integration. Let's dive in!

Deployment & Repo

The website is live at https://quick3-messenger.vercel.app/

Check the repository out on Github: https://github.com/AnoyRC/quick3-messenger

Drop a Star and follow to connect!

Check the Docs for a more detailed walkthrough: https://push.org/docs/

Prerequisites

Before we start, make sure you have Node.js and npm installed on your machine.

Step 1: Set Up Your Next.js App

Create a new Next.js app using the following command:

npx create-next-app@latest

My Configuration :

Step 2: Install Dependencies

Install the required packages for Push Protocol, React Redux, Wagmi, and Tailwind CSS:

npm i @pushprotocol/restapi @reduxjs/toolkit react-redux ethers@5.7 viem@1.19.13 wagmi@1.4.10 boring-avatars @material-tailwind/react @heroicons/react

@heroicons/react for Icons

@material-tailwind/react for UI Components

boring-avatars for Generating avatars based on the address of the user

We are using a specific version of ethers@5.7 viem@1.19.13 and wagmi@1.4.10 . (I consider them the best version for the development of web3 Dapps)

Setting up React-Redux and Toolkit

Creating a store and a slice

Create a folder and name it redux. Inside that create a store.js file.

"use client";

import { configureStore } from "@reduxjs/toolkit";
import pushSlice from "@/redux/slice/pushSlice";

export const store = configureStore({
  reducer: {
    push: pushSlice,
  },

  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: false,
    }),
});

Now, you will notice there is an unknown file import named pushSlice. Let's make that.

Create a folder and name it slice. Inside that create a pushSlice.js file.

import { createSlice } from "@reduxjs/toolkit";

const pushSlice = createSlice({
  name: "push",

  initialState: {
    user: null,
    chats: null,
    requests: null,
    stream: null,
    data: null,
    addDialog: false,
  },

  reducers: {
    setUser: (state, action) => {
      state.user = action.payload;
    },
    setChats: (state, action) => {
      state.chats = action.payload;
    },
    setRequests: (state, action) => {
      state.requests = action.payload;
    },
    setStream: (state, action) => {
      state.stream = action.payload;
    },
    setData: (state, action) => {
      state.data = action.payload;
    },
    handleAddDialog: (state, action) => {
      state.addDialog = !state.addDialog;
    },
  },
});

export const {
  setUser,
  setChats,
  setRequests,
  setStream,
  setData,
  handleAddDialog,
} = pushSlice.actions;

export default pushSlice.reducer;

These are global states that would be needed later on. For now, let's keep the variables and function names that way.

Creating a provider

As Nextjs runs both on the server side and the client side. We need to specify if we need to use the client or not. And for using Redux and its toolkit, we need all the files working on the client side.

Go to the root folder. Create a new folder and name it providers. Inside that create a reduxProvider.js file.

"use client";

import { store } from "@/redux/store";
import { Provider } from "react-redux";

export const ReduxProvider = ({ children }) => {
  return <Provider store={store}>{children}</Provider>;
};

export default ReduxProvider;

And lastly, let's add that inside our layout.js file located inside our app folder.

import { Poppins } from "next/font/google";
import "./globals.css";
import ReduxProvider from "@/providers/reduxProvider";

const poppins = Poppins({
  subsets: ["latin"],
  weight: ["200", "300", "400", "500", "600", "700", "800", "900"],
});

export const metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body className={poppins.className}>
        <ReduxProvider>
          {children}
        </ReduxProvider>
      </body>
    </html>
  );
}

Congratulations !! You have successfully integrated react-redux in our app.

Happy International Cat Day GIF

Setting Up Wagmi

Creating a Provider

For this guide, we would only connect to Metamask and Coinbase Wallet. Open the providers folder that we created earlier. Create a wagmiProvider.js file.

"use client";
import { WagmiConfig, createConfig, configureChains, mainnet } from "wagmi";
import { publicProvider } from "wagmi/providers/public";
import { MetaMaskConnector } from "wagmi/connectors/metaMask";
import { CoinbaseWalletConnector } from "wagmi/connectors/coinbaseWallet";

const { chains, publicClient, webSocketPublicClient } = configureChains(
  [mainnet],
  [publicProvider()]
);

const config = createConfig({
  autoConnect: true,
  connectors: [
    new MetaMaskConnector({ chains }),
    new CoinbaseWalletConnector({
      chains,
      options: {
        appName: "wagmi",
      },
    }),
  ],
  publicClient,
  webSocketPublicClient,
});

export const WagmiProvider = ({ children }) => {
  return <WagmiConfig config={config}>{children}</WagmiConfig>;
};

Lastly, Add the provider to our layout.js file located inside our app folder.

import { Poppins } from "next/font/google";
import "./globals.css";
import { WagmiProvider } from "@/providers/wagmiProvider";
import ReduxProvider from "@/providers/reduxProvider";

const poppins = Poppins({
  subsets: ["latin"],
  weight: ["200", "300", "400", "500", "600", "700", "800", "900"],
});

export const metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body className={poppins.className}>
        <ReduxProvider>
          <WagmiProvider>{children}</WagmiProvider>
        </ReduxProvider>
      </body>
    </html>
  );
}

Congratulations !! Wagmi is ready to use. Let's connect some wallets. Shall we?

Disney gif. Retro animation of Donald Duck walking along happily while counting money.

Frontend File Structure

Let's Make a simple file structure and UI for our client. After all, we need to complete it in 1 Day.

Routes

  1. "/" - We have a home page where users will connect and initiate push protocol.

  2. "/dashboard" - a dashboard to see all the chats and requests in one place.

  3. "/chat/:id" - a chat window where the users can chat with each other.

UI

  • Connect window in "/" route.

  • Sign window in "/" route.

  • All Chats in the "/dashboard" route

  • All Chat Requests in the "/dashboard" route

  • Chat window in the "/chat/:id" route

  • Add Contact Dialog in the "/dashboard" route

  • Fallback Handler on both "/dashboard" and "/chat/:id"

The UI is made using material tailwind, see the GitHub Repository for code.

Push Protocol Basics

Initialize Push

Source: Link

const user = await PushAPI.initialize(signer, {
    env: CONSTANTS.ENV.PROD,
});

Use initialize function immediately after connecting (using useEffect maybe) or assign this function to a button when the user has connected with their wallet.

The user will be needed for all other function calls, so store it in redux, so that we can globally access the object.

if (user) {
    if (!user.readMode) {
        dispatch(setUser(user));
        streamChat(user);
        router.push("/dashboard");
    }
}

Source: Link

Check for user.readMode as it checks if the user has signed or not.

Stream

Source: Link

const streamChat = async (user) => {
    const stream = await user.initStream(
      [CONSTANTS.STREAM.CHAT, CONSTANTS.STREAM.CHAT_OPS],
      {
        filter: {
          channels: ["*"],
          chats: ["*"],
        },
        connection: {
          retries: 3,
        },
        raw: false,
      }
    );

    //Check for Changes in Chat or others
    stream.on(CONSTANTS.STREAM.CHAT, (data) => {
      dispatch(setData(data));
    });

    //Check for Changes in Chat Operations
    stream.on(CONSTANTS.STREAM.CHAT_OPS, (data) => {
      dispatch(setData(data));
    });

    // Connect 
    stream.connect();

    //Set Globally to Disconnect Later
    dispatch(setStream(stream));
  };

Stream checks for any chats, chat requests, or messages received from any user. Try to connect to the stream as soon as the user initializes push. So that you can get live updates of chats and chat requests.

Save the stream globally, so that you can disconnect the stream when the user has logged out. And store the data received from the stream to check for changes.

Source: Link

const stream = useSelector((state) => state.push.stream);

if (stream) {
    stream.disconnect();
}

Fetch Chats / Contacts

Source: Link

const fetchChats = async () => {
    const chat = await user.chat.list("CHATS");
    dispatch(setChats(chat));
};

Fetch Chats of the users you are already talking to. Use this function when loading into the dashboard to show the existing contacts

Fetch Chat Requests

Source: Link

const fetchRequests = async () => {
    const requests = await user.chat.list("REQUESTS");
    dispatch(setRequests(requests));
};

The first Message from any user becomes a chat request. This feature is given to avoid spam from random users. Fetch Chat requests on the request tab of the dashboard.

Accept Chat Requests

Source: Link

const acceptRequest = async (walletAddress) => {
    await user.chat.accept(walletAddress);
    fetchChats();
    fetchRequests();
};

Accept Chat Requests received from the given wallet address. Use this function where you let the user accept chat requests from other unknown wallet addresses.

Reject Chat Requests

Source: Link

const rejectRequest = async (walletAddress) => {
    await user.chat.reject(walletAddress);
    fetchChats();
    fetchRequests();
};

Reject Chat Requests received from the given wallet address. Use this function where you let the user reject chat requests from other unknown wallet addresses.

Fetch Chat History

Source: Link

 const fetchHistory = async () => {
    const history = await user.chat.history(did);
    setHistory(history);
 };

Fetch Chat history based on the given did. Did can be found in the user object when you Fetch Chats / Contacts. Fetch Chat history when the user opens a particular chat to send a message.

Send Message

Source: Link

const sendMessage = async () => {
    await user.chat.send(did, {
      type: "Text",
      content: message,
    });
    setMessage("");
    fetchHistory();
};

Sends the message after taking input from the user. The message can be any type, for example, file, gif, image, or text. For this project, we are only sending text messages.

Conclusion

Test your Web3 chat app locally using npm run dev. Once satisfied, deploy it to your preferred hosting platform.

Congratulations! You've successfully built a Web3 chat app in just one day using Push Protocol, Next.js, Tailwind CSS, React Redux, and Wagmi. This app provides a seamless and decentralized communication experience for users in the Web3 ecosystem. Feel free to enhance and customize it based on your project requirements. Happy coding!