Change avatar to use Compound implementation (#11448)
* Move avatar to new compound implementation * Make space avatars square * Remove reference to the avatar initial CSS class * remove references to mx_BaseAvatar_image * Fixe test suites * Fix accessbility violations * Add ConfirmUserActionDialog test * Fix tests * Add FacePile test * Fix items clipping in members list * Fix user info avatar sizing * Fix tests
This commit is contained in:
parent
e34920133e
commit
09c5e06d12
125 changed files with 936 additions and 1413 deletions
|
@ -19,34 +19,29 @@ limitations under the License.
|
|||
|
||||
import React, { useCallback, useContext, useEffect, useState } from "react";
|
||||
import classNames from "classnames";
|
||||
import { ResizeMethod, ClientEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { ClientEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { Avatar } from "@vector-im/compound-web";
|
||||
|
||||
import * as AvatarLogic from "../../../Avatar";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import { toPx } from "../../../utils/units";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
interface IProps {
|
||||
name?: string; // The name (first initial used as default)
|
||||
idName?: string; // ID for generating hash colours
|
||||
name?: React.ComponentProps<typeof Avatar>["name"]; // The name (first initial used as default)
|
||||
idName?: React.ComponentProps<typeof Avatar>["id"]; // ID for generating hash colours
|
||||
title?: string; // onHover title text
|
||||
url?: string | null; // highest priority of them all, shortcut to set in urls[0]
|
||||
urls?: string[]; // [highest_priority, ... , lowest_priority]
|
||||
width: number;
|
||||
height: number;
|
||||
// XXX: resizeMethod not actually used.
|
||||
resizeMethod?: ResizeMethod;
|
||||
defaultToInitialLetter?: boolean; // true to add default url
|
||||
type?: React.ComponentProps<typeof Avatar>["type"];
|
||||
size: string;
|
||||
onClick?: (ev: ButtonEvent) => void;
|
||||
inputRef?: React.RefObject<HTMLImageElement & HTMLSpanElement>;
|
||||
inputRef?: React.RefObject<HTMLSpanElement>;
|
||||
className?: string;
|
||||
tabIndex?: number;
|
||||
altText?: string;
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
const calculateUrls = (url?: string | null, urls?: string[], lowBandwidth = false): string[] => {
|
||||
|
@ -107,120 +102,47 @@ const BaseAvatar: React.FC<IProps> = (props) => {
|
|||
title,
|
||||
url,
|
||||
urls,
|
||||
width = 40,
|
||||
height = 40,
|
||||
resizeMethod = "crop", // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
defaultToInitialLetter = true,
|
||||
size = "40px",
|
||||
onClick,
|
||||
inputRef,
|
||||
className,
|
||||
type = "round",
|
||||
altText = _t("Avatar"),
|
||||
ariaLabel = _t("Avatar"),
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const [imageUrl, onError] = useImageUrl({ url, urls });
|
||||
|
||||
if (!imageUrl && defaultToInitialLetter && name) {
|
||||
const initialLetter = AvatarLogic.getInitialLetter(name);
|
||||
const textNode = (
|
||||
<span
|
||||
className="mx_BaseAvatar_initial"
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
fontSize: toPx(width * 0.65),
|
||||
width: toPx(width),
|
||||
lineHeight: toPx(height),
|
||||
}}
|
||||
>
|
||||
{initialLetter}
|
||||
</span>
|
||||
);
|
||||
const imgNode = (
|
||||
<img
|
||||
loading="lazy"
|
||||
className="mx_BaseAvatar_image"
|
||||
src={AvatarLogic.defaultAvatarUrlForString(idName || name)}
|
||||
alt=""
|
||||
title={title}
|
||||
onError={onError}
|
||||
style={{
|
||||
width: toPx(width),
|
||||
height: toPx(height),
|
||||
}}
|
||||
aria-hidden="true"
|
||||
data-testid="avatar-img"
|
||||
/>
|
||||
);
|
||||
|
||||
if (onClick) {
|
||||
return (
|
||||
<AccessibleButton
|
||||
aria-label={ariaLabel}
|
||||
aria-live="off"
|
||||
{...otherProps}
|
||||
element="span"
|
||||
className={classNames("mx_BaseAvatar", className)}
|
||||
onClick={onClick}
|
||||
inputRef={inputRef}
|
||||
>
|
||||
{textNode}
|
||||
{imgNode}
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<span
|
||||
className={classNames("mx_BaseAvatar", className)}
|
||||
ref={inputRef}
|
||||
{...otherProps}
|
||||
role="presentation"
|
||||
>
|
||||
{textNode}
|
||||
{imgNode}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
const extraProps: Partial<React.ComponentProps<typeof Avatar>> = {};
|
||||
|
||||
if (onClick) {
|
||||
return (
|
||||
<AccessibleButton
|
||||
className={classNames("mx_BaseAvatar mx_BaseAvatar_image", className)}
|
||||
element="img"
|
||||
src={imageUrl}
|
||||
onClick={onClick}
|
||||
onError={onError}
|
||||
style={{
|
||||
width: toPx(width),
|
||||
height: toPx(height),
|
||||
}}
|
||||
title={title}
|
||||
alt={altText}
|
||||
inputRef={inputRef}
|
||||
data-testid="avatar-img"
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
extraProps["aria-live"] = "off";
|
||||
extraProps["role"] = "button";
|
||||
} else if (!imageUrl) {
|
||||
extraProps["role"] = "presentation";
|
||||
extraProps["aria-label"] = undefined;
|
||||
} else {
|
||||
return (
|
||||
<img
|
||||
loading="lazy"
|
||||
className={classNames("mx_BaseAvatar mx_BaseAvatar_image", className)}
|
||||
src={imageUrl}
|
||||
onError={onError}
|
||||
style={{
|
||||
width: toPx(width),
|
||||
height: toPx(height),
|
||||
}}
|
||||
title={title}
|
||||
alt=""
|
||||
ref={inputRef}
|
||||
data-testid="avatar-img"
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
extraProps["role"] = undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<Avatar
|
||||
ref={inputRef}
|
||||
src={imageUrl}
|
||||
id={idName ?? ""}
|
||||
name={name ?? ""}
|
||||
type={type}
|
||||
size={size}
|
||||
className={classNames("mx_BaseAvatar", className)}
|
||||
aria-label={altText}
|
||||
onError={onError}
|
||||
title={title}
|
||||
onClick={onClick}
|
||||
{...extraProps}
|
||||
{...otherProps}
|
||||
data-testid="avatar-img"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default BaseAvatar;
|
||||
|
|
|
@ -33,7 +33,7 @@ import TooltipTarget from "../elements/TooltipTarget";
|
|||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
avatarSize: number;
|
||||
size: string;
|
||||
displayBadge?: boolean;
|
||||
forceCount?: boolean;
|
||||
oobData?: IOOBData;
|
||||
|
@ -207,8 +207,7 @@ export default class DecoratedRoomAvatar extends React.PureComponent<IProps, ISt
|
|||
<div className={classes}>
|
||||
<RoomAvatar
|
||||
room={this.props.room}
|
||||
width={this.props.avatarSize}
|
||||
height={this.props.avatarSize}
|
||||
size={this.props.size}
|
||||
oobData={this.props.oobData}
|
||||
viewAvatarOnClick={this.props.viewAvatarOnClick}
|
||||
/>
|
||||
|
|
|
@ -30,8 +30,7 @@ import { _t } from "../../../languageHandler";
|
|||
interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url"> {
|
||||
member: RoomMember | null;
|
||||
fallbackUserId?: string;
|
||||
width: number;
|
||||
height: number;
|
||||
size: string;
|
||||
resizeMethod?: ResizeMethod;
|
||||
// Whether the onClick of the avatar should be overridden to dispatch `Action.ViewUser`
|
||||
viewUserOnClick?: boolean;
|
||||
|
@ -44,8 +43,7 @@ interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" |
|
|||
}
|
||||
|
||||
export default function MemberAvatar({
|
||||
width,
|
||||
height,
|
||||
size,
|
||||
resizeMethod = "crop",
|
||||
viewUserOnClick,
|
||||
forceHistorical,
|
||||
|
@ -68,8 +66,8 @@ export default function MemberAvatar({
|
|||
if (member?.name) {
|
||||
if (member.getMxcAvatarUrl()) {
|
||||
imageUrl = mediaFromMxc(member.getMxcAvatarUrl() ?? "").getThumbnailOfSourceHttp(
|
||||
width,
|
||||
height,
|
||||
parseInt(size, 10),
|
||||
parseInt(size, 10),
|
||||
resizeMethod,
|
||||
);
|
||||
}
|
||||
|
@ -85,9 +83,7 @@ export default function MemberAvatar({
|
|||
return (
|
||||
<BaseAvatar
|
||||
{...props}
|
||||
width={width}
|
||||
height={height}
|
||||
resizeMethod={resizeMethod}
|
||||
size={size}
|
||||
name={name ?? ""}
|
||||
title={hideTitle ? undefined : title}
|
||||
idName={member?.userId ?? fallbackUserId}
|
||||
|
@ -104,7 +100,6 @@ export default function MemberAvatar({
|
|||
: props.onClick
|
||||
}
|
||||
altText={_t("Profile picture")}
|
||||
ariaLabel={_t("Profile picture")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
|
||||
import React, { ComponentProps } from "react";
|
||||
import { Room, RoomStateEvent, MatrixEvent, EventType, RoomType } from "matrix-js-sdk/src/matrix";
|
||||
import classNames from "classnames";
|
||||
|
||||
import BaseAvatar from "./BaseAvatar";
|
||||
import ImageView from "../elements/ImageView";
|
||||
|
@ -47,9 +46,7 @@ interface IState {
|
|||
|
||||
export default class RoomAvatar extends React.Component<IProps, IState> {
|
||||
public static defaultProps = {
|
||||
width: 36,
|
||||
height: 36,
|
||||
resizeMethod: "crop",
|
||||
size: "36px",
|
||||
oobData: {},
|
||||
};
|
||||
|
||||
|
@ -87,9 +84,9 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
|
|||
let oobAvatar: string | null = null;
|
||||
if (props.oobData.avatarUrl) {
|
||||
oobAvatar = mediaFromMxc(props.oobData.avatarUrl).getThumbnailOfSourceHttp(
|
||||
props.width,
|
||||
props.height,
|
||||
props.resizeMethod,
|
||||
parseInt(props.size, 10),
|
||||
parseInt(props.size, 10),
|
||||
"crop",
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -102,7 +99,7 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
|
|||
private static getRoomAvatarUrl(props: IProps): string | null {
|
||||
if (!props.room) return null;
|
||||
|
||||
return Avatar.avatarUrlForRoom(props.room, props.width, props.height, props.resizeMethod);
|
||||
return Avatar.avatarUrlForRoom(props.room, parseInt(props.size, 10), parseInt(props.size, 10), "crop");
|
||||
}
|
||||
|
||||
private onRoomAvatarClick = (): void => {
|
||||
|
@ -140,9 +137,7 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
|
|||
return (
|
||||
<BaseAvatar
|
||||
{...otherProps}
|
||||
className={classNames(className, {
|
||||
mx_RoomAvatar_isSpaceRoom: (room?.getType() ?? this.props.oobData?.roomType) === RoomType.Space,
|
||||
})}
|
||||
type={(room?.getType() ?? this.props.oobData?.roomType) === RoomType.Space ? "square" : "round"}
|
||||
name={roomName}
|
||||
idName={this.roomIdName}
|
||||
urls={this.state.urls}
|
||||
|
|
|
@ -24,7 +24,7 @@ import BaseAvatar from "./BaseAvatar";
|
|||
|
||||
interface SearchResultAvatarProps {
|
||||
user: Member | RoomMember;
|
||||
size: number;
|
||||
size: string;
|
||||
}
|
||||
|
||||
export function SearchResultAvatar({ user, size }: SearchResultAvatarProps): JSX.Element {
|
||||
|
@ -46,11 +46,10 @@ export function SearchResultAvatar({ user, size }: SearchResultAvatarProps): JSX
|
|||
return (
|
||||
<BaseAvatar
|
||||
className="mx_SearchResultAvatar"
|
||||
url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(size) : null}
|
||||
url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(parseInt(size, 10)) : null}
|
||||
name={user.name}
|
||||
idName={user.userId}
|
||||
width={size}
|
||||
height={size}
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -22,13 +22,12 @@ import { IApp, isAppWidget } from "../../../stores/WidgetStore";
|
|||
import BaseAvatar, { BaseAvatarType } from "./BaseAvatar";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
|
||||
interface IProps extends Omit<ComponentProps<BaseAvatarType>, "name" | "url" | "urls" | "height" | "width"> {
|
||||
interface IProps extends Omit<ComponentProps<BaseAvatarType>, "name" | "url" | "urls"> {
|
||||
app: IApp | IWidget;
|
||||
height?: number;
|
||||
width?: number;
|
||||
size: string;
|
||||
}
|
||||
|
||||
const WidgetAvatar: React.FC<IProps> = ({ app, className, width = 20, height = 20, ...props }) => {
|
||||
const WidgetAvatar: React.FC<IProps> = ({ app, className, size = "20px", ...props }) => {
|
||||
let iconUrls = [require("../../../../res/img/element-icons/room/default_app.svg").default];
|
||||
// heuristics for some better icons until Widgets support their own icons
|
||||
if (app.type.includes("jitsi")) {
|
||||
|
@ -49,8 +48,7 @@ const WidgetAvatar: React.FC<IProps> = ({ app, className, width = 20, height = 2
|
|||
// MSC2765
|
||||
url={isAppWidget(app) && app.avatar_url ? mediaFromMxc(app.avatar_url).getSquareThumbnailHttp(20) : null}
|
||||
urls={iconUrls}
|
||||
width={width}
|
||||
height={height}
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue