Skip to content

Commit

Permalink
Chat bubbles and custom display name
Browse files Browse the repository at this point in the history
  • Loading branch information
MarcusLongmuir committed Jan 30, 2025
1 parent 8dada83 commit 391f1af
Show file tree
Hide file tree
Showing 25 changed files with 864 additions and 539 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@
"@mml-io/3d-web-text-chat": "^0.20.0",
"@mml-io/3d-web-user-networking": "^0.20.0",
"@mml-io/3d-web-voice-chat": "^0.20.0",
"@mml-io/mml-web": "0.19.0",
"@mml-io/mml-web-runner": "0.19.0",
"@mml-io/mml-web-threejs-standalone": "0.19.0",
"@mml-io/networked-dom-document": "0.19.0",
"@mml-io/mml-web": "0.19.1",
"@mml-io/mml-web-runner": "0.19.1",
"@mml-io/mml-web-threejs-standalone": "0.19.1",
"@mml-io/networked-dom-document": "0.19.1",
"three": "0.163.0"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"dependencies": {
"@example/local-only-multi-user-3d-web-experience-client": "^0.20.0",
"@mml-io/3d-web-experience-server": "^0.20.0",
"@mml-io/networked-dom-server": "0.19.0",
"@mml-io/networked-dom-server": "0.19.1",
"chokidar": "^3.6.0",
"cors": "^2.8.5",
"express": "4.19.2",
Expand Down
2 changes: 1 addition & 1 deletion example/multi-user-3d-web-experience/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"@mml-io/3d-web-experience-server": "^0.20.0",
"@mml-io/3d-web-text-chat": "^0.20.0",
"@mml-io/3d-web-user-networking": "^0.20.0",
"@mml-io/networked-dom-server": "0.19.0",
"@mml-io/networked-dom-server": "0.19.1",
"chokidar": "^3.6.0",
"express": "4.19.2",
"express-ws": "5.0.2",
Expand Down
361 changes: 189 additions & 172 deletions package-lock.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@ import { AvatarSelectionUIComponent } from "./components/AvatarPanel/AvatarSecti

const ForwardedAvatarSelectionUIComponent = forwardRef(AvatarSelectionUIComponent);

export type CustomAvatar = AvatarType & {
isCustomAvatar?: boolean;
};

export type AvatarSelectionUIProps = {
holderElement: HTMLElement;
visibleByDefault?: boolean;
sendMessageToServerMethod: (avatar: CustomAvatar) => void;
displayName: string;
characterDescription: AvatarType;
allowCustomDisplayName: boolean;
sendIdentityUpdateToServer: (displayName: string, characterDescription: AvatarType) => void;
} & AvatarConfiguration;

export class AvatarSelectionUI {
Expand All @@ -29,8 +28,14 @@ export class AvatarSelectionUI {
this.root = createRoot(this.wrapper);
}

private onUpdateUserAvatar = (avatar: CustomAvatar) => {
this.config.sendMessageToServerMethod(avatar);
private onUpdateUserAvatar = (avatar: AvatarType) => {
this.config.characterDescription = avatar;
this.config.sendIdentityUpdateToServer(this.config.displayName, avatar);
};

private onUpdateDisplayName = (displayName: string) => {
this.config.displayName = displayName;
this.config.sendIdentityUpdateToServer(displayName, this.config.characterDescription);
};

public updateAvatarConfig(avatarConfig: AvatarConfiguration) {
Expand All @@ -41,15 +46,27 @@ export class AvatarSelectionUI {
this.init();
}

public updateAllowCustomDisplayName(allowCustomDisplayName: boolean) {
this.config = {
...this.config,
allowCustomDisplayName,
};
this.init();
}

init() {
flushSync(() =>
this.root.render(
<ForwardedAvatarSelectionUIComponent
ref={this.appRef}
onUpdateUserAvatar={this.onUpdateUserAvatar}
onUpdateDisplayName={this.onUpdateDisplayName}
visibleByDefault={this.config.visibleByDefault}
displayName={this.config.displayName}
characterDescription={this.config.characterDescription}
availableAvatars={this.config.availableAvatars}
allowCustomAvatars={this.config.allowCustomAvatars}
allowCustomAvatars={this.config.allowCustomAvatars || false}
allowCustomDisplayName={this.config.allowCustomDisplayName || false}
/>,
),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import React, {
useState,
} from "react";

import { CustomAvatar } from "../../AvatarSelectionUI";
import { AvatarType } from "../../AvatarSelectionUI";
import { AvatarType } from "../../AvatarType";
import AvatarIcon from "../../icons/Avatar.svg";

Expand All @@ -16,7 +16,13 @@ type AvatarSelectionUIProps = {
onUpdateUserAvatar: (avatar: AvatarType) => void;
visibleByDefault?: boolean;
availableAvatars: AvatarType[];
allowCustomAvatars?: boolean;

characterDescription: AvatarType;
allowCustomAvatars: boolean;

displayName: string;
allowCustomDisplayName: boolean;
onUpdateDisplayName: (displayNameValue: string) => void;
};

enum CustomAvatarType {
Expand All @@ -34,19 +40,24 @@ export const AvatarSelectionUIComponent: ForwardRefRenderFunction<any, AvatarSel
) => {
const visibleByDefault: boolean = props.visibleByDefault ?? false;
const [isVisible, setIsVisible] = useState<boolean>(visibleByDefault);
const [selectedAvatar, setSelectedAvatar] = useState<CustomAvatar | undefined>(undefined);
const [selectedAvatar, setSelectedAvatar] = useState<AvatarType | undefined>(
props.characterDescription,
);
const [customAvatarType, setCustomAvatarType] = useState<CustomAvatarType>(
CustomAvatarType.mmlUrl,
);
const [customAvatarValue, setCustomAvatarValue] = useState<string>("");
const inputRef = useRef<HTMLInputElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);

const [displayNameValue, setDisplayNameValue] = useState<string>(props.displayName);
const displayNameRef = useRef<HTMLInputElement>(null);

const handleRootClick = (e: MouseEvent) => {
e.stopPropagation();
};

const selectAvatar = (avatar: CustomAvatar) => {
const selectAvatar = (avatar: AvatarType) => {
setSelectedAvatar(avatar);
props.onUpdateUserAvatar(avatar);
};
Expand All @@ -55,18 +66,30 @@ export const AvatarSelectionUIComponent: ForwardRefRenderFunction<any, AvatarSel
setCustomAvatarValue(e.target.value);
};

const handleDisplayNameChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
setDisplayNameValue(e.target.value);
};

const setDisplayName = () => {
if (!displayNameValue) {
return;
}
props.onUpdateDisplayName(displayNameValue);
};

const addCustomAvatar = () => {
if (!customAvatarValue) {
return;
}

const newSelectedAvatar = {
const newSelectedAvatar: AvatarType = {
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);
Expand All @@ -76,6 +99,20 @@ export const AvatarSelectionUIComponent: ForwardRefRenderFunction<any, AvatarSel
e.stopPropagation();
};

const handleAvatarInputKeyPress = (e: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
e.stopPropagation();
if (e.key === "Enter") {
addCustomAvatar();
}
};

const handleDisplayNameKeyPress = (e: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
e.stopPropagation();
if (e.key === "Enter") {
setDisplayName();
}
};

const handleTypeSwitch = (type: CustomAvatarType) => {
setCustomAvatarType(type);
setCustomAvatarValue("");
Expand All @@ -92,10 +129,16 @@ export const AvatarSelectionUIComponent: ForwardRefRenderFunction<any, AvatarSel
}
};

if (!props.availableAvatars.length && !props.allowCustomAvatars) {
if (
!props.availableAvatars.length &&
!props.allowCustomAvatars &&
!props.allowCustomDisplayName
) {
return null;
}

let recognizedAvatar = false;

return (
<>
<div className={styles.menuButton} onClick={handleRootClick}>
Expand All @@ -112,21 +155,45 @@ export const AvatarSelectionUIComponent: ForwardRefRenderFunction<any, AvatarSel
</div>
{isVisible && (
<div className={`${styles.avatarSelectionContainer}`}>
{!!props.availableAvatars.length && (
<div className={styles.avatarSelectionUi}>
<div className={styles.avatarSelectionUiHeader}>
<h2>Choose your avatar</h2>
{props.allowCustomDisplayName && (
<div className={styles.displayNameSection}>
<div className={styles.sectionHeading}>Display Name</div>
<div className={styles.displayNameInputSection}>
<input
ref={displayNameRef}
className={styles.input}
value={displayNameValue}
onKeyDown={handleDisplayNameKeyPress}
onChange={handleDisplayNameChange}
placeholder={"Enter your display name"}
/>
<button
className={styles.setButton}
disabled={!displayNameValue}
type="button"
onClick={setDisplayName}
>
Set
</button>
</div>
<div className={styles.avatarSelectionUiContent}>
</div>
)}
{!!props.availableAvatars.length && (
<div className={styles.avatarSelectionSection}>
<div className={styles.sectionHeading}>Choose your Avatar</div>
<div className={styles.avatarSelectionUi}>
{props.availableAvatars.map((avatar, index) => {
const isSelected =
!selectedAvatar?.isCustomAvatar &&
((selectedAvatar?.meshFileUrl &&
(selectedAvatar?.meshFileUrl &&
selectedAvatar?.meshFileUrl === avatar.meshFileUrl) ||
(selectedAvatar?.mmlCharacterUrl &&
selectedAvatar?.mmlCharacterUrl === avatar.mmlCharacterUrl) ||
(selectedAvatar?.mmlCharacterString &&
selectedAvatar?.mmlCharacterString === avatar.mmlCharacterString));
(selectedAvatar?.mmlCharacterUrl &&
selectedAvatar?.mmlCharacterUrl === avatar.mmlCharacterUrl) ||
(selectedAvatar?.mmlCharacterString &&
selectedAvatar?.mmlCharacterString === avatar.mmlCharacterString);

if (isSelected) {
recognizedAvatar = true;
}

return (
<div
Expand All @@ -137,9 +204,18 @@ export const AvatarSelectionUIComponent: ForwardRefRenderFunction<any, AvatarSel
<div className={styles.avatarSelectionUiAvatarImgContainer}>
{isSelected && <SelectedPill />}
{avatar.thumbnailUrl ? (
<img src={avatar.thumbnailUrl} alt={avatar.name} />
<img
className={styles.avatarSelectionUiAvatarImage}
src={avatar.thumbnailUrl}
alt={avatar.name}
/>
) : (
<div>No Image Available</div>
<div className={styles.avatarSelectionNoImage}>
<img
alt={avatar.name}
src={`data:image/svg+xml;utf8,${encodeURIComponent(AvatarIcon)}`}
/>
</div>
)}
<p>{avatar.name}</p>
<span className={styles.tooltipText}>{avatar.name}</span>
Expand All @@ -152,8 +228,7 @@ export const AvatarSelectionUIComponent: ForwardRefRenderFunction<any, AvatarSel
)}
{props.allowCustomAvatars && (
<div className={styles.customAvatarSection}>
{!!props.availableAvatars.length && <hr />}
<h2>Custom Avatar Section</h2>
<div className={styles.sectionHeading}>Custom Avatar</div>
<input
type="radio"
id="html"
Expand Down Expand Up @@ -181,12 +256,12 @@ export const AvatarSelectionUIComponent: ForwardRefRenderFunction<any, AvatarSel
checked={customAvatarType === CustomAvatarType.meshFileUrl}
/>
<label htmlFor="glb">Mesh URL</label>
{selectedAvatar?.isCustomAvatar && <SelectedPill />}
{!recognizedAvatar && <SelectedPill />}
<div className={styles.customAvatarInputSection}>
{customAvatarType === CustomAvatarType.mml ? (
<textarea
ref={textareaRef}
className={styles.customAvatarInput}
className={styles.input}
value={customAvatarValue}
onChange={handleInputChange}
onKeyDown={handleKeyPress}
Expand All @@ -196,14 +271,19 @@ export const AvatarSelectionUIComponent: ForwardRefRenderFunction<any, AvatarSel
) : (
<input
ref={inputRef}
className={styles.customAvatarInput}
className={styles.input}
value={customAvatarValue}
onKeyDown={handleKeyPress}
onKeyDown={handleAvatarInputKeyPress}
onChange={handleInputChange}
placeholder={getPlaceholderByType(customAvatarType)}
/>
)}
<button disabled={!customAvatarValue} type="button" onClick={addCustomAvatar}>
<button
className={styles.setButton}
disabled={!customAvatarValue}
type="button"
onClick={addCustomAvatar}
>
Set
</button>
</div>
Expand Down
Loading

0 comments on commit 391f1af

Please sign in to comment.