Revolutionize Your Web3 Communication with Push Chat: Say Hello to Secure, Decentralized Messaging!
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
andCoinbase Wallet
in this case), and the public client and WebSocket public client returned by theconfigureChains
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 auseEffect
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 thepushSign
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
andpushSign
objects exist. If they do, we set the loading state to true and call theinitializeChats
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 asendMessage
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!
Subscribe to my newsletter
Read articles from Gautam Raj directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by