Finish user avatars

This commit is contained in:
Robert Long 2022-02-18 16:02:27 -08:00
parent e76a805c8f
commit 1ab7d27ba9
9 changed files with 157 additions and 30 deletions

View file

@ -56,4 +56,5 @@
width: 90px; width: 90px;
height: 90px; height: 90px;
border-radius: 90px; border-radius: 90px;
font-size: 48px;
} }

View file

@ -43,14 +43,7 @@ export function UserMenuContainer({ preventNavigation }) {
displayName || (userName ? userName.replace("@", "") : undefined) displayName || (userName ? userName.replace("@", "") : undefined)
} }
/> />
{modalState.isOpen && ( {modalState.isOpen && <ProfileModal client={client} {...modalProps} />}
<ProfileModal
client={client}
isAuthenticated={isAuthenticated}
isPasswordlessUser={isPasswordlessUser}
{...modalProps}
/>
)}
</> </>
); );
} }

4
src/icons/Edit.svg Normal file
View file

@ -0,0 +1,4 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.64856 7.35501C2.65473 7.31601 2.67231 7.27972 2.69908 7.25069L8.40377 1.06442C8.47865 0.983217 8.60518 0.978093 8.68638 1.05297L9.8626 2.13763C9.9438 2.21251 9.94893 2.33904 9.87405 2.42024L4.16936 8.60651C4.1426 8.63554 4.10783 8.656 4.06946 8.6653L2.66781 9.00511C2.52911 9.03873 2.40084 8.92044 2.42315 8.77948L2.64856 7.35501Z" fill="white"/>
<path d="M1.75 9.44346C1.33579 9.44346 1 9.77925 1 10.1935C1 10.6077 1.33579 10.9435 1.75 10.9435L10.75 10.9435C11.1642 10.9435 11.5 10.6077 11.5 10.1935C11.5 9.77925 11.1642 9.44346 10.75 9.44346L1.75 9.44346Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 689 B

View file

@ -0,0 +1,78 @@
import { useObjectRef } from "@react-aria/utils";
import React, { useEffect } from "react";
import { useCallback } from "react";
import { useState } from "react";
import { forwardRef } from "react";
import { Avatar } from "../Avatar";
import { Button } from "../button";
import classNames from "classnames";
import { ReactComponent as EditIcon } from "../icons/Edit.svg";
import styles from "./AvatarInputField.module.css";
export const AvatarInputField = forwardRef(
(
{ id, label, className, avatarUrl, displayName, onRemoveAvatar, ...rest },
ref
) => {
const [removed, setRemoved] = useState(false);
const [objUrl, setObjUrl] = useState(null);
const fileInputRef = useObjectRef(ref);
useEffect(() => {
const onChange = (e) => {
if (e.target.files.length > 0) {
setObjUrl(URL.createObjectURL(e.target.files[0]));
setRemoved(false);
} else {
setObjUrl(null);
}
};
fileInputRef.current.addEventListener("change", onChange);
return () => {
if (fileInputRef.current) {
fileInputRef.current.removeEventListener("change", onChange);
}
};
});
const onPressRemoveAvatar = useCallback(() => {
setRemoved(true);
onRemoveAvatar();
}, [onRemoveAvatar]);
return (
<div className={classNames(styles.avatarInputField, className)}>
<div className={styles.avatarContainer}>
<Avatar
size="xl"
src={removed ? null : objUrl || avatarUrl}
fallback={displayName.slice(0, 1).toUpperCase()}
/>
<input
id={id}
accept="image/png, image/jpeg"
ref={fileInputRef}
type="file"
className={styles.fileInput}
role="button"
aria-label={label}
{...rest}
/>
<label htmlFor={id} className={styles.fileInputButton}>
<EditIcon />
</label>
</div>
<Button
className={styles.removeButton}
variant="icon"
onPress={onPressRemoveAvatar}
>
Remove
</Button>
</div>
);
}
);

View file

@ -0,0 +1,41 @@
.avatarInputField {
display: flex;
flex-direction: column;
justify-content: center;
}
.avatarContainer {
position: relative;
margin-bottom: 8px;
}
.fileInput {
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
}
.fileInput:focus + .fileInputButton {
outline: auto;
}
.fileInputButton {
position: absolute;
bottom: 11px;
right: -4px;
background-color: var(--bgColor4);
width: 20px;
height: 20px;
border-radius: 10px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.removeButton {
color: #0dbd8b;
}

View file

@ -3,22 +3,25 @@ import { Button } from "../button";
import { useProfile } from "./useProfile"; import { useProfile } from "./useProfile";
import { FieldRow, InputField, ErrorMessage } from "../input/Input"; import { FieldRow, InputField, ErrorMessage } from "../input/Input";
import { Modal, ModalContent } from "../Modal"; import { Modal, ModalContent } from "../Modal";
import { AvatarInputField } from "../input/AvatarInputField";
import styles from "./ProfileModal.module.css";
export function ProfileModal({ export function ProfileModal({ client, ...rest }) {
client,
isAuthenticated,
isPasswordlessUser,
...rest
}) {
const { onClose } = rest; const { onClose } = rest;
const { const {
success, success,
error, error,
loading, loading,
displayName: initialDisplayName, displayName: initialDisplayName,
avatarUrl,
saveProfile, saveProfile,
} = useProfile(client); } = useProfile(client);
const [displayName, setDisplayName] = useState(initialDisplayName || ""); const [displayName, setDisplayName] = useState(initialDisplayName || "");
const [removeAvatar, setRemoveAvatar] = useState(false);
const onRemoveAvatar = useCallback(() => {
setRemoveAvatar(true);
}, []);
const onChangeDisplayName = useCallback( const onChangeDisplayName = useCallback(
(e) => { (e) => {
@ -37,9 +40,10 @@ export function ProfileModal({
saveProfile({ saveProfile({
displayName, displayName,
avatar: avatar && avatar.size > 0 ? avatar : undefined, avatar: avatar && avatar.size > 0 ? avatar : undefined,
removeAvatar: removeAvatar && (!avatar || avatar.size === 0),
}); });
}, },
[saveProfile] [saveProfile, removeAvatar]
); );
useEffect(() => { useEffect(() => {
@ -52,6 +56,16 @@ export function ProfileModal({
<Modal title="Profile" isDismissable {...rest}> <Modal title="Profile" isDismissable {...rest}>
<ModalContent> <ModalContent>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<FieldRow className={styles.avatarFieldRow}>
<AvatarInputField
id="avatar"
name="avatar"
label="Avatar"
avatarUrl={avatarUrl}
displayName={displayName}
onRemoveAvatar={onRemoveAvatar}
/>
</FieldRow>
<FieldRow> <FieldRow>
<InputField <InputField
id="userId" id="userId"
@ -75,16 +89,6 @@ export function ProfileModal({
onChange={onChangeDisplayName} onChange={onChangeDisplayName}
/> />
</FieldRow> </FieldRow>
{isAuthenticated && (
<FieldRow>
<InputField
type="file"
id="avatar"
name="avatar"
label="Avatar"
/>
</FieldRow>
)}
{error && ( {error && (
<FieldRow> <FieldRow>
<ErrorMessage>{error.message}</ErrorMessage> <ErrorMessage>{error.message}</ErrorMessage>

View file

@ -0,0 +1,3 @@
.avatarFieldRow {
justify-content: center;
}

View file

@ -44,7 +44,7 @@ export function useProfile(client) {
}, [client]); }, [client]);
const saveProfile = useCallback( const saveProfile = useCallback(
async ({ displayName, avatar }) => { async ({ displayName, avatar, removeAvatar }) => {
if (client) { if (client) {
setState((prev) => ({ setState((prev) => ({
...prev, ...prev,
@ -58,7 +58,9 @@ export function useProfile(client) {
let mxcAvatarUrl; let mxcAvatarUrl;
if (avatar) { if (removeAvatar) {
await client.setAvatarUrl("");
} else if (avatar) {
mxcAvatarUrl = await client.uploadContent(avatar); mxcAvatarUrl = await client.uploadContent(avatar);
await client.setAvatarUrl(mxcAvatarUrl); await client.setAvatarUrl(mxcAvatarUrl);
} }
@ -66,7 +68,9 @@ export function useProfile(client) {
setState((prev) => ({ setState((prev) => ({
...prev, ...prev,
displayName, displayName,
avatarUrl: mxcAvatarUrl avatarUrl: removeAvatar
? null
: mxcAvatarUrl
? getAvatarUrl(client, mxcAvatarUrl) ? getAvatarUrl(client, mxcAvatarUrl)
: prev.avatarUrl, : prev.avatarUrl,
loading: false, loading: false,

View file

@ -10,7 +10,6 @@ import { OverflowMenu } from "./OverflowMenu";
import { UserMenuContainer } from "../UserMenuContainer"; import { UserMenuContainer } from "../UserMenuContainer";
import { Body, Link } from "../typography/Typography"; import { Body, Link } from "../typography/Typography";
import { Avatar } from "../Avatar"; import { Avatar } from "../Avatar";
import { getAvatarUrl } from "../matrix-utils";
import { useProfile } from "../profile/useProfile"; import { useProfile } from "../profile/useProfile";
import useMeasure from "react-use-measure"; import useMeasure from "react-use-measure";
import { ResizeObserver } from "@juggle/resize-observer"; import { ResizeObserver } from "@juggle/resize-observer";
@ -86,7 +85,7 @@ export function LobbyView({
borderRadius: avatarSize, borderRadius: avatarSize,
fontSize: Math.round(avatarSize / 2), fontSize: Math.round(avatarSize / 2),
}} }}
src={avatarUrl && getAvatarUrl(client, avatarUrl, 96)} src={avatarUrl}
fallback={displayName.slice(0, 1).toUpperCase()} fallback={displayName.slice(0, 1).toUpperCase()}
/> />
</div> </div>