From 5b1c6d2deb22342e03343b7cc8d881e12f9ddefc Mon Sep 17 00:00:00 2001 From: jmoguilevsky Date: Wed, 9 Aug 2023 15:45:47 -0300 Subject: [PATCH] use custom chat --- package-lock.json | 244 +++++++++++++++--- package.json | 4 +- src/components/VideoConference/Chat/Chat.tsx | 129 +++++++++ src/components/VideoConference/Chat/index.ts | 2 + src/components/VideoConference/Chat/utils.ts | 97 +++++++ .../VideoConference/Videoconference.tsx | 35 ++- 6 files changed, 466 insertions(+), 45 deletions(-) create mode 100644 src/components/VideoConference/Chat/Chat.tsx create mode 100644 src/components/VideoConference/Chat/index.ts create mode 100644 src/components/VideoConference/Chat/utils.ts diff --git a/package-lock.json b/package-lock.json index 86d02b6..4af5b09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@dcl/crypto": "^3.4.3", + "@dcl/protocol": "^1.0.0-5810343446.commit-4ee6d08", "@dcl/schemas": "^7.4.1", "@dcl/social-rpc-client": "^0.0.0-20230725183127.commit-e0fdbae", "@dcl/ui-env": "^1.4.0", @@ -30,7 +31,8 @@ "react-virtualized-auto-sizer": "^1.0.20", "react-window": "^1.8.9", "redux-logger": "^3.0.6", - "redux-saga": "^1.2.3" + "redux-saga": "^1.2.3", + "rxjs": "^7.8.1" }, "devDependencies": { "@dcl/eslint-config": "1.1.7", @@ -1258,6 +1260,17 @@ "url": "https://opencollective.com/preact" } }, + "node_modules/@coinbase/wallet-sdk/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -1342,6 +1355,14 @@ "multiformats": "^9.6.3" } }, + "node_modules/@dcl/protocol": { + "version": "1.0.0-5810343446.commit-4ee6d08", + "resolved": "https://registry.npmjs.org/@dcl/protocol/-/protocol-1.0.0-5810343446.commit-4ee6d08.tgz", + "integrity": "sha512-9vhbn+IfdOn1qnJa7PE6kLsmT2G0pYoUXi2ydEelStEQw6Rkvfhc564mAx1zz+xLz7ZSE+9gWit90zroHNvavw==", + "dependencies": { + "@dcl/ts-proto": "1.154.0" + } + }, "node_modules/@dcl/rpc": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@dcl/rpc/-/rpc-1.1.2.tgz", @@ -1415,6 +1436,65 @@ } } }, + "node_modules/@dcl/ts-proto": { + "version": "1.154.0", + "resolved": "https://registry.npmjs.org/@dcl/ts-proto/-/ts-proto-1.154.0.tgz", + "integrity": "sha512-2S5AKMMPVZrVfa/1WRy4/h0niikcbu3Yf6dCoudh7ScG7BsyKAPC3CMg6IJKHzrmWS593UZClq7YJof6Vt4O+w==", + "dependencies": { + "@types/object-hash": "^3.0.2", + "case-anything": "^2.1.10", + "dataloader": "^1.4.0", + "object-hash": "^3.0.0", + "protobufjs": "^7.2.4", + "ts-poet": "^6.4.1", + "ts-proto-descriptors": "^1.15.0" + }, + "bin": { + "protoc-gen-dcl_ts_proto": "protoc-gen-dcl_ts_proto" + } + }, + "node_modules/@dcl/ts-proto/node_modules/@types/node": { + "version": "20.4.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.9.tgz", + "integrity": "sha512-8e2HYcg7ohnTUbHk8focoklEQYvemQmu9M/f43DZVx43kHn0tE3BY/6gSDxS7k0SprtS0NHvj+L80cGLnoOUcQ==" + }, + "node_modules/@dcl/ts-proto/node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, + "node_modules/@dcl/ts-proto/node_modules/protobufjs": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz", + "integrity": "sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@dcl/ts-proto/node_modules/ts-proto-descriptors": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/ts-proto-descriptors/-/ts-proto-descriptors-1.15.0.tgz", + "integrity": "sha512-TYyJ7+H+7Jsqawdv+mfsEpZPTIj9siDHS6EMCzG/z3b/PZiphsX+mWtqFfFVe5/N0Th6V3elK9lQqjnrgTOfrg==", + "dependencies": { + "long": "^5.2.3", + "protobufjs": "^7.2.4" + } + }, "node_modules/@dcl/ui-env": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@dcl/ui-env/-/ui-env-1.4.0.tgz", @@ -3413,19 +3493,6 @@ "livekit-client": "^1.12.0" } }, - "node_modules/@livekit/components-core/node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@livekit/components-core/node_modules/tslib": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", - "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==" - }, "node_modules/@livekit/components-react": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@livekit/components-react/-/components-react-1.0.8.tgz", @@ -4860,6 +4927,11 @@ "form-data": "^3.0.0" } }, + "node_modules/@types/object-hash": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/object-hash/-/object-hash-3.0.2.tgz", + "integrity": "sha512-tfyXl1JPCf2hzIDK29gO7qGqJjThKBzg/Cn3bA68R9NmWdOx+f7k5mm4to/n43BHspCwcoUC6FU4NpUoK/h9bQ==" + }, "node_modules/@types/pbkdf2": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@types/pbkdf2/-/pbkdf2-3.1.0.tgz", @@ -7999,6 +8071,11 @@ "node": ">=12" } }, + "node_modules/dataloader": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-1.4.0.tgz", + "integrity": "sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==" + }, "node_modules/date-fns": { "version": "2.29.3", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", @@ -15406,6 +15483,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -17335,16 +17420,18 @@ "integrity": "sha512-4VlvkRUuCJvr2J6Y0ImW7NvTCriMi7ErOAqWk1y69vAdoNIzCF3yPmgeNzx+RQTLEDFq5sHfscn1MwHxP9hNfA==" }, "node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" + "tslib": "^2.1.0" } }, + "node_modules/rxjs/node_modules/tslib": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", + "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -20629,6 +20716,14 @@ "version": "10.16.0", "resolved": "https://registry.npmjs.org/preact/-/preact-10.16.0.tgz", "integrity": "sha512-XTSj3dJ4roKIC93pald6rWuB2qQJO9gO2iLLyTe87MrjQN+HklueLsmskbywEWqCHlclgz3/M4YLL2iBr9UmMA==" + }, + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "requires": { + "tslib": "^1.9.0" + } } } }, @@ -20717,6 +20812,14 @@ "multiformats": "^9.6.3" } }, + "@dcl/protocol": { + "version": "1.0.0-5810343446.commit-4ee6d08", + "resolved": "https://registry.npmjs.org/@dcl/protocol/-/protocol-1.0.0-5810343446.commit-4ee6d08.tgz", + "integrity": "sha512-9vhbn+IfdOn1qnJa7PE6kLsmT2G0pYoUXi2ydEelStEQw6Rkvfhc564mAx1zz+xLz7ZSE+9gWit90zroHNvavw==", + "requires": { + "@dcl/ts-proto": "1.154.0" + } + }, "@dcl/rpc": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@dcl/rpc/-/rpc-1.1.2.tgz", @@ -20774,6 +20877,60 @@ } } }, + "@dcl/ts-proto": { + "version": "1.154.0", + "resolved": "https://registry.npmjs.org/@dcl/ts-proto/-/ts-proto-1.154.0.tgz", + "integrity": "sha512-2S5AKMMPVZrVfa/1WRy4/h0niikcbu3Yf6dCoudh7ScG7BsyKAPC3CMg6IJKHzrmWS593UZClq7YJof6Vt4O+w==", + "requires": { + "@types/object-hash": "^3.0.2", + "case-anything": "^2.1.10", + "dataloader": "^1.4.0", + "object-hash": "^3.0.0", + "protobufjs": "^7.2.4", + "ts-poet": "^6.4.1", + "ts-proto-descriptors": "^1.15.0" + }, + "dependencies": { + "@types/node": { + "version": "20.4.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.9.tgz", + "integrity": "sha512-8e2HYcg7ohnTUbHk8focoklEQYvemQmu9M/f43DZVx43kHn0tE3BY/6gSDxS7k0SprtS0NHvj+L80cGLnoOUcQ==" + }, + "long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, + "protobufjs": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz", + "integrity": "sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==", + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + } + }, + "ts-proto-descriptors": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/ts-proto-descriptors/-/ts-proto-descriptors-1.15.0.tgz", + "integrity": "sha512-TYyJ7+H+7Jsqawdv+mfsEpZPTIj9siDHS6EMCzG/z3b/PZiphsX+mWtqFfFVe5/N0Th6V3elK9lQqjnrgTOfrg==", + "requires": { + "long": "^5.2.3", + "protobufjs": "^7.2.4" + } + } + } + }, "@dcl/ui-env": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@dcl/ui-env/-/ui-env-1.4.0.tgz", @@ -22089,21 +22246,6 @@ "global-tld-list": "^0.0.1139", "loglevel": "^1.8.1", "rxjs": "^7.8.0" - }, - "dependencies": { - "rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "requires": { - "tslib": "^2.1.0" - } - }, - "tslib": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", - "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==" - } } }, "@livekit/components-react": { @@ -23283,6 +23425,11 @@ "form-data": "^3.0.0" } }, + "@types/object-hash": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/object-hash/-/object-hash-3.0.2.tgz", + "integrity": "sha512-tfyXl1JPCf2hzIDK29gO7qGqJjThKBzg/Cn3bA68R9NmWdOx+f7k5mm4to/n43BHspCwcoUC6FU4NpUoK/h9bQ==" + }, "@types/pbkdf2": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@types/pbkdf2/-/pbkdf2-3.1.0.tgz", @@ -25809,6 +25956,11 @@ } } }, + "dataloader": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-1.4.0.tgz", + "integrity": "sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==" + }, "date-fns": { "version": "2.29.3", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", @@ -31519,6 +31671,11 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, + "object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==" + }, "object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -32927,11 +33084,18 @@ "integrity": "sha512-4VlvkRUuCJvr2J6Y0ImW7NvTCriMi7ErOAqWk1y69vAdoNIzCF3yPmgeNzx+RQTLEDFq5sHfscn1MwHxP9hNfA==" }, "rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "requires": { - "tslib": "^1.9.0" + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", + "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==" + } } }, "safe-buffer": { diff --git a/package.json b/package.json index f7e5bae..f44e360 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ }, "dependencies": { "@dcl/crypto": "^3.4.3", + "@dcl/protocol": "^1.0.0-5810343446.commit-4ee6d08", "@dcl/schemas": "^7.4.1", "@dcl/social-rpc-client": "^0.0.0-20230725183127.commit-e0fdbae", "@dcl/ui-env": "^1.4.0", @@ -38,7 +39,8 @@ "react-virtualized-auto-sizer": "^1.0.20", "react-window": "^1.8.9", "redux-logger": "^3.0.6", - "redux-saga": "^1.2.3" + "redux-saga": "^1.2.3", + "rxjs": "^7.8.1" }, "devDependencies": { "@dcl/eslint-config": "1.1.7", diff --git a/src/components/VideoConference/Chat/Chat.tsx b/src/components/VideoConference/Chat/Chat.tsx new file mode 100644 index 0000000..8e572f5 --- /dev/null +++ b/src/components/VideoConference/Chat/Chat.tsx @@ -0,0 +1,129 @@ +import type { ChatMessage, ReceivedChatMessage } from '@livekit/components-core' +import { ChatEntry, MessageFormatter, useMaybeLayoutContext, useRoomContext } from '@livekit/components-react' +import * as React from 'react' +import { cloneSingleChild, setupChat, useObservableState } from './utils' + +export type { ChatMessage, ReceivedChatMessage } + +export interface ChatProps extends React.HTMLAttributes { + messageFormatter?: MessageFormatter +} + +/** @public */ +export function useChat() { + const room = useRoomContext() + const [setup, setSetup] = React.useState>() + const isSending = useObservableState(setup?.isSendingObservable, false) + const chatMessages = useObservableState(setup?.messageObservable, []) + + React.useEffect(() => { + const setupChatReturn = setupChat(room) + setSetup(setupChatReturn) + return setupChatReturn.destroy + }, [room]) + + return { send: setup?.send, chatMessages, isSending } +} + +/** + * The Chat component adds a basis chat functionality to the LiveKit room. The messages are distributed to all participants + * in the room. Only users who are in the room at the time of dispatch will receive the message. + * + * @example + * ```tsx + * + * + * + * ``` + * @public + */ +export default function Chat({ messageFormatter, ...props }: ChatProps) { + const inputRef = React.useRef(null) + const ulRef = React.useRef(null) + + const { send, chatMessages, isSending } = useChat() + + const layoutContext = useMaybeLayoutContext() + const lastReadMsgAt = React.useRef(0) + + async function handleSubmit(event: React.FormEvent) { + event.preventDefault() + if (inputRef.current && inputRef.current.value.trim() !== '') { + if (send) { + await send(inputRef.current.value) + inputRef.current.value = '' + inputRef.current.focus() + } + } + } + + React.useEffect(() => { + if (ulRef) { + ulRef.current?.scrollTo({ top: ulRef.current.scrollHeight }) + } + }, [ulRef, chatMessages]) + + React.useEffect(() => { + if (!layoutContext || chatMessages.length === 0) { + return + } + + if ( + layoutContext.widget.state?.showChat && + chatMessages.length > 0 && + lastReadMsgAt.current !== chatMessages[chatMessages.length - 1]?.timestamp + ) { + lastReadMsgAt.current = chatMessages[chatMessages.length - 1]?.timestamp + return + } + + const unreadMessageCount = chatMessages.filter(msg => !lastReadMsgAt.current || msg.timestamp > lastReadMsgAt.current).length + + const { widget } = layoutContext + if (unreadMessageCount > 0 && widget.state?.unreadMessages !== unreadMessageCount) { + widget.dispatch?.({ msg: 'unread_msg', count: unreadMessageCount }) + } + }, [chatMessages, layoutContext?.widget]) + + return ( +
+
    + {props.children + ? chatMessages.map((msg, idx) => + cloneSingleChild(props.children, { + entry: msg, + key: idx, + messageFormatter + }) + ) + : chatMessages.map((msg, idx, allMsg) => { + const hideName = idx >= 1 && allMsg[idx - 1].from === msg.from + // If the time delta between two messages is bigger than 60s show timestamp. + const hideTimestamp = idx >= 1 && msg.timestamp - allMsg[idx - 1].timestamp < 60_000 + + return ( + + ) + })} +
+
+ + +
+
+ ) +} diff --git a/src/components/VideoConference/Chat/index.ts b/src/components/VideoConference/Chat/index.ts new file mode 100644 index 0000000..50f2623 --- /dev/null +++ b/src/components/VideoConference/Chat/index.ts @@ -0,0 +1,2 @@ +import Chat from './Chat' +export default Chat diff --git a/src/components/VideoConference/Chat/utils.ts b/src/components/VideoConference/Chat/utils.ts new file mode 100644 index 0000000..8d862d7 --- /dev/null +++ b/src/components/VideoConference/Chat/utils.ts @@ -0,0 +1,97 @@ +import { DataTopic, sendMessage, setupDataMessageHandler } from '@livekit/components-core' +import { ReceivedChatMessage } from '@livekit/components-react' +import { DataPacket_Kind, Participant, Room } from 'livekit-client' +import * as React from 'react' +import { BehaviorSubject, Subject, takeUntil, map, scan, filter } from 'rxjs' +import type { Observable } from 'rxjs' +import { Packet } from '@dcl/protocol/out-js/decentraland/kernel/comms/rfc4/comms.gen' + +/** + * @internal + */ +export function useObservableState(observable: Observable | undefined, startWith: T) { + const [state, setState] = React.useState(startWith) + React.useEffect(() => { + // observable state doesn't run in SSR + if (typeof window === 'undefined' || !observable) return + const subscription = observable.subscribe(setState) + return () => subscription.unsubscribe() + }, [observable]) + return state +} + +export function cloneSingleChild(children: React.ReactNode | React.ReactNode[], props?: Record, key?: any) { + return React.Children.map(children, child => { + // Checking isValidElement is the safe way and avoids a typescript + // error too. + if (React.isValidElement(child) && React.Children.only(children)) { + return React.cloneElement(child, { ...props, key }) + } + return child + }) +} + +export function setupChat(room: Room) { + const onDestroyObservable = new Subject() + const messageSubject = new Subject<{ + payload: Uint8Array + topic: string | undefined + from: Participant | undefined + }>() + + /** Subscribe to all messages send over the wire. */ + const { messageObservable } = setupDataMessageHandler(room) + messageObservable.pipe(takeUntil(onDestroyObservable)).subscribe(messageSubject) + + /** Build up the message array over time. */ + const messagesObservable = messageSubject.pipe( + map(msg => { + const packet = Packet.decode(msg.payload) + return { packet, msg } + }), + filter(({ packet }) => packet.message?.$case === 'chat'), + map(({ packet, msg }) => { + if (packet.message?.$case === 'chat') { + const { timestamp, message } = packet.message.chat + return { from: msg.from, timestamp, message } + } + throw new Error('Found msg without chat') + }), + scan((acc, value) => [...acc, value], []), + takeUntil(onDestroyObservable) + ) + + const isSending$ = new BehaviorSubject(false) + + const send = async (message: string) => { + const encodedMsg = Packet.encode({ + message: { + $case: 'chat', + chat: { + timestamp: Date.now(), + message: message + } + } + }).finish() + isSending$.next(true) + try { + await sendMessage(room.localParticipant, encodedMsg, undefined, { + kind: DataPacket_Kind.RELIABLE + }) + messageSubject.next({ + payload: encodedMsg, + topic: DataTopic.CHAT, + from: room.localParticipant + }) + } finally { + isSending$.next(false) + } + } + + function destroy() { + onDestroyObservable.next() + onDestroyObservable.complete() + } + + return { messageObservable: messagesObservable, isSendingObservable: isSending$, send, destroy } +} diff --git a/src/components/VideoConference/Videoconference.tsx b/src/components/VideoConference/Videoconference.tsx index 3b4a848..0f164b3 100644 --- a/src/components/VideoConference/Videoconference.tsx +++ b/src/components/VideoConference/Videoconference.tsx @@ -2,7 +2,6 @@ import * as React from 'react' import { isEqualTrackRef, isTrackReference, log, isWeb } from '@livekit/components-core' import { CarouselView, - Chat, ConnectionStateToast, ControlBar, FocusLayout, @@ -14,11 +13,34 @@ import { useCreateLayoutContext, // useParticipants, usePinnedTracks, - useTracks + useTracks, + MessageEncoder, + MessageDecoder } from '@livekit/components-react' import { RoomEvent, Track } from 'livekit-client' import ParticipantTile from './ParticipantTile' import type { TrackReferenceOrPlaceholder, WidgetState } from '@livekit/components-core' +import { ChatMessage } from '@livekit/components-react' +import { Packet } from '@dcl/protocol/out-js/decentraland/kernel/comms/rfc4/comms.gen' +import Chat from './Chat' + +const messageDecoder: MessageDecoder = (message: Uint8Array) => { + const packet = Packet.decode(message) + if (packet.message && packet.message.$case === 'chat') { + const { timestamp, message } = packet.message.chat + return { timestamp, message } + } else if (packet.message?.$case === 'position') { + } + + return { + message: 'Error', + timestamp: 0 + } +} + +const messageEncoder: MessageEncoder = (message: ChatMessage) => { + return new Uint8Array() +} /** * @public @@ -44,7 +66,7 @@ export interface VideoConferenceProps extends React.HTMLAttributes({ showChat: false }) + const [widgetState, setWidgetState] = React.useState({ showChat: false, unreadMessages: 0 }) const lastAutoFocusedScreenShareTrack = React.useRef(null) const tracks = useTracks( @@ -115,7 +137,12 @@ export function VideoConference({ chatMessageFormatter, ...props }: VideoConfere )} - + )}