use custom chat

This commit is contained in:
jmoguilevsky 2023-08-09 15:45:47 -03:00
parent c50b2319b0
commit 5b1c6d2deb
No known key found for this signature in database
GPG Key ID: D097BD46EA6103FA
6 changed files with 466 additions and 45 deletions

244
package-lock.json generated
View File

@ -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": {

View File

@ -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",

View File

@ -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<HTMLDivElement> {
messageFormatter?: MessageFormatter
}
/** @public */
export function useChat() {
const room = useRoomContext()
const [setup, setSetup] = React.useState<ReturnType<typeof setupChat>>()
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
* <LiveKitRoom>
* <Chat />
* </LiveKitRoom>
* ```
* @public
*/
export default function Chat({ messageFormatter, ...props }: ChatProps) {
const inputRef = React.useRef<HTMLInputElement>(null)
const ulRef = React.useRef<HTMLUListElement>(null)
const { send, chatMessages, isSending } = useChat()
const layoutContext = useMaybeLayoutContext()
const lastReadMsgAt = React.useRef<ChatMessage['timestamp']>(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 (
<div {...props} className="lk-chat">
<ul className="lk-list lk-chat-messages" ref={ulRef}>
{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 (
<ChatEntry
key={idx}
hideName={hideName}
hideTimestamp={hideName === false ? false : hideTimestamp} // If we show the name always show the timestamp as well.
entry={msg}
messageFormatter={messageFormatter}
/>
)
})}
</ul>
<form className="lk-chat-form" onSubmit={handleSubmit}>
<input
className="lk-form-control lk-chat-form-input"
disabled={isSending}
ref={inputRef}
type="text"
placeholder="Enter a message..."
/>
<button type="submit" className="lk-button lk-chat-form-button" disabled={isSending}>
Send
</button>
</form>
</div>
)
}

View File

@ -0,0 +1,2 @@
import Chat from './Chat'
export default Chat

View File

@ -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<T>(observable: Observable<T> | undefined, startWith: T) {
const [state, setState] = React.useState<T>(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<string, any>, 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<void>()
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<ReceivedChatMessage, ReceivedChatMessage[]>((acc, value) => [...acc, value], []),
takeUntil(onDestroyObservable)
)
const isSending$ = new BehaviorSubject<boolean>(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 }
}

View File

@ -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<HTMLDivElemen
* @public
*/
export function VideoConference({ chatMessageFormatter, ...props }: VideoConferenceProps) {
const [widgetState, setWidgetState] = React.useState<WidgetState>({ showChat: false })
const [widgetState, setWidgetState] = React.useState<WidgetState>({ showChat: false, unreadMessages: 0 })
const lastAutoFocusedScreenShareTrack = React.useRef<TrackReferenceOrPlaceholder | null>(null)
const tracks = useTracks(
@ -115,7 +137,12 @@ export function VideoConference({ chatMessageFormatter, ...props }: VideoConfere
)}
<ControlBar controls={{ chat: true }} />
</div>
<Chat style={{ display: widgetState.showChat ? 'flex' : 'none' }} messageFormatter={chatMessageFormatter} />
<Chat
style={{ display: widgetState.showChat ? 'flex' : 'none' }}
messageFormatter={chatMessageFormatter}
messageEncoder={messageEncoder}
messageDecoder={messageDecoder}
/>
</LayoutContextProvider>
)}
<RoomAudioRenderer />