diff --git a/example/local-only-multi-user-3d-web-experience/client/src/LocalAvatarClient.ts b/example/local-only-multi-user-3d-web-experience/client/src/LocalAvatarClient.ts index 90561192..7159999f 100644 --- a/example/local-only-multi-user-3d-web-experience/client/src/LocalAvatarClient.ts +++ b/example/local-only-multi-user-3d-web-experience/client/src/LocalAvatarClient.ts @@ -46,6 +46,7 @@ const characterDescription: CharacterDescription = { export class LocalAvatarClient { public element: HTMLDivElement; + private canvasHolder: HTMLDivElement; private readonly scene = new Scene(); private readonly audioListener = new AudioListener(); @@ -83,8 +84,14 @@ export class LocalAvatarClient { } }); + this.canvasHolder = document.createElement("div"); + this.canvasHolder.style.position = "absolute"; + this.canvasHolder.style.width = "100%"; + this.canvasHolder.style.height = "100%"; + this.element.appendChild(this.canvasHolder); + this.cameraManager = new CameraManager( - this.element, + this.canvasHolder, this.collisionsManager, Math.PI / 2, Math.PI / 2, diff --git a/example/multi-user-3d-web-experience/client/src/index.ts b/example/multi-user-3d-web-experience/client/src/index.ts index e305254e..796a135e 100644 --- a/example/multi-user-3d-web-experience/client/src/index.ts +++ b/example/multi-user-3d-web-experience/client/src/index.ts @@ -27,6 +27,9 @@ const app = new Networked3dWebExperienceClient(holder, { skyboxHdrJpgUrl: hdrJpgUrl, mmlDocuments: [{ url: `${protocol}//${host}/mml-documents/example-mml.html` }], environmentConfiguration: {}, + avatarConfiguration: { + availableAvatars: [], + }, }); app.update(); diff --git a/example/multi-user-3d-web-experience/server/src/BasicUserAuthenticator.ts b/example/multi-user-3d-web-experience/server/src/BasicUserAuthenticator.ts index 5cdc86eb..c11dd7e7 100644 --- a/example/multi-user-3d-web-experience/server/src/BasicUserAuthenticator.ts +++ b/example/multi-user-3d-web-experience/server/src/BasicUserAuthenticator.ts @@ -93,12 +93,28 @@ export class BasicUserAuthenticator { } public onClientUserIdentityUpdate(clientId: number, msg: UserIdentity): UserData | null { - // This implementation does not allow updating user data after initial connect. - // To allow updating user data after initial connect, return the UserData object that reflects the requested change. - // Returning null will not update the user data. - return null; + + const user = this.usersByClientId.get(clientId); + + if (!user) { + console.error(`onClientUserIdentityUpdate - unknown clientId ${clientId}`); + return null; + } + + if (!user.userData) { + console.error(`onClientUserIdentityUpdate - no user data for clientId ${clientId}`); + return null; + } + + const newUserData: UserData = { + username: msg.username ?? user.userData.username, + characterDescription: msg.characterDescription ?? user.userData.characterDescription, + }; + + this.usersByClientId.set(clientId, { ...user, userData: newUserData }); + return newUserData; } public onClientDisconnect(clientId: number) { diff --git a/example/multi-user-3d-web-experience/server/src/index.ts b/example/multi-user-3d-web-experience/server/src/index.ts index 67c3d2d3..616147f7 100644 --- a/example/multi-user-3d-web-experience/server/src/index.ts +++ b/example/multi-user-3d-web-experience/server/src/index.ts @@ -32,6 +32,7 @@ const characterDescription: CharacterDescription = { // // `, }; + const userAuthenticator = new BasicUserAuthenticator(characterDescription, { /* This option allows sessions that are reconnecting from a previous run of the server to connect even if the present a diff --git a/package-lock.json b/package-lock.json index ded0939e..9afca83e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1239,9 +1239,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz", - "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -2227,6 +2227,10 @@ "resolved": "packages/3d-web-avatar-editor-ui", "link": true }, + "node_modules/@mml-io/3d-web-avatar-selection-ui": { + "resolved": "packages/3d-web-avatar-selection-ui", + "link": true + }, "node_modules/@mml-io/3d-web-client-core": { "resolved": "packages/3d-web-client-core", "link": true @@ -2370,9 +2374,9 @@ } }, "node_modules/@mml-io/observable-dom/node_modules/https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "dependencies": { "agent-base": "^7.0.2", "debug": "4" @@ -2597,9 +2601,9 @@ } }, "node_modules/@npmcli/agent/node_modules/https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "dev": true, "dependencies": { "agent-base": "^7.0.2", @@ -2610,9 +2614,9 @@ } }, "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.3.0.tgz", + "integrity": "sha512-CQl19J/g+Hbjbv4Y3mFNNXFEL/5t/KCg8POCuUqd4rMKjGG+j1ybER83hxV58zL+dFI1PTkt3GNFSHRt+d8qEQ==", "dev": true, "engines": { "node": "14 || >=16.14" @@ -2659,9 +2663,9 @@ } }, "node_modules/@npmcli/git/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.3.0.tgz", + "integrity": "sha512-CQl19J/g+Hbjbv4Y3mFNNXFEL/5t/KCg8POCuUqd4rMKjGG+j1ybER83hxV58zL+dFI1PTkt3GNFSHRt+d8qEQ==", "dev": true, "engines": { "node": "14 || >=16.14" @@ -3863,9 +3867,9 @@ "dev": true }, "node_modules/@types/webxr": { - "version": "0.5.18", - "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.18.tgz", - "integrity": "sha512-EM8P5wtYMPUlFbDgKfNoPc5PyhgZvwXz0Wf9gYBfJOpTI8AtPzoLJlvw/D9TmIW/LPLTmwMxWI0axBs3hjLSLg==", + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.19.tgz", + "integrity": "sha512-4hxA+NwohSgImdTSlPXEqDqqFktNgmTXQ05ff1uWam05tNGroCMp4G+4XVl6qWm1p7GQ/9oD41kAYsSssF6Mzw==", "dev": true }, "node_modules/@types/ws": { @@ -5041,9 +5045,9 @@ } }, "node_modules/cacache/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.3.0.tgz", + "integrity": "sha512-CQl19J/g+Hbjbv4Y3mFNNXFEL/5t/KCg8POCuUqd4rMKjGG+j1ybER83hxV58zL+dFI1PTkt3GNFSHRt+d8qEQ==", "dev": true, "engines": { "node": "14 || >=16.14" @@ -5115,9 +5119,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001638", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001638.tgz", - "integrity": "sha512-5SuJUJ7cZnhPpeLHaH0c/HPAnAHZvS6ElWyHK9GSIbVOQABLzowiI2pjmpvZ1WEbkyz46iFd4UXlOHR5SqgfMQ==", + "version": "1.0.30001639", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001639.tgz", + "integrity": "sha512-eFHflNTBIlFwP2AIKaYuBQN/apnUoKNhBdza8ZnW/h2di4LCZ4xFqYlxUxo+LQ76KFI1PGcC1QDxMbxTZpSCAg==", "funding": [ { "type": "opencollective", @@ -6190,9 +6194,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.812", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.812.tgz", - "integrity": "sha512-7L8fC2Ey/b6SePDFKR2zHAy4mbdp1/38Yk5TsARO66W3hC5KEaeKMMHoxwtuH+jcu2AYLSn9QX04i95t6Fl1Hg==" + "version": "1.4.815", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.815.tgz", + "integrity": "sha512-OvpTT2ItpOXJL7IGcYakRjHCt8L5GrrN/wHCQsRB4PQa1X9fe+X9oen245mIId7s14xvArCGSTIq644yPUKKLg==" }, "node_modules/emittery": { "version": "0.13.1", @@ -9155,9 +9159,9 @@ } }, "node_modules/istanbul-lib-instrument": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz", - "integrity": "sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", "dev": true, "dependencies": { "@babel/core": "^7.23.9", @@ -11963,9 +11967,9 @@ } }, "node_modules/npm-pick-manifest/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.3.0.tgz", + "integrity": "sha512-CQl19J/g+Hbjbv4Y3mFNNXFEL/5t/KCg8POCuUqd4rMKjGG+j1ybER83hxV58zL+dFI1PTkt3GNFSHRt+d8qEQ==", "dev": true, "engines": { "node": "14 || >=16.14" @@ -12931,9 +12935,9 @@ } }, "node_modules/pacote/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.3.0.tgz", + "integrity": "sha512-CQl19J/g+Hbjbv4Y3mFNNXFEL/5t/KCg8POCuUqd4rMKjGG+j1ybER83hxV58zL+dFI1PTkt3GNFSHRt+d8qEQ==", "dev": true, "engines": { "node": "14 || >=16.14" @@ -13199,9 +13203,9 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.3.0.tgz", + "integrity": "sha512-CQl19J/g+Hbjbv4Y3mFNNXFEL/5t/KCg8POCuUqd4rMKjGG+j1ybER83hxV58zL+dFI1PTkt3GNFSHRt+d8qEQ==", "dev": true, "engines": { "node": "14 || >=16.14" @@ -14763,14 +14767,14 @@ } }, "node_modules/socks-proxy-agent": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.3.tgz", - "integrity": "sha512-VNegTZKhuGq5vSD6XNKlbqWhyt/40CgoEw8XxD6dhnm8Jq9IEa3nIa4HwnM8XOqU0CdB0BwWVXusqiFXfHB3+A==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", "dev": true, "dependencies": { "agent-base": "^7.1.1", "debug": "^4.3.4", - "socks": "^2.7.1" + "socks": "^2.8.3" }, "engines": { "node": ">= 14" @@ -17060,6 +17064,26 @@ "esbuild-css-modules-plugin": "3.1.0" } }, + "packages/3d-web-avatar-selection-ui": { + "name": "@mml-io/3d-web-avatar-selection-ui", + "version": "0.17.0", + "dependencies": { + "express": "4.19.2", + "express-ws": "5.0.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "ws": "^8.16.0" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/express-ws": "^3.0.4", + "@types/node": "^20.12.7", + "@types/react": "^18.2.79", + "@types/react-dom": "^18.2.25", + "@types/ws": "^8.5.10", + "esbuild-css-modules-plugin": "3.1.0" + } + }, "packages/3d-web-client-core": { "name": "@mml-io/3d-web-client-core", "version": "0.17.0", @@ -17085,6 +17109,7 @@ "name": "@mml-io/3d-web-experience-client", "version": "0.17.0", "dependencies": { + "@mml-io/3d-web-avatar-selection-ui": "^0.17.0", "@mml-io/3d-web-client-core": "^0.17.0", "@mml-io/3d-web-text-chat": "^0.17.0", "@mml-io/3d-web-user-networking": "^0.17.0", diff --git a/packages/3d-web-avatar-selection-ui/build.ts b/packages/3d-web-avatar-selection-ui/build.ts new file mode 100644 index 00000000..6e3aa3be --- /dev/null +++ b/packages/3d-web-avatar-selection-ui/build.ts @@ -0,0 +1,15 @@ +import cssModulesPlugin from "esbuild-css-modules-plugin"; + +import { handleLibraryBuild } from "../../utils/build-library"; + +handleLibraryBuild({ + plugins: [ + cssModulesPlugin({ + inject: true, + emitDeclarationFile: true, + }), + ], + loader: { + ".svg": "text", + }, +}); diff --git a/packages/3d-web-avatar-selection-ui/package.json b/packages/3d-web-avatar-selection-ui/package.json new file mode 100644 index 00000000..24d92512 --- /dev/null +++ b/packages/3d-web-avatar-selection-ui/package.json @@ -0,0 +1,36 @@ +{ + "name": "@mml-io/3d-web-avatar-selection-ui", + "version": "0.17.0", + "publishConfig": { + "access": "public" + }, + "main": "./build/index.js", + "types": "./build/index.d.ts", + "type": "module", + "files": [ + "/build" + ], + "scripts": { + "build": "tsx ./build.ts --build", + "iterate": "tsx ./build.ts --watch", + "type-check": "tsc --noEmit", + "lint": "eslint \"./{src,test}/**/*.{js,jsx,ts,tsx}\" --max-warnings 0", + "lint-fix": "eslint \"./{src,test}/**/*.{js,jsx,ts,tsx}\" --fix" + }, + "dependencies": { + "express": "4.19.2", + "express-ws": "5.0.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "ws": "^8.16.0" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/express-ws": "^3.0.4", + "@types/node": "^20.12.7", + "@types/react": "^18.2.79", + "@types/react-dom": "^18.2.25", + "@types/ws": "^8.5.10", + "esbuild-css-modules-plugin": "3.1.0" + } +} diff --git a/packages/3d-web-avatar-selection-ui/src/avatar-selection-ui/AvatarSelectionUI.tsx b/packages/3d-web-avatar-selection-ui/src/avatar-selection-ui/AvatarSelectionUI.tsx new file mode 100644 index 00000000..413b7b85 --- /dev/null +++ b/packages/3d-web-avatar-selection-ui/src/avatar-selection-ui/AvatarSelectionUI.tsx @@ -0,0 +1,52 @@ +import * as React from "react"; +import { createRef, forwardRef } from "react"; +import { flushSync } from "react-dom"; +import { createRoot, Root } from "react-dom/client"; + +import { AvatarType } from "./AvatarType"; +import { AvatarSelectionUIComponent } from "./components/AvatarPanel/AvatarSectionUIComponent"; + +const ForwardedAvatarSelectionUIComponent = forwardRef(AvatarSelectionUIComponent); + +export type CustomAvatar = AvatarType & { + isCustomAvatar?: boolean; +}; + +export type AvatarSelectionUIProps = { + holderElement: HTMLElement; + clientId: number; + visibleByDefault?: boolean; + availableAvatars: Array; + sendMessageToServerMethod: (avatar: CustomAvatar) => void; + enableCustomAvatar?: boolean; +}; + +export class AvatarSelectionUI { + private root: Root; + private appRef: React.RefObject = createRef(); + + private wrapper = document.createElement("div"); + + constructor(private config: AvatarSelectionUIProps) { + this.config.holderElement.appendChild(this.wrapper); + this.root = createRoot(this.wrapper); + } + + private onUpdateUserAvatar = (avatar: CustomAvatar) => { + this.config.sendMessageToServerMethod(avatar); + }; + + init() { + flushSync(() => + this.root.render( + , + ), + ); + } +} diff --git a/packages/3d-web-avatar-selection-ui/src/avatar-selection-ui/AvatarType.ts b/packages/3d-web-avatar-selection-ui/src/avatar-selection-ui/AvatarType.ts new file mode 100644 index 00000000..7e4b2b1e --- /dev/null +++ b/packages/3d-web-avatar-selection-ui/src/avatar-selection-ui/AvatarType.ts @@ -0,0 +1,21 @@ +export type AvatarType = { + thumbnailUrl?: string; + name?: string; + isDefaultAvatar?: boolean; +} & ( + | { + meshFileUrl: string; + mmlCharacterString?: null; + mmlCharacterUrl?: null; + } + | { + meshFileUrl?: null; + mmlCharacterString: string; + mmlCharacterUrl?: null; + } + | { + meshFileUrl?: null; + mmlCharacterString?: null; + mmlCharacterUrl: string; + } +); diff --git a/packages/3d-web-avatar-selection-ui/src/avatar-selection-ui/components/AvatarPanel/AvatarSectionUIComponent.tsx b/packages/3d-web-avatar-selection-ui/src/avatar-selection-ui/components/AvatarPanel/AvatarSectionUIComponent.tsx new file mode 100644 index 00000000..cd4cfd00 --- /dev/null +++ b/packages/3d-web-avatar-selection-ui/src/avatar-selection-ui/components/AvatarPanel/AvatarSectionUIComponent.tsx @@ -0,0 +1,212 @@ +import React, { + ForwardRefRenderFunction, + KeyboardEvent, + MouseEvent, + useRef, + useState, +} from "react"; + +import { CustomAvatar } from "../../AvatarSelectionUI"; +import { AvatarType } from "../../AvatarType"; +import AvatarIcon from "../../icons/Avatar.svg"; + +import styles from "./AvatarSelectionUIComponent.module.css"; + +type AvatarSelectionUIProps = { + onUpdateUserAvatar: (avatar: AvatarType) => void; + visibleByDefault?: boolean; + availableAvatars: AvatarType[]; + enableCustomAvatar?: boolean; +}; + +enum CustomAvatarType { + meshFileUrl, + mmlUrl, + mml, +} + +function SelectedPill() { + return Selected; +} + +export const AvatarSelectionUIComponent: ForwardRefRenderFunction = ( + props: AvatarSelectionUIProps, +) => { + const visibleByDefault: boolean = props.visibleByDefault ?? false; + const [isVisible, setIsVisible] = useState(visibleByDefault); + const [selectedAvatar, setSelectedAvatar] = useState(undefined); + const [customAvatarType, setCustomAvatarType] = useState( + CustomAvatarType.mmlUrl, + ); + const [customAvatarValue, setCustomAvatarValue] = useState(""); + const inputRef = useRef(null); + const textareaRef = useRef(null); + + const handleRootClick = (e: MouseEvent) => { + e.stopPropagation(); + }; + + const selectAvatar = (avatar: CustomAvatar) => { + setSelectedAvatar(avatar); + props.onUpdateUserAvatar(avatar); + }; + + const handleInputChange = (e: React.ChangeEvent) => { + setCustomAvatarValue(e.target.value); + }; + + const addCustomAvatar = () => { + if (!customAvatarValue) { + return; + } + + const newSelectedAvatar = { + mmlCharacterString: customAvatarType === CustomAvatarType.mml ? customAvatarValue : undefined, + mmlCharacterUrl: customAvatarType === CustomAvatarType.mmlUrl ? customAvatarValue : undefined, + meshFileUrl: + customAvatarType === CustomAvatarType.meshFileUrl ? customAvatarValue : undefined, + isCustomAvatar: true, + } as CustomAvatar; + + setSelectedAvatar(newSelectedAvatar); + props.onUpdateUserAvatar(newSelectedAvatar); + }; + + const handleKeyPress = (e: KeyboardEvent) => { + e.stopPropagation(); + }; + + const handleTypeSwitch = (type: CustomAvatarType) => { + setCustomAvatarType(type); + setCustomAvatarValue(""); + }; + + const getPlaceholderByType = (type: CustomAvatarType) => { + switch (type) { + case CustomAvatarType.meshFileUrl: + return "https://.../avatar.glb"; + case CustomAvatarType.mmlUrl: + return "https://.../avatar.html"; + case CustomAvatarType.mml: + return '\n +
+ {!isVisible && ( +
setIsVisible(true)}> + +
+ )} + {isVisible && ( + + )} +
+ {isVisible && ( +
+ {!!props.availableAvatars.length && ( +
+
+

Choose your avatar

+
+
+ {props.availableAvatars.map((avatar, index) => { + const isSelected = + !selectedAvatar?.isCustomAvatar && + ((selectedAvatar?.meshFileUrl && + selectedAvatar?.meshFileUrl === avatar.meshFileUrl) || + (selectedAvatar?.mmlCharacterUrl && + selectedAvatar?.mmlCharacterUrl === avatar.mmlCharacterUrl) || + (selectedAvatar?.mmlCharacterString && + selectedAvatar?.mmlCharacterString === avatar.mmlCharacterString)); + + return ( +
selectAvatar(avatar)} + > +
+ {isSelected && } + {avatar.thumbnailUrl ? ( + {avatar.name} + ) : ( +
No Image Available
+ )} +

{avatar.name}

+ {avatar.name} +
+
+ ); + })} +
+
+ )} + {props.enableCustomAvatar && ( +
+ {!!props.availableAvatars.length &&
} +

Custom Avatar Section

+ handleTypeSwitch(CustomAvatarType.mmlUrl)} + defaultChecked={customAvatarType === CustomAvatarType.mmlUrl} + checked={customAvatarType === CustomAvatarType.mmlUrl} + /> + + handleTypeSwitch(CustomAvatarType.mml)} + defaultChecked={customAvatarType === CustomAvatarType.mml} + checked={customAvatarType === CustomAvatarType.mml} + /> + + handleTypeSwitch(CustomAvatarType.meshFileUrl)} + defaultChecked={customAvatarType === CustomAvatarType.meshFileUrl} + checked={customAvatarType === CustomAvatarType.meshFileUrl} + /> + + {selectedAvatar?.isCustomAvatar && } +
+ {customAvatarType === CustomAvatarType.mml ? ( +