feat: Update Chat Panel (#9)

* chore: Upgrade the @dcl/eslint-config package

* fix: eslint

* feat: Add new SendIcon

* fix: Show a max of 12 participants

* feat: Update layout style

* feat: Update Chat Panel styles

* refactor: Move useChat definition to hooks and utils

* feat: Update ChatEntry component

* feat: Add translations
This commit is contained in:
Gabriel Díaz 2023-08-17 13:25:06 -03:00 committed by GitHub
parent 61aaaeeebc
commit 8e62a25833
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 432 additions and 190 deletions

186
package-lock.json generated
View File

@ -35,7 +35,7 @@
"rxjs": "^7.8.1"
},
"devDependencies": {
"@dcl/eslint-config": "1.1.7",
"@dcl/eslint-config": "^1.1.10",
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@swc/core": "^1.3.69",
@ -1327,14 +1327,14 @@
}
},
"node_modules/@dcl/eslint-config": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@dcl/eslint-config/-/eslint-config-1.1.7.tgz",
"integrity": "sha512-MwAL4hFxwXZ5VeJTPFDnzjX6Qe/PKOaxJrkOQN8a0jkwDjYw9rTMcj+FIAoLu+pBzUg6B+XyFm0OqpAvIxHjeQ==",
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/@dcl/eslint-config/-/eslint-config-1.1.10.tgz",
"integrity": "sha512-6cIgvYKExnhn6DD6C/p3eQo+jlqFH5cMIbI8leaxAL6mym6jlLzKEqvksHW0S120Zgh5QqQ2B3MZGUgj8CBStQ==",
"dev": true,
"dependencies": {
"@typescript-eslint/eslint-plugin": "^5.60.1",
"@typescript-eslint/parser": "^5.60.1",
"eslint": "^8.43.0",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"eslint": "^8.44.0",
"eslint-config-prettier": "^8.8.0",
"eslint-import-resolver-babel-module": "^5.3.2",
"eslint-import-resolver-typescript": "^3.5.5",
@ -5096,15 +5096,15 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "5.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.61.0.tgz",
"integrity": "sha512-A5l/eUAug103qtkwccSCxn8ZRwT+7RXWkFECdA4Cvl1dOlDUgTpAOfSEElZn2uSUxhdDpnCdetrf0jvU4qrL+g==",
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
"integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
"dev": true,
"dependencies": {
"@eslint-community/regexpp": "^4.4.0",
"@typescript-eslint/scope-manager": "5.61.0",
"@typescript-eslint/type-utils": "5.61.0",
"@typescript-eslint/utils": "5.61.0",
"@typescript-eslint/scope-manager": "5.62.0",
"@typescript-eslint/type-utils": "5.62.0",
"@typescript-eslint/utils": "5.62.0",
"debug": "^4.3.4",
"graphemer": "^1.4.0",
"ignore": "^5.2.0",
@ -5130,14 +5130,14 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "5.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.61.0.tgz",
"integrity": "sha512-yGr4Sgyh8uO6fSi9hw3jAFXNBHbCtKKFMdX2IkT3ZqpKmtAq3lHS4ixB/COFuAIJpwl9/AqF7j72ZDWYKmIfvg==",
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
"integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
"dev": true,
"dependencies": {
"@typescript-eslint/scope-manager": "5.61.0",
"@typescript-eslint/types": "5.61.0",
"@typescript-eslint/typescript-estree": "5.61.0",
"@typescript-eslint/scope-manager": "5.62.0",
"@typescript-eslint/types": "5.62.0",
"@typescript-eslint/typescript-estree": "5.62.0",
"debug": "^4.3.4"
},
"engines": {
@ -5157,13 +5157,13 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "5.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.61.0.tgz",
"integrity": "sha512-W8VoMjoSg7f7nqAROEmTt6LoBpn81AegP7uKhhW5KzYlehs8VV0ZW0fIDVbcZRcaP3aPSW+JZFua+ysQN+m/Nw==",
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz",
"integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "5.61.0",
"@typescript-eslint/visitor-keys": "5.61.0"
"@typescript-eslint/types": "5.62.0",
"@typescript-eslint/visitor-keys": "5.62.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@ -5174,13 +5174,13 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "5.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.61.0.tgz",
"integrity": "sha512-kk8u//r+oVK2Aj3ph/26XdH0pbAkC2RiSjUYhKD+PExemG4XSjpGFeyZ/QM8lBOa7O8aGOU+/yEbMJgQv/DnCg==",
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz",
"integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==",
"dev": true,
"dependencies": {
"@typescript-eslint/typescript-estree": "5.61.0",
"@typescript-eslint/utils": "5.61.0",
"@typescript-eslint/typescript-estree": "5.62.0",
"@typescript-eslint/utils": "5.62.0",
"debug": "^4.3.4",
"tsutils": "^3.21.0"
},
@ -5201,9 +5201,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "5.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.61.0.tgz",
"integrity": "sha512-ldyueo58KjngXpzloHUog/h9REmHl59G1b3a5Sng1GfBo14BkS3ZbMEb3693gnP1k//97lh7bKsp6/V/0v1veQ==",
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz",
"integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@ -5214,13 +5214,13 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "5.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.61.0.tgz",
"integrity": "sha512-Fud90PxONnnLZ36oR5ClJBLTLfU4pIWBmnvGwTbEa2cXIqj70AEDEmOmpkFComjBZ/037ueKrOdHuYmSFVD7Rw==",
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz",
"integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "5.61.0",
"@typescript-eslint/visitor-keys": "5.61.0",
"@typescript-eslint/types": "5.62.0",
"@typescript-eslint/visitor-keys": "5.62.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@ -5241,17 +5241,17 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "5.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.61.0.tgz",
"integrity": "sha512-mV6O+6VgQmVE6+xzlA91xifndPW9ElFW8vbSF0xCT/czPXVhwDewKila1jOyRwa9AE19zKnrr7Cg5S3pJVrTWQ==",
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz",
"integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@types/json-schema": "^7.0.9",
"@types/semver": "^7.3.12",
"@typescript-eslint/scope-manager": "5.61.0",
"@typescript-eslint/types": "5.61.0",
"@typescript-eslint/typescript-estree": "5.61.0",
"@typescript-eslint/scope-manager": "5.62.0",
"@typescript-eslint/types": "5.62.0",
"@typescript-eslint/typescript-estree": "5.62.0",
"eslint-scope": "^5.1.1",
"semver": "^7.3.7"
},
@ -5267,12 +5267,12 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "5.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.61.0.tgz",
"integrity": "sha512-50XQ5VdbWrX06mQXhy93WywSFZZGsv3EOjq+lqp6WC2t+j3mb6A9xYVdrRxafvK88vg9k9u+CT4l6D8PEatjKg==",
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz",
"integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "5.61.0",
"@typescript-eslint/types": "5.62.0",
"eslint-visitor-keys": "^3.3.0"
},
"engines": {
@ -20784,14 +20784,14 @@
}
},
"@dcl/eslint-config": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@dcl/eslint-config/-/eslint-config-1.1.7.tgz",
"integrity": "sha512-MwAL4hFxwXZ5VeJTPFDnzjX6Qe/PKOaxJrkOQN8a0jkwDjYw9rTMcj+FIAoLu+pBzUg6B+XyFm0OqpAvIxHjeQ==",
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/@dcl/eslint-config/-/eslint-config-1.1.10.tgz",
"integrity": "sha512-6cIgvYKExnhn6DD6C/p3eQo+jlqFH5cMIbI8leaxAL6mym6jlLzKEqvksHW0S120Zgh5QqQ2B3MZGUgj8CBStQ==",
"dev": true,
"requires": {
"@typescript-eslint/eslint-plugin": "^5.60.1",
"@typescript-eslint/parser": "^5.60.1",
"eslint": "^8.43.0",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"eslint": "^8.44.0",
"eslint-config-prettier": "^8.8.0",
"eslint-import-resolver-babel-module": "^5.3.2",
"eslint-import-resolver-typescript": "^3.5.5",
@ -23593,15 +23593,15 @@
"dev": true
},
"@typescript-eslint/eslint-plugin": {
"version": "5.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.61.0.tgz",
"integrity": "sha512-A5l/eUAug103qtkwccSCxn8ZRwT+7RXWkFECdA4Cvl1dOlDUgTpAOfSEElZn2uSUxhdDpnCdetrf0jvU4qrL+g==",
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
"integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
"dev": true,
"requires": {
"@eslint-community/regexpp": "^4.4.0",
"@typescript-eslint/scope-manager": "5.61.0",
"@typescript-eslint/type-utils": "5.61.0",
"@typescript-eslint/utils": "5.61.0",
"@typescript-eslint/scope-manager": "5.62.0",
"@typescript-eslint/type-utils": "5.62.0",
"@typescript-eslint/utils": "5.62.0",
"debug": "^4.3.4",
"graphemer": "^1.4.0",
"ignore": "^5.2.0",
@ -23611,53 +23611,53 @@
}
},
"@typescript-eslint/parser": {
"version": "5.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.61.0.tgz",
"integrity": "sha512-yGr4Sgyh8uO6fSi9hw3jAFXNBHbCtKKFMdX2IkT3ZqpKmtAq3lHS4ixB/COFuAIJpwl9/AqF7j72ZDWYKmIfvg==",
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
"integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
"dev": true,
"requires": {
"@typescript-eslint/scope-manager": "5.61.0",
"@typescript-eslint/types": "5.61.0",
"@typescript-eslint/typescript-estree": "5.61.0",
"@typescript-eslint/scope-manager": "5.62.0",
"@typescript-eslint/types": "5.62.0",
"@typescript-eslint/typescript-estree": "5.62.0",
"debug": "^4.3.4"
}
},
"@typescript-eslint/scope-manager": {
"version": "5.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.61.0.tgz",
"integrity": "sha512-W8VoMjoSg7f7nqAROEmTt6LoBpn81AegP7uKhhW5KzYlehs8VV0ZW0fIDVbcZRcaP3aPSW+JZFua+ysQN+m/Nw==",
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz",
"integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==",
"dev": true,
"requires": {
"@typescript-eslint/types": "5.61.0",
"@typescript-eslint/visitor-keys": "5.61.0"
"@typescript-eslint/types": "5.62.0",
"@typescript-eslint/visitor-keys": "5.62.0"
}
},
"@typescript-eslint/type-utils": {
"version": "5.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.61.0.tgz",
"integrity": "sha512-kk8u//r+oVK2Aj3ph/26XdH0pbAkC2RiSjUYhKD+PExemG4XSjpGFeyZ/QM8lBOa7O8aGOU+/yEbMJgQv/DnCg==",
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz",
"integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==",
"dev": true,
"requires": {
"@typescript-eslint/typescript-estree": "5.61.0",
"@typescript-eslint/utils": "5.61.0",
"@typescript-eslint/typescript-estree": "5.62.0",
"@typescript-eslint/utils": "5.62.0",
"debug": "^4.3.4",
"tsutils": "^3.21.0"
}
},
"@typescript-eslint/types": {
"version": "5.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.61.0.tgz",
"integrity": "sha512-ldyueo58KjngXpzloHUog/h9REmHl59G1b3a5Sng1GfBo14BkS3ZbMEb3693gnP1k//97lh7bKsp6/V/0v1veQ==",
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz",
"integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==",
"dev": true
},
"@typescript-eslint/typescript-estree": {
"version": "5.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.61.0.tgz",
"integrity": "sha512-Fud90PxONnnLZ36oR5ClJBLTLfU4pIWBmnvGwTbEa2cXIqj70AEDEmOmpkFComjBZ/037ueKrOdHuYmSFVD7Rw==",
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz",
"integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==",
"dev": true,
"requires": {
"@typescript-eslint/types": "5.61.0",
"@typescript-eslint/visitor-keys": "5.61.0",
"@typescript-eslint/types": "5.62.0",
"@typescript-eslint/visitor-keys": "5.62.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@ -23666,28 +23666,28 @@
}
},
"@typescript-eslint/utils": {
"version": "5.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.61.0.tgz",
"integrity": "sha512-mV6O+6VgQmVE6+xzlA91xifndPW9ElFW8vbSF0xCT/czPXVhwDewKila1jOyRwa9AE19zKnrr7Cg5S3pJVrTWQ==",
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz",
"integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==",
"dev": true,
"requires": {
"@eslint-community/eslint-utils": "^4.2.0",
"@types/json-schema": "^7.0.9",
"@types/semver": "^7.3.12",
"@typescript-eslint/scope-manager": "5.61.0",
"@typescript-eslint/types": "5.61.0",
"@typescript-eslint/typescript-estree": "5.61.0",
"@typescript-eslint/scope-manager": "5.62.0",
"@typescript-eslint/types": "5.62.0",
"@typescript-eslint/typescript-estree": "5.62.0",
"eslint-scope": "^5.1.1",
"semver": "^7.3.7"
}
},
"@typescript-eslint/visitor-keys": {
"version": "5.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.61.0.tgz",
"integrity": "sha512-50XQ5VdbWrX06mQXhy93WywSFZZGsv3EOjq+lqp6WC2t+j3mb6A9xYVdrRxafvK88vg9k9u+CT4l6D8PEatjKg==",
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz",
"integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==",
"dev": true,
"requires": {
"@typescript-eslint/types": "5.61.0",
"@typescript-eslint/types": "5.62.0",
"eslint-visitor-keys": "^3.3.0"
}
},

View File

@ -44,7 +44,7 @@
"rxjs": "^7.8.1"
},
"devDependencies": {
"@dcl/eslint-config": "1.1.7",
"@dcl/eslint-config": "^1.1.10",
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@swc/core": "^1.3.69",

View File

@ -0,0 +1,23 @@
import * as React from 'react'
import type { SVGProps } from 'react'
const SendIcon = (props: SVGProps<SVGSVGElement>) => (
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M25.7422 5.75146L14.0085 24.4085L12.2228 15.2179L4.19727 10.3964L25.7422 5.75146Z"
stroke={props.stroke ?? 'white'}
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M12.167 15.2561L25.7411 5.75146"
stroke={props.stroke ?? 'white'}
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
)
export default SendIcon

View File

@ -1,5 +1,7 @@
.ControlBarContainer {
position: relative;
height: var(--lk-control-bar-height);
border-top: none;
}
.ControlBarRightButtonGroup {
@ -10,3 +12,7 @@
align-items: center;
justify-content: center;
}
.chatToggleButton {
background-color: transparent;
}

View File

@ -176,7 +176,7 @@ export function ControlBar({ variation, controls, ...props }: ControlBarProps) {
<StartAudio label="Start Audio" />
<div className={styles.ControlBarRightButtonGroup}>
{visibleControls.chat && (
<ChatToggle>
<ChatToggle className={styles.chatToggleButton}>
{showIcon && <ChatIcon />}
{showText && 'Chat'}
</ChatToggle>

View File

@ -1,5 +1,6 @@
import * as React from 'react'
import { isParticipantSourcePinned } from '@livekit/components-core'
import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
import {
AudioTrack,
ConnectionQualityIndicator,
@ -15,10 +16,9 @@ import {
useParticipantTile
} from '@livekit/components-react'
import { Track } from 'livekit-client'
import type { Participant } from 'livekit-client'
import Profile from 'decentraland-dapps/dist/containers/Profile'
import type { Props } from './ParticipantTile.types'
import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
import type { Participant } from 'livekit-client'
/** @public */
export function ParticipantContextIfNeeded(

View File

@ -1,6 +1,57 @@
.container {
display: none;
height: 100%;
width: 100%;
flex-direction: column;
align-items: stretch;
}
.open {
display: flex;
}
.headerContainer {
display: flex;
justify-content: space-between;
width: 100%;
margin-bottom: 16px;
}
.close:global(.ui.button) {
padding: 0;
min-width: 24px;
width: 24px;
background: url('../../../../assets/icons/Close.svg');
color: var(--toast-text);
}
.close:global(.ui.button):hover {
transform: none;
box-shadow: none;
}
.title:global(.ui.header) {
font-size: 24px;
margin-bottom: 0px;
font-size: 24px;
font-weight: 600;
line-height: 24px;
color: var(--toast-text);
}
.chatMessages {
justify-content: flex-start;
}
.form {
display: flex;
}
.input {
width: 100%;
margin-bottom: 24px;
}
.button {
height: 42px;
}

View File

@ -1,32 +1,17 @@
import * as React from 'react'
import type { ChatMessage, ReceivedChatMessage } from '@livekit/components-core'
import { MessageFormatter, useLocalParticipant, useMaybeLayoutContext, useRoomContext } from '@livekit/components-react'
import React, { useCallback, useEffect, useRef } from 'react'
import type { ChatMessage } from '@livekit/components-core'
import { useLocalParticipant } from '@livekit/components-react'
import classNames from 'classnames'
import { t } from 'decentraland-dapps/dist/modules/translation/utils'
import { Button, Header, Input } from 'decentraland-ui'
import SendIcon from '../../../../assets/icons/SendIcon'
import { useChat } from '../../../../hooks/useChat'
import { useLayoutContext } from '../../../../hooks/useLayoutContext'
import { cloneSingleChild } from '../../../../utils/chat'
import ChatEntry from './ChatEntry'
import { cloneSingleChild, setupChat, useObservableState } from './utils'
import { Props } from './Chat.types'
import styles from './Chat.module.css'
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.
@ -39,14 +24,13 @@ export function useChat() {
* ```
* @public
*/
export default function Chat({ messageFormatter, ...props }: ChatProps) {
const inputRef = React.useRef<HTMLInputElement>(null)
const ulRef = React.useRef<HTMLUListElement>(null)
export default function Chat({ messageFormatter, isOpen, ...props }: Props) {
const { send, chatMessages, isSending } = useChat()
const inputRef = useRef<HTMLInputElement>(null)
const ulRef = useRef<HTMLUListElement>(null)
const layoutContext = useMaybeLayoutContext()
const lastReadMsgAt = React.useRef<ChatMessage['timestamp']>(0)
const layoutContext = useLayoutContext()
const lastReadMsgAt = useRef<ChatMessage['timestamp']>(0)
async function handleSubmit(event: React.FormEvent) {
event.preventDefault()
@ -59,13 +43,13 @@ export default function Chat({ messageFormatter, ...props }: ChatProps) {
}
}
React.useEffect(() => {
useEffect(() => {
if (ulRef) {
ulRef.current?.scrollTo({ top: ulRef.current.scrollHeight })
}
}, [ulRef, chatMessages])
React.useEffect(() => {
useEffect(() => {
if (!layoutContext || chatMessages.length === 0) {
return
}
@ -87,11 +71,24 @@ export default function Chat({ messageFormatter, ...props }: ChatProps) {
}
}, [chatMessages, layoutContext?.widget])
const handleClosePanel = useCallback(() => {
const { dispatch } = layoutContext.widget
if (dispatch) {
dispatch({ msg: 'hide_chat' })
}
}, [layoutContext])
const localParticipant = useLocalParticipant().localParticipant
return (
<div {...props} className={styles.container}>
<ul className="lk-list lk-chat-messages" ref={ulRef}>
<div {...props} className={classNames(styles.container, { [styles['open']]: isOpen })}>
<div className={styles.headerContainer}>
<Header className={styles.title} size="medium">
{t('chat_panel.title')}
</Header>
<Button className={styles.close} onClick={handleClosePanel} />
</div>
<ul className={classNames(styles.chatMessages, 'lk-list', 'lk-chat-messages')} ref={ulRef}>
{props.children
? chatMessages.map((msg, idx) =>
cloneSingleChild(props.children, {
@ -101,9 +98,9 @@ export default function Chat({ messageFormatter, ...props }: ChatProps) {
})
)
: 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
const hideName = idx >= 1 && allMsg[idx - 1].from === msg.from && hideTimestamp
return (
<ChatEntry
@ -117,17 +114,13 @@ export default function Chat({ messageFormatter, ...props }: ChatProps) {
})}
</ul>
{localParticipant.permissions?.canPublish && (
<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 className={styles.form} onSubmit={handleSubmit}>
<Input className={styles.input} disabled={isSending} placeholder="Enter a message">
<input ref={inputRef} />
<Button type="submit" className={styles.button} basic size="small" disabled={isSending}>
<SendIcon />
</Button>
</Input>
</form>
)}
</div>

View File

@ -0,0 +1,9 @@
import type { ChatMessage, ReceivedChatMessage } from '@livekit/components-core'
import { MessageFormatter } from '@livekit/components-react'
export type { ChatMessage, ReceivedChatMessage }
export type Props = React.HTMLAttributes<HTMLDivElement> & {
isOpen: boolean
messageFormatter?: MessageFormatter
}

View File

@ -0,0 +1,15 @@
import { connect } from 'react-redux'
import { getData as getProfiles } from 'decentraland-dapps/dist/modules/profile/selectors'
import { getAddress } from 'decentraland-dapps/dist/modules/wallet/selectors'
import { RootState } from '../../../../../modules/reducer'
import ChatEntry from './ChatEntry'
import type { MapStateProps } from './ChatEntry.types'
const mapStateToProps = (state: RootState): MapStateProps => {
return {
address: getAddress(state) as string,
profiles: getProfiles(state)
}
}
export default connect(mapStateToProps)(ChatEntry)

View File

@ -0,0 +1,42 @@
.chatEntry:not(:last-child) {
margin-bottom: 16px;
}
.sameSender {
margin-top: -16px;
}
.info {
display: flex;
align-items: center;
margin-bottom: 10px;
}
:global(li.lk-chat-entry span.Profile > div.dcl.avatar-face.tiny.inline) {
width: 26px;
height: 26px;
border: 2px solid #fcfcfc;
background: #ecebed;
}
.name {
color: #fff;
font-size: 14px;
font-weight: 600;
line-height: normal;
text-transform: uppercase;
margin-right: 6px;
}
.timestamp {
color: #716b7c;
font-size: 12px;
font-weight: 700;
}
.message {
color: #fff;
font-size: 14px;
font-weight: 400;
line-break: anywhere;
}

View File

@ -1,23 +1,10 @@
import * as React from 'react'
import { tokenize, createDefaultGrammar, ReceivedChatMessage } from '@livekit/components-core'
import { MessageFormatter } from '@livekit/components-react'
import React, { useMemo } from 'react'
import { tokenize, createDefaultGrammar } from '@livekit/components-core'
import classNames from 'classnames'
import Profile from 'decentraland-dapps/dist/containers/Profile'
/**
* ChatEntry composes the HTML div element under the hood, so you can pass all its props.
* These are the props specific to the ChatEntry component:
* @public
*/
export interface ChatEntryProps extends React.HTMLAttributes<HTMLLIElement> {
/** The chat massage object to display. */
entry: ReceivedChatMessage
/** Hide sender name. Useful when displaying multiple consecutive chat messages from the same person. */
hideName?: boolean
/** Hide message timestamp. */
hideTimestamp?: boolean
/** An optional formatter for the message body. */
messageFormatter?: MessageFormatter
}
import { t } from 'decentraland-dapps/dist/modules/translation/utils'
import { Props } from './ChatEntry.types'
import styles from './ChatEntry.module.css'
/**
* The `ChatEntry` component holds and displays one chat message.
@ -31,28 +18,33 @@ export interface ChatEntryProps extends React.HTMLAttributes<HTMLLIElement> {
* @see `Chat`
* @public
*/
export function ChatEntry({ entry, hideName = false, hideTimestamp = false, messageFormatter, ...props }: ChatEntryProps) {
const formattedMessage = React.useMemo(() => {
export function ChatEntry({ entry, hideName = false, hideTimestamp = false, messageFormatter, address, profiles, ...props }: Props) {
const formattedMessage = useMemo(() => {
return messageFormatter ? messageFormatter(entry.message) : entry.message
}, [entry.message, messageFormatter])
const time = new Date(entry.timestamp)
const locale = navigator ? navigator.language : 'en-US'
return (
<li
className="lk-chat-entry"
className={classNames('lk-chat-entry', styles.chatEntry, { [styles.sameSender]: hideName && hideTimestamp })}
title={time.toLocaleTimeString(locale, { timeStyle: 'full' })}
data-lk-message-origin={entry.from?.isLocal ? 'local' : 'remote'}
{...props}
>
{(!hideTimestamp || !hideName) && (
<span className="lk-meta-data">
<span className={styles.info}>
{entry.from?.identity && <Profile address={entry.from?.identity} imageOnly size="normal" />}
{!hideName && <strong className="lk-participant-name">{entry.from?.name ?? entry.from?.identity}</strong>}
{!hideTimestamp && <span className="lk-timestamp">{time.toLocaleTimeString(locale, { timeStyle: 'short' })}</span>}
{!hideName && entry.from?.identity && (
<strong className={styles.name}>
{entry.from?.identity === address ? t('chat_entry.you') : profiles[entry.from?.identity].avatars[0].name}
</strong>
)}
{!hideTimestamp && <span className={styles.timestamp}>{time.toLocaleTimeString(locale, { timeStyle: 'short' })}</span>}
</span>
)}
<span className="lk-message-body">{formattedMessage}</span>
<span className={styles.message}>{formattedMessage}</span>
</li>
)
}
@ -60,11 +52,11 @@ export function ChatEntry({ entry, hideName = false, hideTimestamp = false, mess
/** @public */
export function formatChatMessageLinks(message: string): React.ReactNode {
return tokenize(message, createDefaultGrammar()).map((tok, i) => {
if (typeof tok === `string`) {
if (typeof tok === 'string') {
return tok
} else {
const content = tok.content.toString()
const href = tok.type === `url` ? (/^http(s?):\/\//.test(content) ? content : `https://${content}`) : `mailto:${content}`
const href = tok.type === 'url' ? (/^http(s?):\/\//.test(content) ? content : `https://${content}`) : `mailto:${content}`
return (
<a className="lk-chat-link" key={i} href={href} target="_blank" rel="noreferrer">
{content}
@ -73,3 +65,5 @@ export function formatChatMessageLinks(message: string): React.ReactNode {
}
})
}
export default React.memo(ChatEntry)

View File

@ -0,0 +1,25 @@
import { ReceivedChatMessage } from '@livekit/components-core'
import { MessageFormatter } from '@livekit/components-react'
/**
* ChatEntry composes the HTML div element under the hood, so you can pass all its props.
* These are the props specific to the ChatEntry component:
* @public
*/
export type OwnProps = React.HTMLAttributes<HTMLLIElement> & {
/** The chat massage object to display. */
entry: ReceivedChatMessage
/** Hide sender name. Useful when displaying multiple consecutive chat messages from the same person. */
hideName?: boolean
/** Hide message timestamp. */
hideTimestamp?: boolean
/** An optional formatter for the message body. */
messageFormatter?: MessageFormatter
}
export type Props = OwnProps & {
address: string
profiles: ReturnType<typeof import('decentraland-dapps/dist/modules/profile/selectors').getData>
}
export type MapStateProps = Pick<Props, 'address' | 'profiles'>

View File

@ -1,3 +1,3 @@
import { ChatEntry } from './ChatEntry'
import ChatEntry from './ChatEntry.container'
export default ChatEntry

View File

@ -47,7 +47,7 @@
color: var(--toast-text);
}
.profile:not(:last-child) {
.profile {
margin-bottom: 18px;
}
@ -69,3 +69,17 @@
font-weight: 600;
line-height: 24px;
}
:global(div.ProfileContainer > span.Profile > div.dcl.avatar-face.tiny.inline),
:global(div.TrimmedProfileContainer span.Profile div.dcl.avatar-face.tiny.inline) {
border: 2px solid #fcfcfc;
background: #ecebed;
}
:global(div.TrimmedProfileContainer) {
padding: 2px;
}
:global(div.TrimmedProfileContainer span.Profile:not(:last-child) div.dcl.avatar-face.tiny.inline) {
margin-right: -8px;
}

View File

@ -1,4 +1,4 @@
import React, { useCallback } from 'react'
import React, { useCallback, useMemo } from 'react'
import classNames from 'classnames'
import { t } from 'decentraland-dapps/dist/modules/translation/utils'
import { Button, Header, Profile } from 'decentraland-ui'
@ -6,12 +6,14 @@ import { useLayoutContext } from '../../../../hooks/useLayoutContext'
import type { Props } from './PeoplePanel.types'
import styles from './PeoplePanel.module.css'
const MAX_VISIBLE_PROFILES = 12
/**
* The PeoplePanel component shows all the participants in a list.
*
* @example
* ```tsx
* <PeoplePanel />
* <PeoplePanel isOpen={true} />
* ```
* @public
*/
@ -25,6 +27,14 @@ export const PeoplePanel: React.FC<Props> = ({ profiles, isOpen }: Props) => {
}
}, [layoutContext])
const visibleProfiles = useMemo(() => {
return Object.entries(profiles).slice(0, MAX_VISIBLE_PROFILES)
}, [profiles])
const trimmedProfiles = useMemo(() => {
return Object.entries(profiles).slice(MAX_VISIBLE_PROFILES)
}, [profiles])
return (
<div className={classNames(styles.container, { [styles['open']]: isOpen })}>
<div className={styles.headerContainer}>
@ -37,13 +47,22 @@ export const PeoplePanel: React.FC<Props> = ({ profiles, isOpen }: Props) => {
{t('people_panel.subtitle')}
</Header>
{Object.entries(profiles).map(([address, profile]) =>
{visibleProfiles.map(([address, profile]) =>
address ? (
<div className={`${styles.profile} ProfileContainer`}>
<Profile key={address} address={address} avatar={profile?.avatars[0]} />
</div>
) : null
)}
{visibleProfiles.length === MAX_VISIBLE_PROFILES ? (
<div className={`${styles.profile} TrimmedProfileContainer`}>
{trimmedProfiles.slice(0, 3).map(([address, profile]) => (
<Profile key={address} address={address} avatar={profile?.avatars[0]} imageOnly />
))}
{t('people_panel.more', { name: trimmedProfiles[0][1].avatars[0].name, count: trimmedProfiles.length - 1 })}
</div>
) : null}
</div>
)
}

View File

@ -15,7 +15,7 @@ export const RightPanel = () => {
return (
<div className={classNames(styles.container, { [styles.open]: showChat || showPeoplePanel })}>
<PeoplePanel isOpen={showPeoplePanel} />
<Chat style={{ display: showChat ? 'flex' : 'none' }} />
<Chat isOpen={showChat} />
</div>
)
}

View File

@ -1,7 +1,14 @@
.VideoConferenceInnerContainer {
padding: 0px 60px;
}
.LayoutWrapper {
flex-direction: row;
padding: 8px 0px;
}
.GridLayout {
flex: 1;
padding: 0;
margin-right: 12px;
}

View File

@ -1,5 +1,6 @@
import React, { useRef, useEffect } from 'react'
import { isEqualTrackRef, isTrackReference, log, isWeb } from '@livekit/components-core'
import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
import {
CarouselView,
ConnectionStateToast,
@ -18,7 +19,6 @@ import { ControlBar } from '../ControlBar'
import ParticipantTile from '../ParticipantTile'
import RightPanel from '../RightPanel'
import type { VideoConferenceProps } from './VideoConference.types'
import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
import styles from './VideoConference.module.css'
/**
@ -75,7 +75,7 @@ export function VideoConference(props: VideoConferenceProps) {
<div className="lk-video-conference" {...props}>
{isWeb() && (
<LayoutContextProvider value={layoutContext}>
<div className="lk-video-conference-inner">
<div className={`${styles.VideoConferenceInnerContainer} lk-video-conference-inner`}>
{!focusTrack ? (
<div className={classNames('lk-grid-layout-wrapper', styles.LayoutWrapper)}>
<GridLayout tracks={tracks} className={styles.GridLayout}>

19
src/hooks/useChat.ts Normal file
View File

@ -0,0 +1,19 @@
import { useEffect, useState } from 'react'
import { useRoomContext } from '@livekit/components-react'
import { setupChat, useObservableState } from '../utils/chat'
/** @public */
export function useChat() {
const room = useRoomContext()
const [setup, setSetup] = useState<ReturnType<typeof setupChat>>()
const isSending = useObservableState(setup?.isSendingObservable, false)
const chatMessages = useObservableState(setup?.messageObservable, [])
useEffect(() => {
const setupChatReturn = setupChat(room)
setSetup(setupChatReturn)
return setupChatReturn.destroy
}, [room])
return { send: setup?.send, chatMessages, isSending }
}

View File

@ -1,7 +1,7 @@
import React, { useContext, useReducer } from 'react'
import { WIDGET_DEFAULT_STATE, PIN_DEFAULT_STATE } from '@livekit/components-core'
import { LayoutContext } from '@livekit/components-react'
import type { WidgetState as LivekitWidgetState, PinState, TrackReference } from '@livekit/components-core'
import { LayoutContext } from '@livekit/components-react'
export type PinAction =
| {

View File

@ -137,3 +137,7 @@ code {
margin-right: unset;
}
}
[data-lk-theme='default'] {
--lk-control-bar-height: 44px;
}

View File

@ -9,6 +9,13 @@
},
"people_panel": {
"title": "People",
"subtitle": "People in this meeting"
"subtitle": "People in this meeting",
"more": "{name} and {count} people more."
},
"chat_panel": {
"title": "Chat"
},
"chat_entry": {
"you": "You"
}
}

View File

@ -9,6 +9,13 @@
},
"people_panel": {
"title": "Personas",
"subtitle": "Personas en esta reunión"
"subtitle": "Personas en esta reunión",
"more": "{name} y {count} personas más."
},
"chat_panel": {
"title": "Chat"
},
"chat_entry": {
"you": "Tu"
}
}

View File

@ -9,6 +9,13 @@
},
"people_panel": {
"title": "人们",
"subtitle": "这次会议的人"
"subtitle": "这次会议的人",
"more": "{name} 和另外 {count} 人。"
},
"chat_panel": {
"title": "聊天"
},
"chat_entry": {
"you": "你"
}
}

View File

@ -1,7 +1,7 @@
import React, { useEffect, useState, isValidElement, cloneElement } from 'react'
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'
@ -10,8 +10,8 @@ import { Packet } from '@dcl/protocol/out-js/decentraland/kernel/comms/rfc4/comm
* @internal
*/
export function useObservableState<T>(observable: Observable<T> | undefined, startWith: T) {
const [state, setState] = React.useState<T>(startWith)
React.useEffect(() => {
const [state, setState] = useState<T>(startWith)
useEffect(() => {
// observable state doesn't run in SSR
if (typeof window === 'undefined' || !observable) return
const subscription = observable.subscribe(setState)
@ -24,8 +24,8 @@ export function cloneSingleChild(children: React.ReactNode | React.ReactNode[],
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 })
if (isValidElement(child) && React.Children.only(children)) {
return cloneElement(child, { ...props, key })
}
return child
})