diff --git a/components/Chat.jsx b/components/Chat.jsx index d5ded85..d8f64fd 100644 --- a/components/Chat.jsx +++ b/components/Chat.jsx @@ -1,17 +1,23 @@ 'use client'; import * as Ably from 'ably'; -import { AblyProvider, ChannelProvider } from 'ably/react'; +import { ChatClient } from '@ably/chat'; +import { ChatClientProvider, ChatRoomProvider } from '@ably/chat/react'; import ChatBox from './ChatBox.jsx'; +const roomOptions = { + history: { limit: 50 }, +}; + export default function Chat() { - const client = new Ably.Realtime({ authUrl: '/api' }); + const realtimeClient = new Ably.Realtime({ authUrl: '/api' }); + const chatClient = new ChatClient(realtimeClient); return ( - - + + - - + + ); } diff --git a/components/ChatBox.jsx b/components/ChatBox.jsx index 5a1d6ea..83a5a0a 100644 --- a/components/ChatBox.jsx +++ b/components/ChatBox.jsx @@ -1,24 +1,47 @@ -import React, { useEffect, useState } from 'react'; -import { useChannel } from 'ably/react'; +import React, { useEffect, useState, useRef } from 'react'; +import { useMessages } from '@ably/chat/react'; import styles from './ChatBox.module.css'; export default function ChatBox() { - let inputBox = null; - let messageEnd = null; + const inputBox = useRef(null); + const messageEndRef = useRef(null); const [messageText, setMessageText] = useState(''); - const [receivedMessages, setMessages] = useState([]); + const [messages, setMessages] = useState([]); const messageTextIsEmpty = messageText.trim().length === 0; - const { channel, ably } = useChannel('chat-demo', (message) => { - const history = receivedMessages.slice(-199); - setMessages([...history, message]); + const { send: sendMessage } = useMessages({ + listener: (payload) => { + const newMessage = payload.message; + setMessages((prevMessages) => { + if (prevMessages.some((existingMessage) => existingMessage.isSameAs(newMessage))) { + return prevMessages; + } + + const index = prevMessages.findIndex((existingMessage) => existingMessage.after(newMessage)); + + const newMessages = [...prevMessages]; + if (index === -1) { + newMessages.push(newMessage); + } else { + newMessages.splice(index, 0, newMessage); + } + return newMessages; + }); + }, }); - const sendChatMessage = (messageText) => { - channel.publish({ name: 'chat-message', data: messageText }); - setMessageText(''); - inputBox.focus(); + const sendChatMessage = async (text) => { + if (!sendMessage) { + return; + } + try { + await sendMessage({ text: text }); + setMessageText(''); + inputBox.current?.focus(); + } catch (error) { + console.error('Error sending message:', error); + } }; const handleFormSubmission = (event) => { @@ -27,43 +50,37 @@ export default function ChatBox() { }; const handleKeyPress = (event) => { - if (event.charCode !== 13 || messageTextIsEmpty) { + if (event.key !== 'Enter' || event.shiftKey) { return; } - sendChatMessage(messageText); event.preventDefault(); + sendChatMessage(messageText); }; - const messages = receivedMessages.map((message, index) => { - const author = message.connectionId === ably.connection.id ? 'me' : 'other'; + const messageElements = messages.map((message, index) => { + const key = message.serial ?? index; return ( - - {message.data} + + {message.text} ); }); useEffect(() => { - messageEnd.scrollIntoView({ behaviour: 'smooth' }); - }); + messageEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages]); return ( - {messages} - { - messageEnd = element; - }} - > + {messageElements} + { - inputBox = element; - }} + ref={inputBox} value={messageText} - placeholder="Type a message..." + placeholder={'Type a message...'} onChange={(e) => setMessageText(e.target.value)} onKeyPress={handleKeyPress} className={styles.textarea} diff --git a/package-lock.json b/package-lock.json index dccec67..69087d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,8 @@ "name": "ably-nextjs-vercel-demo", "version": "0.1.0", "dependencies": { - "ably": "^2.0.4", + "@ably/chat": "^0.6.0", + "ably": "2.8.0", "next": "^14.2.3", "react": "^18.2.0", "react-dom": "^18.2.0" @@ -19,6 +20,37 @@ "prettier": "^3.2.5" } }, + "node_modules/@ably/chat": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@ably/chat/-/chat-0.6.0.tgz", + "integrity": "sha512-LS44zVRJkp05i0nRJ31JBOR+Usqp9hUtsaLZ0FroW/FrnXOMoKh0XHywGthLjmRWqH0LUmCaGvomM3pfZ/Rwxw==", + "license": "Apache-2.0", + "dependencies": { + "async-mutex": "^0.5.0", + "dequal": "^2.0.3", + "lodash.clonedeep": "^4.5.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-darwin-arm64": "^4.18", + "@rollup/rollup-linux-x64-gnu": "^4.18" + }, + "peerDependencies": { + "ably": "^2.6.3", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/@ably/msgpack-js": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@ably/msgpack-js/-/msgpack-js-0.4.0.tgz", @@ -312,6 +344,32 @@ "node": ">= 8" } }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.1.tgz", + "integrity": "sha512-VWXGISWFY18v/0JyNUy4A46KCFCb9NVsH+1100XP31lud+TzlezBbz24CYzbnA4x6w4hx+NYCXDfnvDVO6lcAA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.1.tgz", + "integrity": "sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rushstack/eslint-patch": { "version": "1.10.3", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz", @@ -534,14 +592,16 @@ "dev": true }, "node_modules/ably": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/ably/-/ably-2.0.4.tgz", - "integrity": "sha512-mA2Zsv7u29jZSCyz9YOXoxWq8GIktcvA57GTIdMP0Xv3byV41bBoAZUgXMl7VjD5cioA6xenUbqX17d9zcimSw==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/ably/-/ably-2.8.0.tgz", + "integrity": "sha512-JhOwtrbqtUR+n95jsY0syS8rIPhI1vDNfcNE1kxPhNkPaxzmYmGCo7g2xjuLZ/d4OoCuhHzX73+AVYAL70L70g==", + "license": "Apache-2.0", "dependencies": { "@ably/msgpack-js": "^0.4.0", "fastestsmallesttextencoderdecoder": "^1.0.22", "got": "^11.8.5", - "ws": "^8.14.2" + "ulid": "^2.3.0", + "ws": "^8.17.1" }, "engines": { "node": ">=16" @@ -809,6 +869,15 @@ "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", "dev": true }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -1191,7 +1260,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "engines": { "node": ">=6" } @@ -2841,6 +2909,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4035,6 +4109,15 @@ "node": ">=14.17" } }, + "node_modules/ulid": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ulid/-/ulid-2.4.0.tgz", + "integrity": "sha512-fIRiVTJNcSRmXKPZtGzFQv9WRrZ3M9eoptl/teFJvjOzmpU+/K/JH6HZ8deBfb5vMEpicJcLn7JmvdknlMq7Zg==", + "license": "MIT", + "bin": { + "ulid": "bin/cli.js" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -4173,9 +4256,10 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", - "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "license": "MIT", "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index da7f3a8..859bafb 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "format:check": "prettier --check --ignore-path .gitignore app components" }, "dependencies": { - "ably": "^2.0.4", + "@ably/chat": "^0.6.0", + "ably": "2.8.0", "next": "^14.2.3", "react": "^18.2.0", "react-dom": "^18.2.0"