Skip to content

Commit 118504c

Browse files
authored
Merge pull request #443 from talkjs/feat/how-to-make-a-threaded-chat
"How to make a threaded chat" tutorial
2 parents 55f6009 + a493667 commit 118504c

File tree

4 files changed

+387
-0
lines changed

4 files changed

+387
-0
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
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/).
2+
3+
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.
4+
5+
## Prerequisites
6+
7+
To run this tutorial, you will need:
8+
9+
- A [TalkJS account](https://talkjs.com/dashboard/login)
10+
- [Node.js](https://nodejs.org/en)
11+
- [npm](https://www.npmjs.com/)
12+
13+
## How to run the tutorial
14+
15+
1. Clone or download the project.
16+
2. Run `npm install` to install dependencies.
17+
3. Run `npm start` to start the server.
18+
4. Remove the default "Reply" message action:
19+
1. Go to the **Roles** tab of the TalkJS dashboard.
20+
2. Select the "default" role.
21+
3. In **Actions and permissions** > **Built-in message actions**, set **Reply** to **None**.
22+
5. Add a "Reply" action button to the user message styling of your theme:
23+
1. Go to the **Themes** tab of the TalkJS dashboard.
24+
2. Select to **Edit** the theme you use for your "default" role.
25+
3. In the list of **Built-in Components**, select **UserMessage**.
26+
4. Add the following line below the `<MessageBody />` component:
27+
```
28+
<ActionButton t:if="{{ custom.replyCount > 0 }}" action="replyInThread">Replies ({{ custom.replyCount }})</ActionButton>
29+
<ActionButton t:else action="replyInThread">Reply</ActionButton>
30+
```
31+
5. If you are in Live mode, select **Copy to live**.
32+
6. Add a "Back" action button to the chat header of your theme:
33+
1. Go to the **Themes** tab of the TalkJS dashboard.
34+
2. Select to **Edit** the theme you use for your "default" role.
35+
3. In the list of Built-in Components, select **ChatHeader**.
36+
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:
37+
`<span><ActionButton action="back">&lt; Back</ActionButton>{{user.name}}</ActionButton></span>`
38+
5. If you are in Live mode, select **Copy to live**.
39+
7. Set up a webhook to respond to new message events:
40+
1. Go to the **Settings** tab of the TalkJS dashboard.
41+
2. Enable the `message.sent` option in the **Webhooks** section of the TalkJS dashboard.
42+
3. Start ngrok with `ngrok http 3000`.
43+
4. Add the ngrok URL to **Webhook URLs** in the TalkJS dashboard, including the `updateReplyCount` path: `https://<YOUR-URL>.ngrok.io/updateReplyCount`
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<!DOCTYPE html>
2+
3+
<html lang="en">
4+
<head>
5+
<meta charset="utf-8" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1" />
7+
8+
<title>TalkJS tutorial</title>
9+
</head>
10+
11+
<!-- minified snippet to load TalkJS without delaying your page -->
12+
<script>
13+
(function (t, a, l, k, j, s) {
14+
s = a.createElement("script");
15+
s.async = 1;
16+
s.src = "https://cdn.talkjs.com/talk.js";
17+
a.head.appendChild(s);
18+
k = t.Promise;
19+
t.Talk = {
20+
v: 3,
21+
ready: {
22+
then: function (f) {
23+
if (k)
24+
return new k(function (r, e) {
25+
l.push([f, r, e]);
26+
});
27+
l.push([f]);
28+
},
29+
catch: function () {
30+
return k && new k();
31+
},
32+
c: l,
33+
},
34+
};
35+
})(window, document, []);
36+
</script>
37+
38+
<script>
39+
Talk.ready.then(function () {
40+
const me = new Talk.User({
41+
id: "threadsExampleReceiver",
42+
name: "Alice",
43+
44+
photoUrl: "https://talkjs.com/new-web/avatar-14.jpg",
45+
role: "default",
46+
});
47+
const talkSession = new Talk.Session({
48+
appId: "<APP_ID>", // replace with your app ID
49+
me: me,
50+
});
51+
52+
const chatbox = talkSession.createChatbox();
53+
54+
chatbox.onCustomMessageAction("replyInThread", (event) => {
55+
async function postMessageData(
56+
messageId,
57+
conversationId,
58+
messageText,
59+
participants
60+
) {
61+
// Send message data to your backend server
62+
const response = await fetch("http://localhost:3000/newThread", {
63+
method: "POST",
64+
headers: {
65+
"Content-Type": "application/json",
66+
},
67+
body: JSON.stringify({
68+
messageId,
69+
conversationId,
70+
messageText,
71+
participants,
72+
}),
73+
});
74+
}
75+
76+
postMessageData(
77+
event.message.id,
78+
event.message.conversation.id,
79+
event.message.body,
80+
Object.keys(event.message.conversation.participants)
81+
);
82+
83+
let thread = talkSession.getOrCreateConversation(
84+
"replyto_" + event.message.id
85+
);
86+
thread.setParticipant(me);
87+
chatbox.select(thread);
88+
});
89+
90+
chatbox.onCustomConversationAction("back", async (event) => {
91+
const parentConvId = event.conversation.custom.parentConvId;
92+
93+
if (parentConvId !== undefined) {
94+
let thread = talkSession.getOrCreateConversation(parentConvId);
95+
chatbox.select(thread);
96+
}
97+
});
98+
99+
chatbox.mount(document.getElementById("talkjs-container"));
100+
});
101+
</script>
102+
103+
<body>
104+
<!-- container element in which TalkJS will display a chat UI -->
105+
<div id="talkjs-container" style="width: 90%; margin: 30px; height: 500px">
106+
<i>Loading chat...</i>
107+
</div>
108+
</body>
109+
</html>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "how-to-make-a-threaded-chat",
3+
"dependencies": {
4+
"cors": "^2.8.5",
5+
"express": "^4.18.2",
6+
"node-fetch": "^3.3.1"
7+
},
8+
"type": "module",
9+
"scripts": {
10+
"start": "node server.js"
11+
}
12+
}
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import express from "express";
2+
import cors from "cors";
3+
import fetch from "node-fetch";
4+
5+
const appId = "<APP_ID>";
6+
const secretKey = "<SECRET_KEY>";
7+
8+
const basePath = "https://api.talkjs.com";
9+
10+
const app = express();
11+
app.use(cors());
12+
app.use(express.json());
13+
14+
app.listen(3000, () => console.log("Server is up"));
15+
16+
const senderId = `threadsExampleSender`;
17+
const receiverId = `threadsExampleReceiver`;
18+
19+
function getMessages(messageId) {
20+
return fetch(
21+
`${basePath}/v1/${appId}/conversations/replyto_${messageId}/messages`,
22+
{
23+
method: "GET",
24+
headers: {
25+
"Content-Type": "application/json",
26+
Authorization: `Bearer ${secretKey}`,
27+
},
28+
}
29+
);
30+
}
31+
32+
// Create a thread as a new conversation
33+
async function createThread(parentMessageId, parentConvId, participants) {
34+
return fetch(
35+
`${basePath}/v1/${appId}/conversations/replyto_${parentMessageId}`,
36+
{
37+
method: "PUT",
38+
headers: {
39+
"Content-Type": "application/json",
40+
Authorization: `Bearer ${secretKey}`,
41+
},
42+
body: JSON.stringify({
43+
participants: participants,
44+
subject: "Replies",
45+
custom: {
46+
parentConvId: parentConvId,
47+
parentMessageId: parentMessageId,
48+
},
49+
}),
50+
}
51+
);
52+
}
53+
54+
async function duplicateParentMessageText(parentMessageId, messageText) {
55+
return fetch(
56+
`${basePath}/v1/${appId}/conversations/replyto_${parentMessageId}/messages`,
57+
{
58+
method: "POST",
59+
headers: {
60+
"Content-Type": "application/json",
61+
Authorization: `Bearer ${secretKey}`,
62+
},
63+
body: JSON.stringify([
64+
{
65+
text: messageText,
66+
type: "SystemMessage",
67+
},
68+
]),
69+
}
70+
);
71+
}
72+
73+
async function updateReplyCount(messageId, conversationId, count) {
74+
return fetch(
75+
`${basePath}/v1/${appId}/conversations/${conversationId}/messages/${messageId}`,
76+
{
77+
method: "PUT",
78+
headers: {
79+
"Content-Type": "application/json",
80+
Authorization: `Bearer ${secretKey}`,
81+
},
82+
body: JSON.stringify({
83+
custom: { replyCount: count.toString() },
84+
}),
85+
}
86+
);
87+
}
88+
89+
app.post("/newThread", async (req, res) => {
90+
// Get details of the message we'll reply to
91+
const parentMessageId = req.body.messageId;
92+
const parentConvId = req.body.conversationId;
93+
const parentMessageText = req.body.messageText;
94+
const parentParticipants = req.body.participants;
95+
96+
const response = await getMessages(parentMessageId);
97+
const messages = await response.json();
98+
99+
// Create a message with the text of the parent message if one doesn't already exist
100+
if (!messages.data?.length) {
101+
await createThread(parentMessageId, parentConvId, parentParticipants);
102+
await duplicateParentMessageText(parentMessageId, parentMessageText);
103+
}
104+
105+
res.status(200).end();
106+
});
107+
108+
// Endpoint for message.sent webhook
109+
app.post("/updateReplyCount", async (req, res) => {
110+
const data = req.body.data;
111+
const conversationId = data.conversation.id;
112+
const messageType = data.message.type;
113+
114+
if (conversationId.startsWith("replyto_") && messageType === "UserMessage") {
115+
const { parentMessageId, parentConvId } = data.conversation.custom;
116+
117+
const response = await getMessages(parentMessageId);
118+
const messages = await response.json();
119+
120+
const messageCount = messages.data.length;
121+
122+
// Ignore the first message in thread (it's a repeat of the parent message)
123+
if (messageCount > 1) {
124+
await updateReplyCount(parentMessageId, parentConvId, messageCount - 1);
125+
}
126+
}
127+
128+
res.status(200).end();
129+
});
130+
131+
// THIS IS SETUP CODE FOR THE EXAMPLE
132+
// You won't need any of it in your live app!
133+
//
134+
// It's just here so that you can play around with this example more easily
135+
// Whenever you run this script, we make sure the two example users are created
136+
// recreate the two conversations, and send messages from the example users
137+
138+
async function setupConversation() {
139+
const conversationId = "threadsExample";
140+
141+
// Delete the conversation (if it exists)
142+
await fetch(`${basePath}/v1/${appId}/conversations/${conversationId}`, {
143+
method: "delete",
144+
headers: {
145+
Authorization: `Bearer ${secretKey}`,
146+
},
147+
});
148+
149+
// Create a new conversation
150+
await fetch(`${basePath}/v1/${appId}/conversations/${conversationId}`, {
151+
method: "put",
152+
headers: {
153+
"Content-Type": "application/json",
154+
Authorization: `Bearer ${secretKey}`,
155+
},
156+
body: JSON.stringify({
157+
participants: [receiverId, senderId],
158+
}),
159+
});
160+
}
161+
162+
async function sendMessage(messageText) {
163+
const conversationId = "threadsExample";
164+
165+
// Send a message from the user to make sure it will show up in the conversation list
166+
await fetch(
167+
`${basePath}/v1/${appId}/conversations/${conversationId}/messages`,
168+
{
169+
method: "post",
170+
headers: {
171+
"Content-Type": "application/json",
172+
Authorization: `Bearer ${secretKey}`,
173+
},
174+
body: JSON.stringify([
175+
{
176+
text: messageText,
177+
sender: senderId,
178+
type: "UserMessage",
179+
},
180+
]),
181+
}
182+
);
183+
}
184+
185+
async function setup() {
186+
const receiver = fetch(`${basePath}/v1/${appId}/users/${receiverId}`, {
187+
method: "PUT",
188+
headers: {
189+
"Content-Type": "application/json",
190+
Authorization: `Bearer ${secretKey}`,
191+
},
192+
body: JSON.stringify({
193+
name: "Alice",
194+
email: ["[email protected]"],
195+
role: "default",
196+
photoUrl: "https://talkjs.com/new-web/avatar-14.jpg",
197+
}),
198+
});
199+
200+
const sender = fetch(`${basePath}/v1/${appId}/users/${senderId}`, {
201+
method: "PUT",
202+
headers: {
203+
"Content-Type": "application/json",
204+
Authorization: `Bearer ${secretKey}`,
205+
},
206+
body: JSON.stringify({
207+
name: "Bob",
208+
email: ["[email protected]"],
209+
role: "default",
210+
photoUrl: "https://talkjs.com/new-web/avatar-15.jpg",
211+
}),
212+
});
213+
await receiver;
214+
await sender;
215+
216+
const conv = setupConversation();
217+
await conv;
218+
219+
const message = sendMessage("We've added reply threads to the chat!");
220+
await message;
221+
}
222+
223+
setup();

0 commit comments

Comments
 (0)