Build a Chat App
Welcome to Web5, the magical land where Web2 and Web3 intertwine. My name is Alice. I am a wanderer in search of an application where I can send short-text messages to my best friend, Bob.
π Your mission
Build Dinger, a self-sovereign, modern day pager that allows users to send short text-based messages or "dings" to each other.
π οΈ Your toolkit
To build this app, you will wield the following technologies:
- React - A JavaScript library, your brush to paint user interfaces, craft reusable components, and manage the state of your creation.
- Next.js - A React metaframework, your compass to guide you through creating pages, managing routes, and managing server-side rendering.
- Web5.js - A Web5 JavaScript SDK, your key to creating decentralized applications, facilitating direct communication between users, and granting them sovereignty over their data and identity.
π Your reward
By the end of this journey, you will acquire a deeper understanding of peer-to-peer communication in a Web5 ecosystem.
If you're ready for the challenge, let's get started!
This tutorial is most suitable for those who prefer to learn by doing.
Level 1: Explore the starter codeβ
Good news! You don't have to start from scratch. There is a pre-built application that contains the user interface for Dinger. Right now, the functions in the code donβt do much. They just print messages to the console and donβt have any real actions. As you progress through the tutorial, you will add logic to the functions.
π Taskβ
To access the starter code, follow the instructions below or explore the project directly in CodeSandbox.
Note: If you don't have pnpm
installed, you can install it by running npm install -g pnpm
.
git clone https://github.com/TBD54566975/developer.tbd.website.git
cd developer.tbd.website
pnpm install
pnpm start:tutorial-dinger-starter
Finished Dinger App
If youβd like to skip ahead and see the finished version of this tutorial, you can check out the running app on CodeSandbox.
You can also download and run the example by executing:
Note: If you don't have pnpm
installed, you can install it by running npm install -g pnpm
.
git clone https://github.com/TBD54566975/developer.tbd.website.git
cd developer.tbd.website
pnpm install
pnpm start:tutorial-dinger-completed
There is also a hosted example deployed at https://dinger-chat.vercel.app/
To fully interact with the web app, please click "Open in New Tab" as demonstrated in the GIF below.
π΅οΈ What to Exploreβ
- Navigate to the
src/pages/index.js
file. - Identify the functions that currently only print console.logs.
- Try to understand how each function interacts.
Level 2: Connect to Web5 and create a DIDβ
π Taskβ
Locate the line:
console.log(`this log is in initWeb5`);
and replace it with the following code snippet:
const { web5, did } = await Web5.connect();
setWeb5(web5);
setMyDid(did);
𧩠Breaking it downβ
- Web5.connect() returns a Web5 instance: It initializes and returns an instance of Web5, allowing you to interact with the Web5 ecosystem.
- Web5.connect() also creates or connects to a DID: A DID (Decentralized Identifier) is a userβs unique ID card for the decentralized, digital world. It belongs only to you.
Web5.connect()
will either connect to an existing DID that you have or create a new one for you if you don't have one yet. A DID acts as an authenticator for Web5 apps, removing the need for login credentials.
π Learn moreβ
Learn more about DID authentication.
Level 3: Define a protocolβ
π Taskβ
Locate the line:
console.log('this log is in createProtocolDefinition');
And replace it with the following code snippet:
const dingerProtocolDefinition = {
protocol: "https://blackgirlbytes.dev/dinger-chat-protocol",
published: true,
types: {
ding: {
schema: "https://blackgirlbytes.dev/ding",
dataFormats: ["application/json"],
},
},
structure: {
ding: {
$actions: [
{ who: "anyone", can: ["create"] },
{ who: "author", of: "ding", can: ["read"] },
{ who: "recipient", of: "ding", can: ["read"] },
],
},
},
};
return dingerProtocolDefinition;
𧩠Breaking it downβ
- dingerProtocolDefinition: This object defines the protocol, its structure, and it grants permissions outlining who can perform specific actions like reading or writing a ding.
π Learn moreβ
Learn more about defining protocols.
Level 4: Install the defined protocolβ
π Taskβ
Locate the line:
console.log(`this log is in installProtocolLocally`);
And replace it with the following code snippet:
return await web5.dwn.protocols.configure({
message: {
definition: protocolDefinition,
},
});
𧩠Breaking it downβ
- web5.dwn.protocols.configure(): This function installs the protocol. The protocol needs to be installed because it ensures that both the sender's and recipient's applications (or Decentralized Web Nodes - DWNs) are aligned in terms of communication standards and data formats.
Level 5: Query the protocolβ
π Taskβ
console.log(`this log is in queryForProtocol`);
And replace it with the following code snippet:
return await web5.dwn.protocols.query({
message: {
filter: {
protocol: "https://blackgirlbytes.dev/dinger-chat-protocol",
},
},
});
𧩠Breaking it downβ
- web5.dwn.protocols.query(): This function checks if the protocol already exists
π Learn moreβ
Learn more about querying protocols.
Level 6: Bring all the protocol steps togetherβ
π Taskβ
console.log(`this log is in configureProtocol`);
And replace it with the following code snippet:
const protocolDefinition = await createProtocolDefinition();
const { protocols: localProtocol, status: localProtocolStatus } = await queryForProtocol(web5);
console.log({ localProtocol, localProtocolStatus });
if (localProtocolStatus.code !== 200 || localProtocol.length === 0) {
const { protocol, status } = await installProtocolLocally(web5, protocolDefinition);
console.log("Protocol installed locally", protocol, status);
const { status: configureRemoteStatus } = await protocol.send(did);
console.log("Did the protocol install on the remote DWN?", configureRemoteStatus);
} else {
console.log("Protocol already installed");
}
- configureProtocol(): This function is responsible for configuring the protocol in both the local and remote DWNs. It first checks if the protocol is already installed locally. If not, it installs the protocol locally and then attempts to install it on the remote DWN associated with the user's DID.
π Learn moreβ
- Learn more about protocols.
Level 7: Write to the DWNβ
π Taskβ
Locate the line:
console.log(`this log is in writeToDwn`);
And replace it with the following code snippet:
const { record } = await web5.dwn.records.write({
data: ding,
message: {
protocol: "https://blackgirlbytes.dev/dinger-chat-protocol",
protocolPath: "ding",
schema: "https://blackgirlbytes.dev/ding",
recipient: recipientDid,
},
});
return record;
𧩠Breaking it downβ
- You are storing each ding on a Decentralized Web Node (DWN), or personal data store. This method web5.dwn.records.write() is responsible for writing data to the DWN.
π Learn moreβ
- Learn more about Decentralized Web Nodes and writing DWN records.
Level 8: Send dingsβ
π Taskβ
Locate the line:
console.log(`this log is in sendRecord`);
And replace it with the following code snippet:
return await record.send(recipientDid);
𧩠Breaking it downβ
- Users can have multiple DWNs, such as on their phone, laptop, or in the cloud. The method record.send() immediately sends the ding to all of the recipient's Decentralized Web Nodes (DWNs).
π Learn moreβ
- Learn more about sync intervals.
Level 9: Retrieve dingsβ
π Task 1β
Locate the line:
console.log(`this log is in fetchSentMessages`);
And replace it with the following code snippet:
const response = await web5.dwn.records.query({
message: {
filter: {
protocol: "https://blackgirlbytes.dev/dinger-chat-protocol",
},
},
});
if (response.status.code === 200) {
const sentDings = await Promise.all(
response.records.map(async (record) => {
const data = await record.data.json();
return data;
})
);
console.log(sentDings, "I sent these dings");
return sentDings;
} else {
console.log("error", response.status);
}
π Task 2β
Locate the line:
console.log(`this log is in fetchReceivedMessages`);
And replace it with the following code snippet:
const response = await web5.dwn.records.query({
from: did,
message: {
filter: {
protocol: "https://blackgirlbytes.dev/dinger-chat-protocol",
schema: "https://blackgirlbytes.dev/ding",
},
},
});
if (response.status.code === 200) {
const receivedDings = await Promise.all(
response.records.map(async (record) => {
const data = await record.data.json();
return data;
})
);
console.log(receivedDings, "I received these dings");
return receivedDings;
} else {
console.log("error", response.status);
}
π Task 3β
Locate the line:
console.log(`this log is in fetchDings`);
And replace it with the following code snippet:
const sentMessages = await fetchSentMessages(web5, did);
const receivedMessages = await fetchReceivedMessages(web5, did);
const allMessages = [...(sentMessages || []), ...(receivedMessages || [])];
setAllDings(allMessages);
𧩠Breaking it downβ
- The method web5.dwn.records.query() queries the DWN for records matching a specified filter. In the above code snippet, it retrieves records with the protocol https://blackgirlbytes.dev/dinger-chat-protocol.
π Learn moreβ
-
- Learn more about how to query DWN records and filterable record properties.
Level 10: Sync the UI with the DWNβ
π Taskβ
Locate the line:
console.log(`this log is in intervalId`);
And replace it with the following code snippet:
await fetchDings(web5, myDid);
This line is located within the following function:
useEffect(() => {
if (!web5 || !myDid) return;
const intervalId = setInterval(async () => {
console.log(`this log is in intervalId`);
}, 2000);
return () => clearInterval(intervalId);
}, [web5, myDid]);
𧩠Breaking it downβ
- The useEffect() and setInterval() fetch new dings every two seconds, allowing the UI to accurately display the chat.
π Learn moreβ
- Learn more about React's useEffect() hook and JavaScript's setInterval() method.
Level 11: Try it out!β
π Taskβ
Time to test the Dinger app by simulating a conversation between two users by using two different browser sessions.
Set Up Your Browsers:
- Open your browser in regular mode.
- Open a new window in incognito/private mode.
Send a Message from Incognito to Regular Mode:
- In the incognito window, locate and press the
Copy DID
button to copy the DID. - Switch to the regular mode browser window.
- Press the
Create +
button - Paste the copied DID into the
recipient DID
input box, thenConfirm
- Type a short message in the
note
input box. - Press
Send
to send the message.
Reply from Regular Mode to Incognito:
- In the regular mode window, locate and press the
Copy DID
button to copy the DID. - Switch back to the incognito window.
- Press the
Create +
button - Paste the copied DID into the
recipient DID
input box, thenConfirm
- Type a reply in the
note
input box. - Press
Send
to send your reply.
After following these steps, you should see a conversation taking place between the two browser sessions!
Details
Not receiving any messages?
Sync intervals occur every two minutes. If you're eager to see immediate updates for testing purposes, you can adjust the sync settings. Update the line:const { web5, did } = await Web5.connect();
to:
const { web5, did } = await Web5.connect({sync: '5s'});
This change forces a synchronization interval to happen every 5 seconds. However, be cautious: this is not recommended for production applications as it can lead to higher resource usage, battery drain, and potential rate limiting.
π§ Knowledge checkβ
π«‘ Mission completeβ
Congratulations! By building Dinger, you've successfully empowered users like Alice to communicate peer-to-peer and maintain sovereignty over her data and identity. Now, Alice can send messages to her best friend, Bob. You can check out the finished version of the app running on CodeSandbox.
Was this page helpful?
Connect with us on Discord
Submit feedback: Open a GitHub issue
Edit this page: GitHub Repo
Contribute: Contributing Guide