forked from talkjs/talkjs-examples
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request talkjs#443 from talkjs/feat/how-to-make-a-threaded…
…-chat "How to make a threaded chat" tutorial
- Loading branch information
Showing
4 changed files
with
387 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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">< 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` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |