Slash commands have long existed in chat since the IRC days, but they have been reimagined by modern chat apps such as Slack. They act as shortcuts for simple or complex workflows within an application.
To follow along with this tutorial, you need need to:
Go to your Chatkit dashboard, create a new Chatkit instance for this tutorial and take note of your Instance Locator and Secret Key in the Credentials tab.
Also make sure your Test Token Provider is enabled as shown below. Once enabled, the endpoint from which the test token will be generated will be displayed for you to copy and paste. Note that the test token is not to be used in production. More information on the authentication process for production apps can be found here.
Next, click the Console tab and create a new user and a brand new room for your instance. Take note of the room ID as we’ll be needing it later on.
Run the command below to install create-react-app globally on your machine, then use it to bootstrap a new react React application:
npm install -g create-react-app
create-react-app chatkit-slash-cmd
Once the app has been created, cd
into the new chatkit-slash-cmd
directory and install the following additional dependencies that we’ll be needing in the course of building the chat app:
npm install @pusher/chatkit-client prop-types skeleton-css --save
You can now start your development server by running npm start
then navigate to http://localhost:3000 in your browser to view the app.
Open up src/App.css
in your code editor, and change it to look like this:
// src/App.css
.App {
width: 100vw;
height: 100vh;
display: flex;
overflow: hidden;
}
.sidebar {
height: 100%;
width: 20%;
background-color: darkcyan;
}
.login {
padding: 5px 20px;
}
.sidebar input {
width: 100%;
}
.chat-rooms .active {
background-color: whitesmoke;
color: #181919;
}
.chat-rooms li {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px 20px;
font-size: 18px;
color: #181919;
cursor: pointer;
border-bottom: 1px solid #eee;
margin-bottom: 0;
}
.room-list h3 {
padding-left: 20px;
padding-right: 20px;
}
.room-unread {
display: inline-block;
width: 20px;
height: 20px;
line-height: 20px;
border-radius: 50%;
font-size: 16px;
text-align: center;
padding: 5px;
background-color: greenyellow;
color: #222;
}
.chat-rooms li:hover {
background-color: #D8D1D1;
}
.chat-screen {
display: flex;
flex-direction: column;
height: 100vh;
width: calc(100vw - 20%);
}
.chat-header {
height: 70px;
flex-shrink: 0;
border-bottom: 1px solid #ccc;
padding-left: 10px;
padding-right: 20px;
display: flex;
flex-direction: column;
justify-content: center;
}
.chat-header h3 {
margin-bottom: 0;
text-align: center;
}
.chat-messages {
flex-grow: 1;
overflow-y: scroll;
display: flex;
flex-direction: column;
justify-content: flex-end;
margin-bottom: 0;
min-height: min-content;
}
.message {
padding-left: 20px;
padding-right: 20px;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.message span {
display: block;
text-align: left;
}
.message .user-id {
font-weight: bold;
}
.message-form {
border-top: 1px solid #ccc;
}
.message-form, .message-input {
width: 100%;
margin-bottom: 0;
}
input[type="text"].message-input {
height: 50px;
border: none;
padding-left: 20px;
}
Next, let’s set up a basic chat interface so we can add the slash command functionality.
// src/App.js
import React, { Component } from "react";
import {
handleInput,
connectToChatkit,
connectToRoom,
sendMessage,
} from "./methods";
import "skeleton-css/css/normalize.css";
import "skeleton-css/css/skeleton.css";
import "./App.css";
class App extends Component {
constructor() {
super();
this.state = {
userId: "",
currentUser: null,
currentRoom: null,
rooms: [],
messages: [],
newMessage: "",
};
this.handleInput = handleInput.bind(this);
this.connectToChatkit = connectToChatkit.bind(this);
this.connectToRoom = connectToRoom.bind(this);
this.sendMessage = sendMessage.bind(this);
}
render() {
const {
rooms,
currentRoom,
currentUser,
messages,
newMessage,
} = this.state;
const roomList = rooms.map(room => {
const isRoomActive = room.id === currentRoom.id ? 'active' : '';
return (
<li
className={isRoomActive}
key={room.id}
onClick={() => this.connectToRoom(room.id)}
>
<span className="room-name">{room.name}</span>
</li>
);
});
const messageList = messages.map(message => {
const arr = message.parts.map(p => {
return (
<span className="message-text">{p.payload.content}</span>
);
});
return (
<li className="message" key={message.id}>
<div>
<span className="user-id">{message.senderId}</span>
{arr}
</div>
</li>
)
});
return (
<div className="App">
<aside className="sidebar left-sidebar">
{!currentUser ? (
<div className="login">
<h3>Join Chat</h3>
<form id="login" onSubmit={this.connectToChatkit}>
<input
onChange={this.handleInput}
className="userId"
type="text"
name="userId"
placeholder="Enter your username"
/>
</form>
</div>
) : null
}
{currentRoom ? (
<div className="room-list">
<h3>Rooms</h3>
<ul className="chat-rooms">
{roomList}
</ul>
</div>
) : null
}
</aside>
{
currentUser ? (
<section className="chat-screen">
<ul className="chat-messages">
{messageList}
</ul>
<footer className="chat-footer">
<form onSubmit={this.sendMessage} className="message-form">
<input
type="text"
value={newMessage}
name="newMessage"
className="message-input"
placeholder="Type your message and hit ENTER to send"
onChange={this.handleInput}
/>
</form>
</footer>
</section>
) : null
}
</div>
);
}
}
export default App;
Create a new methods.js
file within the src
directory and add the following code into it:
// src/methods.js
import { ChatManager, TokenProvider } from "@pusher/chatkit-client";
function handleInput(event) {
const { value, name } = event.target;
this.setState({
[name]: value
});
}
function connectToChatkit(event) {
event.preventDefault();
const { userId } = this.state;
const tokenProvider = new TokenProvider({
url:
"<test token provider endpoint>"
});
const chatManager = new ChatManager({
instanceLocator: "<your chatkit instance locator>",
userId,
tokenProvider
});
return chatManager
.connect()
.then(currentUser => {
this.setState(
{
currentUser,
},
() => connectToRoom.call(this)
);
})
.catch(console.error);
}
function connectToRoom(roomId = "<your chatkit room id>") {
const { currentUser } = this.state;
this.setState({
messages: []
});
return currentUser
.subscribeToRoomMultipart({
roomId,
messageLimit: 10,
hooks: {
onMessage: message => {
this.setState({
messages: [...this.state.messages, message],
});
},
}
})
.then(currentRoom => {
this.setState({
currentRoom,
rooms: currentUser.rooms,
});
})
.catch(console.error);
}
function sendMessage(event) {
event.preventDefault();
const { newMessage, currentUser, currentRoom } = this.state;
const parts = [];
if (newMessage.trim() === "") return;
parts.push({
type: "text/plain",
content: newMessage
});
currentUser.sendMultipartMessage({
roomId: `${currentRoom.id}`,
parts
});
this.setState({
newMessage: "",
});
}
export {
handleInput,
connectToRoom,
connectToChatkit,
sendMessage,
}
Make sure to replace the <test token provider endpoint>
, <your chatkit instance locator>
and <your chatkit room id>
placeholders above with the appropriate values from your Chatkit dashboard.
At this point, users can converse in a room. What is left is to add slash commands to the room. To demonstrate this functionality, we’ll make a /news [topic]
command that will look up news headlines on the provided topic, and post the first three results to the room.
Head over to https://newsapi.org and register for a free account. Once your account is created, your API key will be presented to you. Take note of it as we’ll be using it shortly.
Next, modify your methods.js
file to look like this:
// src/methods.js
// [..]
function handleSlashCommand(message) {
const cmd = message.split(" ")[0];
const query = message.slice(cmd.length)
if (cmd !== "/news") {
alert(`${cmd} is not a valid command`);
return;
}
return sendNews.call(this, query);
}
function sendMessage(event) {
event.preventDefault();
const { newMessage, currentUser, currentRoom } = this.state;
const parts = [];
if (newMessage.trim() === "") return;
if (newMessage.startsWith("/")) {
handleSlashCommand.call(this, newMessage);
this.setState({
newMessage: "",
});
return;
}
parts.push({
type: "text/plain",
content: newMessage
});
currentUser.sendMultipartMessage({
roomId: `${currentRoom.id}`,
parts
});
this.setState({
newMessage: "",
});
}
// [..]
In sendMessage()
, we check if the message starts with a slash character. If so, the message is passed to the handleSlashCommand()
function where we retrieve the command name and the query after the command. If the command is not /news
, a “Command not found” message will be displayed to the user. Otherwise, the query is passed off to the sendNews()
function which you should create above handleSlashCommand()
:
// src/methods.js
function sendNews(query) {
const { currentUser, currentRoom } = this.state;
fetch(`https://newsapi.org/v2/everything?q=${query}&pageSize=3&apiKey=<your news api key>`)
.then(res => res.json())
.then(data => {
const parts = [];
data.articles.forEach(article => {
parts.push({
type: "text/plain",
content: `${article.title} - ${article.source.name} - ${article.url}`
});
});
currentUser.sendMultipartMessage({
roomId: `${currentRoom.id}`,
parts
});
})
.catch(console.error);
}
In sendNews()
, a request for the top three headlines on the requested topic is made to newsapi.org, and the results are sent to the current room. You need to replace <your news api key>
your API key in the URL endpoint above.
Since the news results will contain links, let’s account for that in the way we’re displaying the messages in the room so that the links don’t display as plain text:
// src/App.js
// [..]
class App extends Component {
// [..]
render() {
// [..]
const insertTextAtIndices = (text, obj) => {
return text.replace(/./g, function(character, index) {
return obj[index] ? obj[index] + character : character;
});
};
const messageList = messages.map(message => {
const arr = message.parts.map(p => {
const urlMatches = p.payload.content.match(/\b(http|https)?:\/\/\S+/gi) || [];
let text = p.payload.content;
urlMatches.forEach(link => {
const startIndex = text.indexOf(link);
const endIndex = startIndex + link.length;
text = insertTextAtIndices(text, {
[startIndex]: `<a href="${link}" target="_blank" rel="noopener noreferrer" class="embedded-link">`,
[endIndex]: "</a>"
});
});
return (
<span className="message-text" dangerouslySetInnerHTML={{
__html: text
}}></span>
);
});
return (
<li className="message" key={message.id}>
<div>
<span className="user-id">{message.senderId}</span>
{arr}
</div>
</li>
)
});
// [..]
}
}
export default App;
Try it out! Test the app by using the /news
command along with any topic of your choice. It should work similarly to the GIF below:
In this tutorial, you learned about how slash commands work and how to implement them in a React chat app. You can checkout other things Chatkit can do by viewing its extensive documentation.
Don’t forget to grab the complete source code in this GitHub repository.
#reactjs #javascript