Skip to content

Commit

Permalink
Merge pull request talkjs#443 from talkjs/feat/how-to-make-a-threaded…
Browse files Browse the repository at this point in the history
…-chat

"How to make a threaded chat" tutorial
  • Loading branch information
keerlu authored Oct 18, 2023
2 parents 55f6009 + a493667 commit 118504c
Show file tree
Hide file tree
Showing 4 changed files with 387 additions and 0 deletions.
43 changes: 43 additions & 0 deletions howtos/how-to-make-a-threaded-chat/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
This is an example project for TalkJS's tutorial on [how to make a comment section with threaded replies](https://talkjs.com/resources/how-to-build-a-reply-thread-feature-with-talkjs/).

This project uses action buttons and the REST API to add a custom reply option that opens a new conversation for replies, and a back button to navigate back to the original message. It also uses a webhook to listen for new messages and updates the reply action button to show the number of replies.

## Prerequisites

To run this tutorial, you will need:

- A [TalkJS account](https://talkjs.com/dashboard/login)
- [Node.js](https://nodejs.org/en)
- [npm](https://www.npmjs.com/)

## How to run the tutorial

1. Clone or download the project.
2. Run `npm install` to install dependencies.
3. Run `npm start` to start the server.
4. Remove the default "Reply" message action:
1. Go to the **Roles** tab of the TalkJS dashboard.
2. Select the "default" role.
3. In **Actions and permissions** > **Built-in message actions**, set **Reply** to **None**.
5. Add a "Reply" action button to the user message styling of your theme:
1. Go to the **Themes** tab of the TalkJS dashboard.
2. Select to **Edit** the theme you use for your "default" role.
3. In the list of **Built-in Components**, select **UserMessage**.
4. Add the following line below the `<MessageBody />` component:
```
<ActionButton t:if="{{ custom.replyCount > 0 }}" action="replyInThread">Replies ({{ custom.replyCount }})</ActionButton>
<ActionButton t:else action="replyInThread">Reply</ActionButton>
```
5. If you are in Live mode, select **Copy to live**.
6. Add a "Back" action button to the chat header of your theme:
1. Go to the **Themes** tab of the TalkJS dashboard.
2. Select to **Edit** the theme you use for your "default" role.
3. In the list of Built-in Components, select **ChatHeader**.
4. Find the code for displaying the user's name in the header (something like `<span>{{user.name}}</span>`) and replace it with the following:
`<span><ActionButton action="back">&lt; Back</ActionButton>{{user.name}}</ActionButton></span>`
5. If you are in Live mode, select **Copy to live**.
7. Set up a webhook to respond to new message events:
1. Go to the **Settings** tab of the TalkJS dashboard.
2. Enable the `message.sent` option in the **Webhooks** section of the TalkJS dashboard.
3. Start ngrok with `ngrok http 3000`.
4. Add the ngrok URL to **Webhook URLs** in the TalkJS dashboard, including the `updateReplyCount` path: `https://<YOUR-URL>.ngrok.io/updateReplyCount`
109 changes: 109 additions & 0 deletions howtos/how-to-make-a-threaded-chat/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<!DOCTYPE html>

<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

<title>TalkJS tutorial</title>
</head>

<!-- minified snippet to load TalkJS without delaying your page -->
<script>
(function (t, a, l, k, j, s) {
s = a.createElement("script");
s.async = 1;
s.src = "https://cdn.talkjs.com/talk.js";
a.head.appendChild(s);
k = t.Promise;
t.Talk = {
v: 3,
ready: {
then: function (f) {
if (k)
return new k(function (r, e) {
l.push([f, r, e]);
});
l.push([f]);
},
catch: function () {
return k && new k();
},
c: l,
},
};
})(window, document, []);
</script>

<script>
Talk.ready.then(function () {
const me = new Talk.User({
id: "threadsExampleReceiver",
name: "Alice",
email: "alice@example.com",
photoUrl: "https://talkjs.com/new-web/avatar-14.jpg",
role: "default",
});
const talkSession = new Talk.Session({
appId: "<APP_ID>", // replace with your app ID
me: me,
});

const chatbox = talkSession.createChatbox();

chatbox.onCustomMessageAction("replyInThread", (event) => {
async function postMessageData(
messageId,
conversationId,
messageText,
participants
) {
// Send message data to your backend server
const response = await fetch("http://localhost:3000/newThread", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
messageId,
conversationId,
messageText,
participants,
}),
});
}

postMessageData(
event.message.id,
event.message.conversation.id,
event.message.body,
Object.keys(event.message.conversation.participants)
);

let thread = talkSession.getOrCreateConversation(
"replyto_" + event.message.id
);
thread.setParticipant(me);
chatbox.select(thread);
});

chatbox.onCustomConversationAction("back", async (event) => {
const parentConvId = event.conversation.custom.parentConvId;

if (parentConvId !== undefined) {
let thread = talkSession.getOrCreateConversation(parentConvId);
chatbox.select(thread);
}
});

chatbox.mount(document.getElementById("talkjs-container"));
});
</script>

<body>
<!-- container element in which TalkJS will display a chat UI -->
<div id="talkjs-container" style="width: 90%; margin: 30px; height: 500px">
<i>Loading chat...</i>
</div>
</body>
</html>
12 changes: 12 additions & 0 deletions howtos/how-to-make-a-threaded-chat/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "how-to-make-a-threaded-chat",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.18.2",
"node-fetch": "^3.3.1"
},
"type": "module",
"scripts": {
"start": "node server.js"
}
}
223 changes: 223 additions & 0 deletions howtos/how-to-make-a-threaded-chat/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import express from "express";
import cors from "cors";
import fetch from "node-fetch";

const appId = "<APP_ID>";
const secretKey = "<SECRET_KEY>";

const basePath = "https://api.talkjs.com";

const app = express();
app.use(cors());
app.use(express.json());

app.listen(3000, () => console.log("Server is up"));

const senderId = `threadsExampleSender`;
const receiverId = `threadsExampleReceiver`;

function getMessages(messageId) {
return fetch(
`${basePath}/v1/${appId}/conversations/replyto_${messageId}/messages`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${secretKey}`,
},
}
);
}

// Create a thread as a new conversation
async function createThread(parentMessageId, parentConvId, participants) {
return fetch(
`${basePath}/v1/${appId}/conversations/replyto_${parentMessageId}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${secretKey}`,
},
body: JSON.stringify({
participants: participants,
subject: "Replies",
custom: {
parentConvId: parentConvId,
parentMessageId: parentMessageId,
},
}),
}
);
}

async function duplicateParentMessageText(parentMessageId, messageText) {
return fetch(
`${basePath}/v1/${appId}/conversations/replyto_${parentMessageId}/messages`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${secretKey}`,
},
body: JSON.stringify([
{
text: messageText,
type: "SystemMessage",
},
]),
}
);
}

async function updateReplyCount(messageId, conversationId, count) {
return fetch(
`${basePath}/v1/${appId}/conversations/${conversationId}/messages/${messageId}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${secretKey}`,
},
body: JSON.stringify({
custom: { replyCount: count.toString() },
}),
}
);
}

app.post("/newThread", async (req, res) => {
// Get details of the message we'll reply to
const parentMessageId = req.body.messageId;
const parentConvId = req.body.conversationId;
const parentMessageText = req.body.messageText;
const parentParticipants = req.body.participants;

const response = await getMessages(parentMessageId);
const messages = await response.json();

// Create a message with the text of the parent message if one doesn't already exist
if (!messages.data?.length) {
await createThread(parentMessageId, parentConvId, parentParticipants);
await duplicateParentMessageText(parentMessageId, parentMessageText);
}

res.status(200).end();
});

// Endpoint for message.sent webhook
app.post("/updateReplyCount", async (req, res) => {
const data = req.body.data;
const conversationId = data.conversation.id;
const messageType = data.message.type;

if (conversationId.startsWith("replyto_") && messageType === "UserMessage") {
const { parentMessageId, parentConvId } = data.conversation.custom;

const response = await getMessages(parentMessageId);
const messages = await response.json();

const messageCount = messages.data.length;

// Ignore the first message in thread (it's a repeat of the parent message)
if (messageCount > 1) {
await updateReplyCount(parentMessageId, parentConvId, messageCount - 1);
}
}

res.status(200).end();
});

// THIS IS SETUP CODE FOR THE EXAMPLE
// You won't need any of it in your live app!
//
// It's just here so that you can play around with this example more easily
// Whenever you run this script, we make sure the two example users are created
// recreate the two conversations, and send messages from the example users

async function setupConversation() {
const conversationId = "threadsExample";

// Delete the conversation (if it exists)
await fetch(`${basePath}/v1/${appId}/conversations/${conversationId}`, {
method: "delete",
headers: {
Authorization: `Bearer ${secretKey}`,
},
});

// Create a new conversation
await fetch(`${basePath}/v1/${appId}/conversations/${conversationId}`, {
method: "put",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${secretKey}`,
},
body: JSON.stringify({
participants: [receiverId, senderId],
}),
});
}

async function sendMessage(messageText) {
const conversationId = "threadsExample";

// Send a message from the user to make sure it will show up in the conversation list
await fetch(
`${basePath}/v1/${appId}/conversations/${conversationId}/messages`,
{
method: "post",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${secretKey}`,
},
body: JSON.stringify([
{
text: messageText,
sender: senderId,
type: "UserMessage",
},
]),
}
);
}

async function setup() {
const receiver = fetch(`${basePath}/v1/${appId}/users/${receiverId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${secretKey}`,
},
body: JSON.stringify({
name: "Alice",
email: ["alice@example.com"],
role: "default",
photoUrl: "https://talkjs.com/new-web/avatar-14.jpg",
}),
});

const sender = fetch(`${basePath}/v1/${appId}/users/${senderId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${secretKey}`,
},
body: JSON.stringify({
name: "Bob",
email: ["bob@example.com"],
role: "default",
photoUrl: "https://talkjs.com/new-web/avatar-15.jpg",
}),
});
await receiver;
await sender;

const conv = setupConversation();
await conv;

const message = sendMessage("We've added reply threads to the chat!");
await message;
}

setup();

0 comments on commit 118504c

Please sign in to comment.