Build chat applications with PubNub Chat SDK
Agent Success
Agent success rate when using this tile
95%
Improvement
Agent success rate improvement when using this tile compared to baseline
1.83x
Baseline
Agent success rate without this tile
52%
// Create direct conversation with another user
const interlocutor = await chat.getUser('bob-123') ||
await chat.createUser('bob-123', { name: 'Bob' });
const { channel } = await chat.createDirectConversation({
user: interlocutor,
channelData: { name: 'Chat with Bob' }
});// Create group channel with multiple members
const { channel } = await chat.createGroupConversation({
users: [user1, user2, user3],
channelId: 'team-alpha',
channelData: {
name: 'Team Alpha',
description: 'Project discussions',
custom: { type: 'project', visibility: 'members' }
}
});// Create public channel anyone can join
const channel = await chat.createPublicConversation({
channelId: 'public-lobby',
channelData: {
name: 'Public Lobby',
description: 'Open discussion'
}
});// Connect and receive messages
const unsubscribe = channel.connect((message) => {
console.log('Message from:', message.userId);
console.log('Text:', message.text);
console.log('Time:', message.timetoken);
});
// Later: disconnect
unsubscribe();useEffect(() => {
if (!channel) return;
const unsubscribe = channel.connect((message) => {
setMessages(prev => [...prev, message]);
});
return () => {
unsubscribe();
};
}, [channel]);await channel.sendText('Hello, everyone!');
// With metadata
await channel.sendText('Check this out!', {
meta: {
linkPreview: 'https://example.com',
priority: 'high'
}
});// Fetch past messages
const history = await channel.getHistory({
count: 50,
startTimetoken: '17000000000000000' // Optional: pagination
});
history.messages.forEach(msg => {
console.log(`${msg.userId}: ${msg.text}`);
});// Start typing indicator
await channel.startTyping();
// Stop typing indicator
await channel.stopTyping();// Get who's currently typing
const typingUsers = await channel.getTyping();
// Listen for typing changes
channel.onTyping((typingUserIds) => {
if (typingUserIds.length > 0) {
const names = typingUserIds.map(id => getUserName(id));
setTypingIndicator(`${names.join(', ')} typing...`);
} else {
setTypingIndicator('');
}
});function TypingIndicator({ channel }) {
const [typing, setTyping] = useState([]);
useEffect(() => {
if (!channel) return;
const unsubscribe = channel.onTyping((userIds) => {
setTyping(userIds);
});
return () => unsubscribe();
}, [channel]);
if (typing.length === 0) return null;
return (
<div className="typing-indicator">
{typing.length === 1 && `${typing[0]} is typing...`}
{typing.length === 2 && `${typing[0]} and ${typing[1]} are typing...`}
{typing.length > 2 && `${typing.length} people are typing...`}
</div>
);
}// Add emoji reaction to message
await message.toggleReaction('thumbsup');
await message.toggleReaction('heart');
await message.toggleReaction('laugh');// Get all reactions on a message
const reactions = message.reactions;
// { thumbsup: ['user-1', 'user-2'], heart: ['user-3'] }channel.on('reaction', ({ event, data }) => {
console.log(`Reaction ${event}:`, data);
// event: 'added' or 'removed'
// data: { messageTimetoken, reactionType, userId }
// Update local message state
updateMessageReactions(data.messageTimetoken, data);
});function MessageReactions({ message, currentUserId }) {
const handleReaction = async (emoji) => {
await message.toggleReaction(emoji);
};
return (
<div className="reactions">
{Object.entries(message.reactions || {}).map(([emoji, users]) => (
<button
key={emoji}
onClick={() => handleReaction(emoji)}
className={users.includes(currentUserId) ? 'active' : ''}
>
{emoji} {users.length}
</button>
))}
<button onClick={() => handleReaction('thumbsup')}>+</button>
</div>
);
}// Mark message as read
await message.setLastReadMessage();
// Mark channel as read up to timetoken
await channel.setLastReadMessage(message);// Get unread message count for channel
const unreadCount = await channel.getUnreadMessagesCount();
// Get unread counts for all channels
const channels = await chat.getChannels();
for (const channel of channels) {
const unread = await channel.getUnreadMessagesCount();
console.log(`${channel.name}: ${unread} unread`);
}// Get thread from a message
const thread = await message.getThread();
// If no thread exists, it will be created when you reply// Send reply to thread
await thread.sendText('This is a reply in the thread');
// Get thread messages
const threadHistory = await thread.getHistory({ count: 20 });// Check if message has replies
if (message.hasThread) {
const replyCount = message.threadRootMessage?.repliesCount || 0;
console.log(`${replyCount} replies`);
}// Mention user in message
await channel.sendText('Hey @alice, check this out!', {
mentionedUsers: {
0: { id: 'alice-123', name: 'Alice' } // Position 0 of mention
}
});// Get messages where current user is mentioned
const mentions = await chat.getMentions({
userId: chat.currentUser.id,
count: 20
});
mentions.forEach(mention => {
console.log(`Mentioned in ${mention.channel}: ${mention.message.text}`);
});// Reference another channel in message
await channel.sendText('Check out #general for updates', {
referencedChannels: {
0: { id: 'general', name: 'General' }
}
});// Send file with message
await channel.sendFile({
file: fileObject, // File from input
message: 'Check out this document'
});// Get file URL from message
if (message.files && message.files.length > 0) {
const file = message.files[0];
console.log('File name:', file.name);
console.log('File URL:', file.url);
}import { TimetokenUtils } from '@pubnub/chat';
// Convert timetoken to Date
const date = TimetokenUtils.timetokenToDate(message.timetoken);
// Format time display
const timeString = date.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
});
// Convert Date to timetoken
const timetoken = TimetokenUtils.dateToTimetoken(new Date());