Finish user avatars
This commit is contained in:
parent
e76a805c8f
commit
1ab7d27ba9
9 changed files with 157 additions and 30 deletions
|
@ -56,4 +56,5 @@
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
border-radius: 90px;
|
border-radius: 90px;
|
||||||
|
font-size: 48px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
4
src/icons/Edit.svg
Normal 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 |
78
src/input/AvatarInputField.jsx
Normal file
78
src/input/AvatarInputField.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
41
src/input/AvatarInputField.module.css
Normal file
41
src/input/AvatarInputField.module.css
Normal 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;
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
.avatarFieldRow {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue