Revolutionize Your Web3 Communication with Push Chat: Say Hello to Secure, Decentralized Messaging!

Revolutionize Your Web3 Communication with Push Chat: Say Hello to Secure, Decentralized Messaging!

Empowering Users and Developers in the Decentralized Web.

·

14 min read

In the rapidly evolving landscape of web3 communication, traditional messaging platforms are being reimagined to meet the growing demand for secure, decentralized, and privacy-focused solutions. Push Chat emerges as a pioneering protocol that bridges the gap between web3 users and enables seamless communication without compromising on security or privacy.

Before Exploring Push Chat: Let's Get Acquainted with Push:

Push Protocol is a cutting-edge web3 communication protocol that redefines how individuals engage and connect in decentralized environments. Unlike conventional messaging platforms, Push Protocol operates without the need for personal identifiers like phone numbers or email addresses. Instead, it harnesses blockchain technology to facilitate secure and private communication among users, leveraging wallet addresses as unique identifiers.

At its core, Push Protocol offers a comprehensive suite of communication tools:

  • Push Chat: Encrypted messaging

  • Push Notification: decentralized alerts

  • Push Video: wallet-to-wallet video calls.

  • Push Spaces: Token gated way of conducting spaces.

Read more about Push Protocol Introduction here

Introducing Push Chat: A Web3 Messaging Protocol

Push Chat introduces a web3 messaging protocol that enables wallet addresses to send and receive messages securely and privately. Unlike traditional messaging platforms that rely on personal identifiers like phone numbers or email addresses, Push Chat leverages blockchain technology to facilitate direct communication between users using their wallet addresses.

Why Push Chat?

Push Chat offers a range of features and benefits that set it apart from traditional messaging platforms:

  • Enhanced Security and Privacy: Push Chat prioritizes security and privacy by encrypting messages and storing them on IPFS. This ensures that conversations remain confidential and secure, with only authorized parties able to access message content.

  • Native Web3 Messaging Experience: With Push Chat, users can enjoy a native web3 messaging experience, communicating directly via their wallet addresses. This eliminates the need for third-party platforms and ensures seamless integration within the web3 ecosystem.

Integrating Push Chat into Your Application:

Now that you've learned about Push Chat and its capabilities, you're ready to integrate it into your application. Whether you're building a decentralized application, a backend service, Push Chat offers the infrastructure and tools you need to enable secure and decentralized communication.

  • Install Push SDK: Begin by installing the Push SDK package using your preferred package manager. For example, you can use npm to install the package.

      npm install @pushprotocol/restapi@latest @pushprotocol/socket@latest ethers@^5.7
    
  • Import Push SDK: Import the necessary modules from the Push SDK package into your application.

      import { PushAPI, CONSTANTS } from "@pushprotocol/restapi";
      import { ethers } from "ethers";
    
  • Initialize User: Initialize the Push API with the user's signer and other optional parameters:

      // Create a random signer from a wallet
      const signer = ethers.Wallet.createRandom();
    
      // Initialize the wallet user with the Push API, specifying the environment as staging
      const userAlice = await PushAPI.initialize(signer, { env: CONSTANTS.ENV.STAGING });
      // Initialize the wallet specifying the environment as production
      const userAlice = await PushAPI.initialize(signer, { env: CONSTANTS.ENV.PROD});
    
      // Retuen if error occurred
      if (userAlice.errors.length > 0) {
        return;
      }
    
  • Send Messages: Once the user is initialized, you can start sending messages to other users or groups:

      // Specify the wallet address of the recipient
      const bobWalletAddress = "0x99A08ac6254dcf7ccc37CeC662aeba8eFA666666";
    
      // Send a message to Bob
      const aliceMessagesBob = await userAlice.chat.send(bobWalletAddress, {
        content: "Hola! It's me Gautam!",
      });
    
  • Receive Messages: Set up a message stream to listen for incoming messages:

      // Initialize Stream
      const stream = await userAlice.initStream([CONSTANTS.STREAM.CHAT]);
    
      // Configure stream listen events and actions
      stream.on(CONSTANTS.STREAM.CHAT, (message) => {
        console.log("Received message:", message);
      });
    
      // Connect Stream
      stream.connect();
    

Building a Next Application for Chat:

I will walk you through the process of building a Next.js application that allows users to send requests to contacts and chat with each other.

Working dapp example: Github Repo

Technologies Used

  • Next.js: Next.js is a popular open-source framework for building server-side rendered (SSR) and static web applications using React. It's developed and maintained by Vercel, and it's known for its ease of setup, automatic server rendering and code splitting, and built-in CSS and Sass support.

  • React: React is a JavaScript library for building user interfaces. It allows you to create reusable UI components, which makes the development process more efficient and the code more readable. In this project, React was used to build the UI components and manage the application's front-end logic.

  • Redux: Redux is a predictable state container for JavaScript apps. It helps you manage the state of your app, making it predictable and easy to test. In this project, Redux was used to manage the application's state, including the contacts and the requests sent to them.

  • Material Tailwind: Material Tailwind is a UI library that combines the flexibility of Tailwind CSS with the beauty of Material Design. It provides a set of pre-built components that you can use to build your UI quickly and easily. In this project, Material Tailwind was used to style the application and make it responsive and visually appealing.

  • Node.js and npm: Node.js is a JavaScript runtime that allows you to run JavaScript on your server, and npm is a package manager for Node.js. They were used to set up the development environment, manage project dependencies, and run the development server and tests.

Integrating Next with Wagmi:

Wagmi is a powerful React Hooks library designed specifically for Ethereum. It simplifies the process of building Ethereum applications by providing a set of hooks that abstract away the complexities of interacting with the Ethereum blockchain.

  • Setting Up Wagmi:

    First, we need to install Wagmi and its dependencies:

      npm i wagmi@1.4.10 viem@1.19.13
    
  • Next, we create a configuration for Wagmi. This configuration includes the chains we want to interact with, the connectors we want to use (MetaMask and Coinbase Wallet in this case), and the public client and WebSocket public client returned by the configureChains function:

    Check this file for more infomation: WagmiProvider.tsx

      import { publicProvider } from "wagmi/providers/public";
      import { MetaMaskConnector } from "wagmi/connectors/metaMask";
      import { CoinbaseWalletConnector } from "wagmi/connectors/coinbaseWallet";
      import { WagmiConfig, createConfig, configureChains, mainnet } 
      from "wagmi";
    
      const { chains, publicClient, webSocketPublicClient } = configureChains(
        [mainnet],
        [publicProvider()]
      );
    
      const config = createConfig({
        autoConnect: true,
        connectors: [
          new MetaMaskConnector({ chains }),
          new CoinbaseWalletConnector({
            chains,
            options: {
              appName: "wagmi",
            },
          }),
        ],
        publicClient,
        webSocketPublicClient,
      });
    
  • Creating the Wagmi Provider:

    With the configuration set up, we can now create a WagmiProvider component. This component wraps our application and provides the Wagmi configuration to all child components.

      export const WagmiProvider = ({ children }) => {
        return <WagmiConfig config={config}>{children}</WagmiConfig>;
      };
    
      import { WagmiProvider } from './WagmiProvider'
    
      function App() {
        return (
          <WagmiProvider>
            {/* Your app components go here */}
          </WagmiProvider>
        )
      }
    

Integrating Next with Redux:

  • Install Redux:

      npm install @reduxjs/toolkit react-redux
    
  • Then, create a new file store.js to configure your Redux store:

      import { configureStore } from '@reduxjs/toolkit';
    
      export const store: Store = configureStore({
        reducer: {
          // Add your reducers
        },
        middleware: (getDefaultMiddleware) =>
          getDefaultMiddleware({
            serializableCheck: false,
          }),
      });
    
      const store = configureStore({ reducer });
    
      export default store;
    

    Example Code: Redux Store and Provider

Using Redux and Wagmi Provider in Next:

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

Example Code: layout.tsx

Integrating Wagmi and Push Chats into Your Onboarding Process:

Here, we'll walk through the process of creating an onboarding page for a Next application, integrating Wagmi for wallet interactions, and setting up push.

  • Setting Up the Onboarding Page:

    This is not the full code. Its just an overview how you can achieve it. Feel free to modify this or example code provided. For better understanding check Example OnBoarding page: page.tsx, OnBoard.tsx

      import React from 'react';
      import { useConnect } from "wagmi";
    
      const Onboarding = () => {
      const { connect, connectors } = useConnect();
    
        const handleClick = () => {
          connect({
            connector: connectors[activeWallet === "MetaMask" ? 0 : 1],
          });
        };
    
        return (
          <div>
            <h1>Welcome to Our App!</h1>
            <button onClick={handleClick}>Connect Wallet</button>
          </div>
        );
      };
    
      export default Onboarding;
    

  • Setting Up Push Chat:

    Finally, we'll add a button to the onboarding page that users can click to enable push. When the button is clicked, we'll call a hook to request the necessary permissions and sign with push. Example Code: PushBtn.tsx

      import React from 'react';
      import { useRouter } from "next/navigation";
    
      const Onboarding = () => {
        const router = useRouter();
        const { initializePush } = usePush();
    
        const handleClick = async () => {
          try {
            await initializePush();
            router.push("/chats");
          } catch (err) {
            toast.error("Error initializing Push Protocol");
          }
        };
    
        return (
          <button onClick={handleEnablePush}>Enable Push</button>
        );
      };
    
      export default Onboarding;
    

  • Using the usePush Hook:

      import { useEthersSigner } from "@/wagmi/EthersSigner";
      import { useDispatch } from "react-redux";
      import { PushAPI, CONSTANTS } from "@pushprotocol/restapi";
    
      import {
        setPushSign,
        setRecentContact,
        setRecentRequest,
      } from "@/redux/slice/pushSlice";
      import toast from "react-hot-toast";
    
      export default function usePush() {
        const dispatch = useDispatch();
        const signer = useEthersSigner();
    
        const initializePush = async () => {
          try {
            const user = await PushAPI.initialize(signer, {
              env: CONSTANTS.ENV.STAGING,
            });
    
            if (user.errors.length > 0)
              throw new Error("Error initializing push protocol");
    
            dispatch(setPushSign(user as any));
    
            const stream = await user.initStream(
              [
                CONSTANTS.STREAM.CHAT,
                CONSTANTS.STREAM.CONNECT,
                CONSTANTS.STREAM.DISCONNECT,
              ],
              {}
            );
    
            stream.on(CONSTANTS.STREAM.CONNECT, () => {
              console.log("CONNECTED");
            });
    
            stream.on(CONSTANTS.STREAM.CHAT, async (data: any) => {
              data.event.includes("message")
                ? console.log("MESSAGE", data)
                : data.event.includes("request")
                ? user.chat.list("REQUESTS").then((requests: any) => {
                    const filterRecentRequest = requests.map((request: any) => ({
                      profilePicture: request.profilePicture,
                      did: request.did,
                      msg: request.msg.messageContent,
                      name: request.name,
                      about: request.about,
                    }));
    
                    dispatch(setRecentRequest(filterRecentRequest));
                  })
                : data.event.includes("accept")
                ? user.chat.list("CHATS").then((chats: any) => {
                    const filterRecentContact = chats.map((chat: any) => ({
                      profilePicture: chat.profilePicture,
                      did: chat.did,
                      name: chat.name,
                      about: chat.about,
                      chatId: chat.chatId,
                      msg: {
                        content: chat.msg.messageContent,
                        timestamp: chat.msg.timestamp,
                        fromDID: chat.msg.fromDID,
                      },
                    }));
    
                    dispatch(setRecentContact(filterRecentContact));
                  })
                : toast.error("Request Rejected");
            });
    
            await stream.connect();
    
            stream.on(CONSTANTS.STREAM.DISCONNECT, () => {});
    
            return user;
          } catch (error) {
            toast.error("Request Rejected");
            throw new Error("Error initializing push protocol");
          }
        };
    
        return {
          initializePush,
        };
      }
    

    Example Code: usePush.tsx

By combining frontend with the power of Push Chat and Wagmi, we've created an onboarding experience that prioritizes user engagement and simplicity. With just a few clicks, users can connect their wallets and seamlessly initialize Push Chat, setting the stage for a secure and decentralized communication experience within our application.

Working with Push Chats and Requests:

Now we'll explore how to handle push chats and requests. We'll cover how to retrieve chat messages, send new messages, and handle incoming chat requests.

  • Handling Incoming Chat Requests:

    To handle incoming chat requests, we'll use the chat.list("REQUESTS") event from the push api. This event is emitted whenever a new chat request is received.

      useEffect(() => {
          const initializeRequests = async () => {
            try {
              // Fetching requests list
              const requestsLists = await pushSign.chat.list("REQUESTS");
    
              // Filtering recent requests
              const filterRecentRequest = requestsLists.map((request: any) => ({
                profilePicture: request.profilePicture,
                did: request.did,
                msg: request.msg.messageContent,
                name: request.name,
                about: request.about,
              }));
    
              // Dispatching action to set recent requests in Redux store
              dispatch(setRecentRequest(filterRecentRequest));
              setIsLoading(false);
            } catch (error) {
              // Displaying error toast if fetching requests fails
              toast.error("Error fetching requests");
            }
          };
    
          // If connected and signer and pushSign are available, initialize requests
          if (isConnected && signer && pushSign) {
            setIsLoading(true);
            initializeRequests();
          }
        }, [isConnected, signer, pushSign, dispatch]);
    

    Example Repo Code: Requests.tsx

  • Accepting a Chat Request:

    When a chat request is received, you may want to give the user the option to accept it. This can be done using the push.chat.accept(address) method, which takes the address of the requester as an argument.

    Here's an example of a function that accepts a chat request:

      const handleAcceptRequest = async () => {
        // Accept the chat request
        await pushSign.chat.accept(pubKey);
    
        // Update the state to reflect the accepted request
        dispatch(updateRecentRequest(request.did));
    
        // Show a success message
        console.log("Request accepted");
      };
    

    Example Repo Code: RequestItem.tsx

  • Rejecting a Chat Request

    Similarly, you may want to give the user the option to reject a chat request. This can be done using the pushSign.chat.reject(address) method, which also takes the address of the requester as an argument.

    Here's an example of a function that rejects a chat request:

      const handleRejectRequest = async () => {
        // Reject the chat request
        await pushSign.chat.reject(pubKey);
    
        // Update the state to reflect the rejected request
        dispatch(updateRecentRequest(request.did));
    
        // Show a success message
        console.log("Request rejected");
      };
    

    Example Repo Code: RequestItem.tsx

  • Fetching and Initializing Chats:

    In this section, we'll discuss how to fetch and initialize chats in a React application using the pushSign library. This is typically done in a useEffect hook to ensure that the chats are fetched and initialized when the component mounts or when certain dependencies change.

      useEffect(() => {
        // Define an asynchronous function to fetch and initialize chats
        const initializeChats = async () => {
          try {
            // Fetch the list of chats from the pushSign object
            const chatsLists = await pushSign.chat.list("CHATS");
    
            // Map over the list of chats to extract the necessary contact details
            const filterRecentContact = chatsLists.map((chat: any) => ({
              profilePicture: chat.profilePicture,
              did: chat.did,
              name: chat.name,
              about: chat.about,
              chatId: chat.chatId,
              msg: {
                content: chat.msg.messageContent,
                timestamp: chat.msg.timestamp,
                fromDID: chat.msg.fromDID,
              },
            }));
    
            // Dispatch an action to store the recent contacts in the Redux store
            dispatch(setRecentContact(filterRecentContact));
    
            // Set the loading state to false as the chats have been fetched
            setIsLoading(false);
          } catch (error) {
            // Display an error message if there's an error while fetching contacts
            toast.error("Error fetching contacts");
          }
        };
    
        // Check if the user is connected and if the signer and pushSign exist
        if (isConnected && signer && pushSign) {
          // Set the loading state to true before fetching the chats
          setIsLoading(true);
    
          // Call the function to fetch and initialize chats
          initializeChats();
        }
        // The effect depends on the pushSign, isConnected, signer, and dispatch variables
      }, [pushSign, isConnected, signer, dispatch]);
    

    we first define an asynchronous function initializeChats that fetches the list of chats from the pushSign object, maps over the list to extract the necessary contact details, dispatches an action to store these details in the Redux store, and sets the loading state to false.

    We then check if the user is connected and if the signer and pushSign objects exist. If they do, we set the loading state to true and call the initializeChats function.

  • Fetching Chat History:

    Now, we'll discuss how to fetch the chat history for a specific contact. This is typically done in a useEffect hook to ensure that the chat history is fetched when the component mounts or when certain dependencies change.

      const initializeChat = async () => {
        try {
          // Fetch the chat history for the current contact
          const pastMessages: any = await pushSign.chat.history(
            currentContact.did.split(":")[1]
          );
    
          // Map over the past messages to create a new array of Message objects
          const filteredMessages: Message[] = pastMessages.map(
            ({
              fromDID,
              timestamp,
              messageContent,
              messageType,
              chatId,
            }: Message) => ({
              chatId,
              fromDID,
              timestamp,
              messageContent,
              messageType,
            })
          );
    
          // Dispatch an action to store the chat history in the Redux store
          // The array of messages is reversed to display the most recent messages last
          dispatch(setMessages([...filteredMessages].reverse()));
    
          // Set the loading state to false as the chat history has been fetched
          setLoading(false);
        } catch (err) {
          // Display an error message if there's an error while fetching the chat history
          toast.error("Error fetching chat history");
        }
      };
    
      useEffect(() => {
        // Check if the current contact is defined
        if (currentContact) {
          // Set the loading state to true before fetching the chat history
          setLoading(true);
          // Call the function to fetch the chat history
          initializeChat();
        }
        // The effect depends on the currentContact variable
      }, [currentContact]);
    

  • Sending Messages:

    In this section, we'll discuss how to send messages in a React application using the pushSign library. This is typically done in a sendMessage function that is called when the user clicks the send button or presses the enter key.

    Here's an example of a sendMessage function:

      const sendMessage = async (e) => {
        // Prevent the default event behavior
        e.preventDefault();
    
        // Return early if the input is disabled, if the pushSign object doesn't exist, or if the message is empty
        if (disabled || !pushSign || !message.trim()) return;
    
        try {
          // Send the message using the pushSign.chat.send method
          // The currentContact.did.split(":")[1] expression gets the DID of the current contact
          // The second argument is an object with the content and type of the message
          await pushSign.chat.send(currentContact.did.split(":")[1], {
            content: message,
            type: "Text",
          });
    
          // Clear the input and enable it again after the message has been sent
          setMessage("");
        } catch (err) {
          // Display an error message if there's an error while sending the message
          console.log("Error sending message");
        }
      };
    

    Example Code: MessageInput.tsx

    In this function, we first prevent the default event behavior to prevent the form from being submitted. We then check if the input is disabled, if the pushSign object doesn't exist, or if the message is empty, and return early if any of these conditions are true.

Conclusion:

In this blog post, we've covered how to initialize Push Chats, handle chat requests, fetch and initialize chats, fetch chat history, and send messages in a Next.js application using the push library. These functionalities are essential for building a chat application.

We've seen how to use the push library to interact with the chat API, and how to use React hooks and the Redux store to manage the state of our application.

By following these steps, you should be able to build a robust and interactive chat application. Remember to always test your application thoroughly to ensure that it works as expected and provides a good user experience.

For a practical example of the concepts discussed in this blog post, you can check out the sample project on my GitHub repository. You can also see the live version of the application here.

Thank you for reading this blog post. If you have any questions or comments, feel free to leave them below. Happy coding!