Revert "Member avatars without canvas" (#10057

* Revert "Apply more general fix for base avatar regressions (#10045)"

This reverts commit 371a3c0d36.

* Revert "Fix layout and visual regressions around default avatars (#10031)"

This reverts commit 0d1fce37b2.

* Revert "Member avatars without canvas (#9990)"

This reverts commit a8aa4de4b4.

* Update snapshots
This commit is contained in:
Michael Telatynski 2023-02-02 10:22:19 +00:00 committed by GitHub
parent 43e7870d92
commit 21c3967010
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 352 additions and 823 deletions

View file

@ -153,7 +153,10 @@ describe("Spaces", () => {
openSpaceCreateMenu().within(() => { openSpaceCreateMenu().within(() => {
cy.get(".mx_SpaceCreateMenuType_private").click(); cy.get(".mx_SpaceCreateMenuType_private").click();
// We don't set an avatar here to get a Percy snapshot of the default avatar style for spaces cy.get('.mx_SpaceBasicSettings_avatarContainer input[type="file"]').selectFile(
"cypress/fixtures/riot.png",
{ force: true },
);
cy.get('input[label="Address"]').should("not.exist"); cy.get('input[label="Address"]').should("not.exist");
cy.get('textarea[label="Description"]').type("This is a personal space to mourn Riot.im..."); cy.get('textarea[label="Description"]').type("This is a personal space to mourn Riot.im...");
cy.get('input[label="Name"]').type("This is my Riot{enter}"); cy.get('input[label="Name"]').type("This is my Riot{enter}");
@ -166,7 +169,6 @@ describe("Spaces", () => {
cy.contains(".mx_RoomList .mx_RoomTile", "Sample Room").should("exist"); cy.contains(".mx_RoomList .mx_RoomTile", "Sample Room").should("exist");
cy.contains(".mx_SpaceHierarchy_list .mx_SpaceHierarchy_roomTile", "Sample Room").should("exist"); cy.contains(".mx_SpaceHierarchy_list .mx_SpaceHierarchy_roomTile", "Sample Room").should("exist");
cy.get(".mx_LeftPanel_outerWrapper").percySnapshotElement("Left panel with default avatar space");
}); });
it("should allow user to invite another to a space", () => { it("should allow user to invite another to a space", () => {

View file

@ -277,11 +277,14 @@ $activeBorderColor: $primary-content;
.mx_BaseAvatar:not(.mx_UserMenu_userAvatar_BaseAvatar) .mx_BaseAvatar_initial { .mx_BaseAvatar:not(.mx_UserMenu_userAvatar_BaseAvatar) .mx_BaseAvatar_initial {
color: $secondary-content; color: $secondary-content;
border-radius: 8px; border-radius: 8px;
background-color: $panel-actions;
font-size: $font-15px !important; /* override inline style */
font-weight: $font-semi-bold; font-weight: $font-semi-bold;
line-height: $font-18px; line-height: $font-18px;
/* override inline styles which are part of the default avatar style as these uses a monochrome style */
background-color: $panel-actions !important; & + .mx_BaseAvatar_image {
font-size: $font-15px !important; visibility: hidden;
}
} }
.mx_SpaceTreeLevel { .mx_SpaceTreeLevel {

View file

@ -16,7 +16,16 @@ limitations under the License.
.mx_BaseAvatar { .mx_BaseAvatar {
position: relative; position: relative;
display: block; /* In at least Firefox, the case of relative positioned inline elements */
/* (such as mx_BaseAvatar) with absolute positioned children (such as */
/* mx_BaseAvatar_initial) is a dark corner full of spider webs. It will give */
/* different results during full reflow of the page vs. incremental reflow */
/* of small portions. While that's surely a browser bug, we can avoid it by */
/* using `inline-block` instead of the default `inline`. */
/* https://github.com/vector-im/element-web/issues/5594 */
/* https://bugzilla.mozilla.org/show_bug.cgi?id=1535053 */
/* https://bugzilla.mozilla.org/show_bug.cgi?id=255139 */
display: inline-block;
user-select: none; user-select: none;
&.mx_RoomAvatar_isSpaceRoom { &.mx_RoomAvatar_isSpaceRoom {

View file

@ -78,7 +78,7 @@ limitations under the License.
min-width: $font-16px; /* ensure the avatar is not compressed */ min-width: $font-16px; /* ensure the avatar is not compressed */
height: $font-16px; height: $font-16px;
margin-inline-end: 0.24rem; margin-inline-end: 0.24rem;
background: var(--avatar-background); background: var(--avatar-background), $background;
color: $avatar-initial-color; color: $avatar-initial-color;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: $font-16px; background-size: $font-16px;

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2015, 2016, 2023 The Matrix.org Foundation C.I.C. Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -24,19 +24,16 @@ import DMRoomMap from "./utils/DMRoomMap";
import { mediaFromMxc } from "./customisations/Media"; import { mediaFromMxc } from "./customisations/Media";
import { isLocalRoom } from "./utils/localRoom/isLocalRoom"; import { isLocalRoom } from "./utils/localRoom/isLocalRoom";
const DEFAULT_COLORS: Readonly<string[]> = ["#0DBD8B", "#368bd6", "#ac3ba8"];
// Not to be used for BaseAvatar urls as that has similar default avatar fallback already // Not to be used for BaseAvatar urls as that has similar default avatar fallback already
export function avatarUrlForMember( export function avatarUrlForMember(
member: RoomMember | null | undefined, member: RoomMember,
width: number, width: number,
height: number, height: number,
resizeMethod: ResizeMethod, resizeMethod: ResizeMethod,
): string { ): string {
let url: string | undefined; let url: string;
const mxcUrl = member?.getMxcAvatarUrl(); if (member?.getMxcAvatarUrl()) {
if (mxcUrl) { url = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
url = mediaFromMxc(mxcUrl).getThumbnailOfSourceHttp(width, height, resizeMethod);
} }
if (!url) { if (!url) {
// member can be null here currently since on invites, the JS SDK // member can be null here currently since on invites, the JS SDK
@ -47,17 +44,6 @@ export function avatarUrlForMember(
return url; return url;
} }
export function getMemberAvatar(
member: RoomMember | null | undefined,
width: number,
height: number,
resizeMethod: ResizeMethod,
): string | undefined {
const mxcUrl = member?.getMxcAvatarUrl();
if (!mxcUrl) return undefined;
return mediaFromMxc(mxcUrl).getThumbnailOfSourceHttp(width, height, resizeMethod);
}
export function avatarUrlForUser( export function avatarUrlForUser(
user: Pick<User, "avatarUrl">, user: Pick<User, "avatarUrl">,
width: number, width: number,
@ -100,10 +86,18 @@ function urlForColor(color: string): string {
// hard to install a listener here, even if there were a clear event to listen to // hard to install a listener here, even if there were a clear event to listen to
const colorToDataURLCache = new Map<string, string>(); const colorToDataURLCache = new Map<string, string>();
export function defaultAvatarUrlForString(s: string | undefined): string { export function defaultAvatarUrlForString(s: string): string {
if (!s) return ""; // XXX: should never happen but empirically does by evidence of a rageshake if (!s) return ""; // XXX: should never happen but empirically does by evidence of a rageshake
const defaultColors = ["#0DBD8B", "#368bd6", "#ac3ba8"];
const color = getColorForString(s); let total = 0;
for (let i = 0; i < s.length; ++i) {
total += s.charCodeAt(i);
}
const colorIndex = total % defaultColors.length;
// overwritten color value in custom themes
const cssVariable = `--avatar-background-colors_${colorIndex}`;
const cssValue = document.body.style.getPropertyValue(cssVariable);
const color = cssValue || defaultColors[colorIndex];
let dataUrl = colorToDataURLCache.get(color); let dataUrl = colorToDataURLCache.get(color);
if (!dataUrl) { if (!dataUrl) {
// validate color as this can come from account_data // validate color as this can come from account_data
@ -118,23 +112,13 @@ export function defaultAvatarUrlForString(s: string | undefined): string {
return dataUrl; return dataUrl;
} }
export function getColorForString(input: string): string {
const charSum = [...input].reduce((s, c) => s + c.charCodeAt(0), 0);
const index = charSum % DEFAULT_COLORS.length;
// overwritten color value in custom themes
const cssVariable = `--avatar-background-colors_${index}`;
const cssValue = document.body.style.getPropertyValue(cssVariable);
return cssValue || DEFAULT_COLORS[index]!;
}
/** /**
* returns the first (non-sigil) character of 'name', * returns the first (non-sigil) character of 'name',
* converted to uppercase * converted to uppercase
* @param {string} name * @param {string} name
* @return {string} the first letter * @return {string} the first letter
*/ */
export function getInitialLetter(name: string): string | undefined { export function getInitialLetter(name: string): string {
if (!name) { if (!name) {
// XXX: We should find out what causes the name to sometimes be falsy. // XXX: We should find out what causes the name to sometimes be falsy.
console.trace("`name` argument to `getInitialLetter` not supplied"); console.trace("`name` argument to `getInitialLetter` not supplied");
@ -150,20 +134,19 @@ export function getInitialLetter(name: string): string | undefined {
} }
// rely on the grapheme cluster splitter in lodash so that we don't break apart compound emojis // rely on the grapheme cluster splitter in lodash so that we don't break apart compound emojis
return split(name, "", 1)[0]!.toUpperCase(); return split(name, "", 1)[0].toUpperCase();
} }
export function avatarUrlForRoom( export function avatarUrlForRoom(
room: Room | undefined, room: Room,
width: number, width: number,
height: number, height: number,
resizeMethod?: ResizeMethod, resizeMethod?: ResizeMethod,
): string | null { ): string | null {
if (!room) return null; // null-guard if (!room) return null; // null-guard
const mxcUrl = room.getMxcAvatarUrl(); if (room.getMxcAvatarUrl()) {
if (mxcUrl) { return mediaFromMxc(room.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
return mediaFromMxc(mxcUrl).getThumbnailOfSourceHttp(width, height, resizeMethod);
} }
// space rooms cannot be DMs so skip the rest // space rooms cannot be DMs so skip the rest
@ -176,9 +159,8 @@ export function avatarUrlForRoom(
// If there are only two members in the DM use the avatar of the other member // If there are only two members in the DM use the avatar of the other member
const otherMember = room.getAvatarFallbackMember(); const otherMember = room.getAvatarFallbackMember();
const otherMemberMxc = otherMember?.getMxcAvatarUrl(); if (otherMember?.getMxcAvatarUrl()) {
if (otherMemberMxc) { return mediaFromMxc(otherMember.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
return mediaFromMxc(otherMemberMxc).getThumbnailOfSourceHttp(width, height, resizeMethod);
} }
return null; return null;
} }

View file

@ -1,6 +1,8 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2015, 2016, 2018, 2019, 2020, 2023 The Matrix.org Foundation C.I.C. Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,46 +17,38 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { CSSProperties, useCallback, useContext, useEffect, useState } from "react"; import React, { useCallback, useContext, useEffect, useState } from "react";
import classNames from "classnames"; import classNames from "classnames";
import { ResizeMethod } from "matrix-js-sdk/src/@types/partials"; import { ResizeMethod } from "matrix-js-sdk/src/@types/partials";
import { ClientEvent } from "matrix-js-sdk/src/client"; import { ClientEvent } from "matrix-js-sdk/src/client";
import { SyncState } from "matrix-js-sdk/src/sync";
import * as AvatarLogic from "../../../Avatar"; import * as AvatarLogic from "../../../Avatar";
import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import RoomContext from "../../../contexts/RoomContext"; import RoomContext from "../../../contexts/RoomContext";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter"; import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
import { toPx } from "../../../utils/units"; import { toPx } from "../../../utils/units";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
interface IProps { interface IProps {
/** The name (first initial used as default) */ name: string; // The name (first initial used as default)
name: string; idName?: string; // ID for generating hash colours
/** ID for generating hash colours */ title?: string; // onHover title text
idName?: string; url?: string; // highest priority of them all, shortcut to set in urls[0]
/** onHover title text */ urls?: string[]; // [highest_priority, ... , lowest_priority]
title?: string;
/** highest priority of them all, shortcut to set in urls[0] */
url?: string;
/** [highest_priority, ... , lowest_priority] */
urls?: string[];
width?: number; width?: number;
height?: number; height?: number;
/** @deprecated not actually used */ // XXX: resizeMethod not actually used.
resizeMethod?: ResizeMethod; resizeMethod?: ResizeMethod;
/** true to add default url */ defaultToInitialLetter?: boolean; // true to add default url
defaultToInitialLetter?: boolean; onClick?: React.MouseEventHandler;
onClick?: React.ComponentPropsWithoutRef<typeof AccessibleTooltipButton>["onClick"];
inputRef?: React.RefObject<HTMLImageElement & HTMLSpanElement>; inputRef?: React.RefObject<HTMLImageElement & HTMLSpanElement>;
className?: string; className?: string;
tabIndex?: number; tabIndex?: number;
style?: CSSProperties;
} }
const calculateUrls = (url: string | undefined, urls: string[] | undefined, lowBandwidth: boolean): string[] => { const calculateUrls = (url: string, urls: string[], lowBandwidth: boolean): string[] => {
// work out the full set of urls to try to load. This is formed like so: // work out the full set of urls to try to load. This is formed like so:
// imageUrls: [ props.url, ...props.urls ] // imageUrls: [ props.url, ...props.urls ]
@ -72,26 +66,11 @@ const calculateUrls = (url: string | undefined, urls: string[] | undefined, lowB
return Array.from(new Set(_urls)); return Array.from(new Set(_urls));
}; };
/** const useImageUrl = ({ url, urls }): [string, () => void] => {
* Hook for cycling through a changing set of images.
*
* The set of images is updated whenever `url` or `urls` change, the user's
* `lowBandwidth` preference changes, or the client reconnects.
*
* Returns `[imageUrl, onError]`. When `onError` is called, the next image in
* the set will be displayed.
*/
const useImageUrl = ({
url,
urls,
}: {
url: string | undefined;
urls: string[] | undefined;
}): [string | undefined, () => void] => {
// Since this is a hot code path and the settings store can be slow, we // Since this is a hot code path and the settings store can be slow, we
// use the cached lowBandwidth value from the room context if it exists // use the cached lowBandwidth value from the room context if it exists
const roomContext = useContext(RoomContext); const roomContext = useContext(RoomContext);
const lowBandwidth = roomContext.lowBandwidth; const lowBandwidth = roomContext ? roomContext.lowBandwidth : SettingsStore.getValue("lowBandwidth");
const [imageUrls, setUrls] = useState<string[]>(calculateUrls(url, urls, lowBandwidth)); const [imageUrls, setUrls] = useState<string[]>(calculateUrls(url, urls, lowBandwidth));
const [urlsIndex, setIndex] = useState<number>(0); const [urlsIndex, setIndex] = useState<number>(0);
@ -106,10 +85,10 @@ const useImageUrl = ({
}, [url, JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps }, [url, JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const onClientSync = useCallback((syncState: SyncState, prevState: SyncState | null) => { const onClientSync = useCallback((syncState, prevState) => {
// Consider the client reconnected if there is no error with syncing. // Consider the client reconnected if there is no error with syncing.
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP. // This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
const reconnected = syncState !== SyncState.Error && prevState !== syncState; const reconnected = syncState !== "ERROR" && prevState !== syncState;
if (reconnected) { if (reconnected) {
setIndex(0); setIndex(0);
} }
@ -129,25 +108,46 @@ const BaseAvatar: React.FC<IProps> = (props) => {
urls, urls,
width = 40, width = 40,
height = 40, height = 40,
resizeMethod = "crop", // eslint-disable-line @typescript-eslint/no-unused-vars
defaultToInitialLetter = true, defaultToInitialLetter = true,
onClick, onClick,
inputRef, inputRef,
className, className,
style: parentStyle,
resizeMethod: _unused, // to keep it from being in `otherProps`
...otherProps ...otherProps
} = props; } = props;
const style = {
...parentStyle,
width: toPx(width),
height: toPx(height),
};
const [imageUrl, onError] = useImageUrl({ url, urls }); const [imageUrl, onError] = useImageUrl({ url, urls });
if (!imageUrl && defaultToInitialLetter && name) { if (!imageUrl && defaultToInitialLetter && name) {
const avatar = <TextAvatar name={name} idName={idName} width={width} height={height} title={title} />; 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
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) { if (onClick) {
return ( return (
@ -159,9 +159,9 @@ const BaseAvatar: React.FC<IProps> = (props) => {
className={classNames("mx_BaseAvatar", className)} className={classNames("mx_BaseAvatar", className)}
onClick={onClick} onClick={onClick}
inputRef={inputRef} inputRef={inputRef}
style={style}
> >
{avatar} {textNode}
{imgNode}
</AccessibleButton> </AccessibleButton>
); );
} else { } else {
@ -170,10 +170,10 @@ const BaseAvatar: React.FC<IProps> = (props) => {
className={classNames("mx_BaseAvatar", className)} className={classNames("mx_BaseAvatar", className)}
ref={inputRef} ref={inputRef}
{...otherProps} {...otherProps}
style={style}
role="presentation" role="presentation"
> >
{avatar} {textNode}
{imgNode}
</span> </span>
); );
} }
@ -187,7 +187,10 @@ const BaseAvatar: React.FC<IProps> = (props) => {
src={imageUrl} src={imageUrl}
onClick={onClick} onClick={onClick}
onError={onError} onError={onError}
style={style} style={{
width: toPx(width),
height: toPx(height),
}}
title={title} title={title}
alt={_t("Avatar")} alt={_t("Avatar")}
inputRef={inputRef} inputRef={inputRef}
@ -201,7 +204,10 @@ const BaseAvatar: React.FC<IProps> = (props) => {
className={classNames("mx_BaseAvatar mx_BaseAvatar_image", className)} className={classNames("mx_BaseAvatar mx_BaseAvatar_image", className)}
src={imageUrl} src={imageUrl}
onError={onError} onError={onError}
style={style} style={{
width: toPx(width),
height: toPx(height),
}}
title={title} title={title}
alt="" alt=""
ref={inputRef} ref={inputRef}
@ -214,31 +220,3 @@ const BaseAvatar: React.FC<IProps> = (props) => {
export default BaseAvatar; export default BaseAvatar;
export type BaseAvatarType = React.FC<IProps>; export type BaseAvatarType = React.FC<IProps>;
const TextAvatar: React.FC<{
name: string;
idName?: string;
width: number;
height: number;
title?: string;
}> = ({ name, idName, width, height, title }) => {
const initialLetter = AvatarLogic.getInitialLetter(name);
return (
<span
className="mx_BaseAvatar_image mx_BaseAvatar_initial"
aria-hidden="true"
style={{
backgroundColor: AvatarLogic.getColorForString(idName || name),
width: toPx(width),
height: toPx(height),
fontSize: toPx(width * 0.65),
lineHeight: toPx(height),
}}
title={title}
data-testid="avatar-img"
>
{initialLetter}
</span>
);
};

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2015, 2016, 2019 - 2023 The Matrix.org Foundation C.I.C. Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 - 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -25,7 +26,6 @@ import { mediaFromMxc } from "../../../customisations/Media";
import { CardContext } from "../right_panel/context"; import { CardContext } from "../right_panel/context";
import UserIdentifierCustomisations from "../../../customisations/UserIdentifier"; import UserIdentifierCustomisations from "../../../customisations/UserIdentifier";
import { useRoomMemberProfile } from "../../../hooks/room/useRoomMemberProfile"; import { useRoomMemberProfile } from "../../../hooks/room/useRoomMemberProfile";
import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload";
interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url"> { interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url"> {
member: RoomMember | null; member: RoomMember | null;
@ -33,13 +33,14 @@ interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" |
width: number; width: number;
height: number; height: number;
resizeMethod?: ResizeMethod; resizeMethod?: ResizeMethod;
/** Whether the onClick of the avatar should be overridden to dispatch `Action.ViewUser` */ // The onClick to give the avatar
onClick?: React.MouseEventHandler;
// Whether the onClick of the avatar should be overridden to dispatch `Action.ViewUser`
viewUserOnClick?: boolean; viewUserOnClick?: boolean;
pushUserOnClick?: boolean; pushUserOnClick?: boolean;
title?: string; title?: string;
style?: React.CSSProperties; style?: any;
/** true to deny `useOnlyCurrentProfiles` usage. Default false. */ forceHistorical?: boolean; // true to deny `useOnlyCurrentProfiles` usage. Default false.
forceHistorical?: boolean;
hideTitle?: boolean; hideTitle?: boolean;
} }
@ -76,8 +77,8 @@ export default function MemberAvatar({
if (!title) { if (!title) {
title = title =
UserIdentifierCustomisations.getDisplayUserIdentifier!(member.userId, { UserIdentifierCustomisations.getDisplayUserIdentifier(member?.userId ?? "", {
roomId: member.roomId, roomId: member?.roomId ?? "",
}) ?? fallbackUserId; }) ?? fallbackUserId;
} }
} }
@ -87,6 +88,7 @@ export default function MemberAvatar({
{...props} {...props}
width={width} width={width}
height={height} height={height}
resizeMethod={resizeMethod}
name={name ?? ""} name={name ?? ""}
title={hideTitle ? undefined : title} title={hideTitle ? undefined : title}
idName={member?.userId ?? fallbackUserId} idName={member?.userId ?? fallbackUserId}
@ -94,9 +96,9 @@ export default function MemberAvatar({
onClick={ onClick={
viewUserOnClick viewUserOnClick
? () => { ? () => {
dis.dispatch<ViewUserPayload>({ dis.dispatch({
action: Action.ViewUser, action: Action.ViewUser,
member: propsMember || undefined, member: propsMember,
push: card.isCard, push: card.isCard,
}); });
} }

View file

@ -109,8 +109,7 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
} }
private onRoomAvatarClick = (): void => { private onRoomAvatarClick = (): void => {
const avatarMxc = this.props.room?.getMxcAvatarUrl(); const avatarUrl = Avatar.avatarUrlForRoom(this.props.room, null, null, null);
const avatarUrl = avatarMxc ? mediaFromMxc(avatarMxc).srcHttp : null;
const params = { const params = {
src: avatarUrl, src: avatarUrl,
name: this.props.room.name, name: this.props.room.name,

View file

@ -28,11 +28,4 @@ export interface ViewUserPayload extends ActionPayload {
* should be shown (hide whichever relevant components). * should be shown (hide whichever relevant components).
*/ */
member?: RoomMember | User; member?: RoomMember | User;
/**
* Should this event be pushed as a card into the right panel?
*
* @see RightPanelStore#pushCard
*/
push?: boolean;
} }

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2019, 2023 The Matrix.org Foundation C.I.C. Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -300,9 +301,9 @@ export abstract class PillPart extends BasePart implements IPillPart {
} }
// helper method for subclasses // helper method for subclasses
protected setAvatarVars(node: HTMLElement, avatarBackground: string, initialLetter: string | undefined): void { protected setAvatarVars(node: HTMLElement, avatarUrl: string, initialLetter: string): void {
// const avatarBackground = `url('${avatarUrl}')`; const avatarBackground = `url('${avatarUrl}')`;
const avatarLetter = `'${initialLetter || ""}'`; const avatarLetter = `'${initialLetter}'`;
// check if the value is changing, // check if the value is changing,
// otherwise the avatars flicker on every keystroke while updating. // otherwise the avatars flicker on every keystroke while updating.
if (node.style.getPropertyValue("--avatar-background") !== avatarBackground) { if (node.style.getPropertyValue("--avatar-background") !== avatarBackground) {
@ -418,15 +419,13 @@ class RoomPillPart extends PillPart {
} }
protected setAvatar(node: HTMLElement): void { protected setAvatar(node: HTMLElement): void {
const avatarUrl = Avatar.avatarUrlForRoom(this.room, 16, 16, "crop"); let initialLetter = "";
if (avatarUrl) { let avatarUrl = Avatar.avatarUrlForRoom(this.room, 16, 16, "crop");
this.setAvatarVars(node, `url('${avatarUrl}')`, ""); if (!avatarUrl) {
return; initialLetter = Avatar.getInitialLetter(this.room?.name || this.resourceId);
avatarUrl = Avatar.defaultAvatarUrlForString(this.room?.roomId ?? this.resourceId);
} }
this.setAvatarVars(node, avatarUrl, initialLetter);
const initialLetter = Avatar.getInitialLetter(this.room?.name || this.resourceId);
const color = Avatar.getColorForString(this.room?.roomId ?? this.resourceId);
this.setAvatarVars(node, color, initialLetter);
} }
public get type(): IPillPart["type"] { public get type(): IPillPart["type"] {
@ -472,17 +471,14 @@ class UserPillPart extends PillPart {
if (!this.member) { if (!this.member) {
return; return;
} }
const avatar = Avatar.getMemberAvatar(this.member, 16, 16, "crop");
if (avatar) {
this.setAvatarVars(node, `url('${avatar}')`, "");
return;
}
const name = this.member.name || this.member.userId; const name = this.member.name || this.member.userId;
const initialLetter = Avatar.getInitialLetter(name); const defaultAvatarUrl = Avatar.defaultAvatarUrlForString(this.member.userId);
const color = Avatar.getColorForString(this.member.userId); const avatarUrl = Avatar.avatarUrlForMember(this.member, 16, 16, "crop");
this.setAvatarVars(node, color, initialLetter); let initialLetter = "";
if (avatarUrl === defaultAvatarUrl) {
initialLetter = Avatar.getInitialLetter(name);
}
this.setAvatarVars(node, avatarUrl, initialLetter);
} }
protected onClick = (): void => { protected onClick = (): void => {

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C. Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,106 +15,33 @@ limitations under the License.
*/ */
import { mocked } from "jest-mock"; import { mocked } from "jest-mock";
import { Room, RoomMember, RoomType, User } from "matrix-js-sdk/src/matrix"; import { Room, RoomMember, RoomType } from "matrix-js-sdk/src/matrix";
import { import { avatarUrlForRoom } from "../src/Avatar";
avatarUrlForMember, import { Media, mediaFromMxc } from "../src/customisations/Media";
avatarUrlForRoom,
avatarUrlForUser,
defaultAvatarUrlForString,
getColorForString,
getInitialLetter,
} from "../src/Avatar";
import { mediaFromMxc } from "../src/customisations/Media";
import DMRoomMap from "../src/utils/DMRoomMap"; import DMRoomMap from "../src/utils/DMRoomMap";
import { filterConsole, stubClient } from "./test-utils";
jest.mock("../src/customisations/Media", () => ({
mediaFromMxc: jest.fn(),
}));
const roomId = "!room:example.com"; const roomId = "!room:example.com";
const avatarUrl1 = "https://example.com/avatar1"; const avatarUrl1 = "https://example.com/avatar1";
const avatarUrl2 = "https://example.com/avatar2"; const avatarUrl2 = "https://example.com/avatar2";
describe("avatarUrlForMember", () => {
let member: RoomMember;
beforeEach(() => {
stubClient();
member = new RoomMember(roomId, "@user:example.com");
});
it("returns the member's url", () => {
const mxc = "mxc://example.com/a/b/c/d/avatar.gif";
jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue(mxc);
expect(avatarUrlForMember(member, 32, 32, "crop")).toBe(
mediaFromMxc(mxc).getThumbnailOfSourceHttp(32, 32, "crop"),
);
});
it("returns a default if the member has no avatar", () => {
jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue(undefined);
expect(avatarUrlForMember(member, 32, 32, "crop")).toMatch(/^data:/);
});
});
describe("avatarUrlForUser", () => {
let user: User;
beforeEach(() => {
stubClient();
user = new User("@user:example.com");
});
it("should return the user's avatar", () => {
const mxc = "mxc://example.com/a/b/c/d/avatar.gif";
user.avatarUrl = mxc;
expect(avatarUrlForUser(user, 64, 64, "scale")).toBe(
mediaFromMxc(mxc).getThumbnailOfSourceHttp(64, 64, "scale"),
);
});
it("should not provide a fallback", () => {
expect(avatarUrlForUser(user, 64, 64, "scale")).toBeNull();
});
});
describe("defaultAvatarUrlForString", () => {
it.each(["a", "abc", "abcde", "@".repeat(150)])("should return a value for %s", (s) => {
expect(defaultAvatarUrlForString(s)).not.toBe("");
});
});
describe("getColorForString", () => {
it.each(["a", "abc", "abcde", "@".repeat(150)])("should return a value for %s", (s) => {
expect(getColorForString(s)).toMatch(/^#\w+$/);
});
it("should return different values for different strings", () => {
expect(getColorForString("a")).not.toBe(getColorForString("b"));
});
});
describe("getInitialLetter", () => {
filterConsole("argument to `getInitialLetter` not supplied");
it.each(["a", "abc", "abcde", "@".repeat(150)])("should return a value for %s", (s) => {
expect(getInitialLetter(s)).not.toBe("");
});
it("should return undefined for empty strings", () => {
expect(getInitialLetter("")).toBeUndefined();
});
});
describe("avatarUrlForRoom", () => { describe("avatarUrlForRoom", () => {
let getThumbnailOfSourceHttp: jest.Mock;
let room: Room; let room: Room;
let roomMember: RoomMember; let roomMember: RoomMember;
let dmRoomMap: DMRoomMap; let dmRoomMap: DMRoomMap;
beforeEach(() => { beforeEach(() => {
stubClient(); getThumbnailOfSourceHttp = jest.fn();
mocked(mediaFromMxc).mockImplementation((): Media => {
return {
getThumbnailOfSourceHttp,
} as unknown as Media;
});
room = { room = {
roomId, roomId,
getMxcAvatarUrl: jest.fn(), getMxcAvatarUrl: jest.fn(),
@ -132,14 +59,14 @@ describe("avatarUrlForRoom", () => {
}); });
it("should return null for a null room", () => { it("should return null for a null room", () => {
expect(avatarUrlForRoom(undefined, 128, 128)).toBeNull(); expect(avatarUrlForRoom(null, 128, 128)).toBeNull();
}); });
it("should return the HTTP source if the room provides a MXC url", () => { it("should return the HTTP source if the room provides a MXC url", () => {
mocked(room.getMxcAvatarUrl).mockReturnValue(avatarUrl1); mocked(room.getMxcAvatarUrl).mockReturnValue(avatarUrl1);
expect(avatarUrlForRoom(room, 128, 256, "crop")).toBe( getThumbnailOfSourceHttp.mockReturnValue(avatarUrl2);
mediaFromMxc(avatarUrl1).getThumbnailOfSourceHttp(128, 256, "crop"), expect(avatarUrlForRoom(room, 128, 256, "crop")).toEqual(avatarUrl2);
); expect(getThumbnailOfSourceHttp).toHaveBeenCalledWith(128, 256, "crop");
}); });
it("should return null for a space room", () => { it("should return null for a space room", () => {
@ -156,7 +83,7 @@ describe("avatarUrlForRoom", () => {
it("should return null if there is no other member in the room", () => { it("should return null if there is no other member in the room", () => {
mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue("@user:example.com"); mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue("@user:example.com");
mocked(room.getAvatarFallbackMember).mockReturnValue(undefined); mocked(room.getAvatarFallbackMember).mockReturnValue(null);
expect(avatarUrlForRoom(room, 128, 128)).toBeNull(); expect(avatarUrlForRoom(room, 128, 128)).toBeNull();
}); });
@ -170,8 +97,8 @@ describe("avatarUrlForRoom", () => {
mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue("@user:example.com"); mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue("@user:example.com");
mocked(room.getAvatarFallbackMember).mockReturnValue(roomMember); mocked(room.getAvatarFallbackMember).mockReturnValue(roomMember);
mocked(roomMember.getMxcAvatarUrl).mockReturnValue(avatarUrl2); mocked(roomMember.getMxcAvatarUrl).mockReturnValue(avatarUrl2);
expect(avatarUrlForRoom(room, 128, 256, "crop")).toEqual( getThumbnailOfSourceHttp.mockReturnValue(avatarUrl2);
mediaFromMxc(avatarUrl2).getThumbnailOfSourceHttp(128, 256, "crop"), expect(avatarUrlForRoom(room, 128, 256, "crop")).toEqual(avatarUrl2);
); expect(getThumbnailOfSourceHttp).toHaveBeenCalledWith(128, 256, "crop");
}); });
}); });

View file

@ -20,16 +20,22 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
<span <span
class="mx_BaseAvatar" class="mx_BaseAvatar"
role="presentation" role="presentation"
style="width: 24px; height: 24px;"
> >
<span <span
aria-hidden="true" aria-hidden="true"
class="mx_BaseAvatar_image mx_BaseAvatar_initial" class="mx_BaseAvatar_initial"
data-testid="avatar-img" style="font-size: 15.600000000000001px; width: 24px; line-height: 24px;"
style="background-color: rgb(172, 59, 168); width: 24px; height: 24px; font-size: 15.600000000000001px; line-height: 24px;"
> >
U U
</span> </span>
<img
alt=""
aria-hidden="true"
class="mx_BaseAvatar_image"
data-testid="avatar-img"
src="data:image/png;base64,00"
style="width: 24px; height: 24px;"
/>
</span> </span>
</div> </div>
</div> </div>
@ -113,16 +119,22 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
<span <span
class="mx_BaseAvatar" class="mx_BaseAvatar"
role="presentation" role="presentation"
style="width: 24px; height: 24px;"
> >
<span <span
aria-hidden="true" aria-hidden="true"
class="mx_BaseAvatar_image mx_BaseAvatar_initial" class="mx_BaseAvatar_initial"
data-testid="avatar-img" style="font-size: 15.600000000000001px; width: 24px; line-height: 24px;"
style="background-color: rgb(172, 59, 168); width: 24px; height: 24px; font-size: 15.600000000000001px; line-height: 24px;"
> >
U U
</span> </span>
<img
alt=""
aria-hidden="true"
class="mx_BaseAvatar_image"
data-testid="avatar-img"
src="data:image/png;base64,00"
style="width: 24px; height: 24px;"
/>
</span> </span>
</div> </div>
</div> </div>
@ -203,17 +215,23 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
aria-live="off" aria-live="off"
class="mx_AccessibleButton mx_BaseAvatar" class="mx_AccessibleButton mx_BaseAvatar"
role="button" role="button"
style="width: 52px; height: 52px;"
tabindex="0" tabindex="0"
> >
<span <span
aria-hidden="true" aria-hidden="true"
class="mx_BaseAvatar_image mx_BaseAvatar_initial" class="mx_BaseAvatar_initial"
data-testid="avatar-img" style="font-size: 33.800000000000004px; width: 52px; line-height: 52px;"
style="background-color: rgb(172, 59, 168); width: 52px; height: 52px; font-size: 33.800000000000004px; line-height: 52px;"
> >
U U
</span> </span>
<img
alt=""
aria-hidden="true"
class="mx_BaseAvatar_image"
data-testid="avatar-img"
src="data:image/png;base64,00"
style="width: 52px; height: 52px;"
/>
</span> </span>
<h2> <h2>
@user:example.com @user:example.com
@ -296,16 +314,22 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
<span <span
class="mx_BaseAvatar" class="mx_BaseAvatar"
role="presentation" role="presentation"
style="width: 24px; height: 24px;"
> >
<span <span
aria-hidden="true" aria-hidden="true"
class="mx_BaseAvatar_image mx_BaseAvatar_initial" class="mx_BaseAvatar_initial"
data-testid="avatar-img" style="font-size: 15.600000000000001px; width: 24px; line-height: 24px;"
style="background-color: rgb(172, 59, 168); width: 24px; height: 24px; font-size: 15.600000000000001px; line-height: 24px;"
> >
U U
</span> </span>
<img
alt=""
aria-hidden="true"
class="mx_BaseAvatar_image"
data-testid="avatar-img"
src="data:image/png;base64,00"
style="width: 24px; height: 24px;"
/>
</span> </span>
</div> </div>
</div> </div>
@ -386,17 +410,23 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
aria-live="off" aria-live="off"
class="mx_AccessibleButton mx_BaseAvatar" class="mx_AccessibleButton mx_BaseAvatar"
role="button" role="button"
style="width: 52px; height: 52px;"
tabindex="0" tabindex="0"
> >
<span <span
aria-hidden="true" aria-hidden="true"
class="mx_BaseAvatar_image mx_BaseAvatar_initial" class="mx_BaseAvatar_initial"
data-testid="avatar-img" style="font-size: 33.800000000000004px; width: 52px; line-height: 52px;"
style="background-color: rgb(172, 59, 168); width: 52px; height: 52px; font-size: 33.800000000000004px; line-height: 52px;"
> >
U U
</span> </span>
<img
alt=""
aria-hidden="true"
class="mx_BaseAvatar_image"
data-testid="avatar-img"
src="data:image/png;base64,00"
style="width: 52px; height: 52px;"
/>
</span> </span>
<h2> <h2>
@user:example.com @user:example.com
@ -551,16 +581,22 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
<span <span
class="mx_BaseAvatar" class="mx_BaseAvatar"
role="presentation" role="presentation"
style="width: 24px; height: 24px;"
> >
<span <span
aria-hidden="true" aria-hidden="true"
class="mx_BaseAvatar_image mx_BaseAvatar_initial" class="mx_BaseAvatar_initial"
data-testid="avatar-img" style="font-size: 15.600000000000001px; width: 24px; line-height: 24px;"
style="background-color: rgb(172, 59, 168); width: 24px; height: 24px; font-size: 15.600000000000001px; line-height: 24px;"
> >
U U
</span> </span>
<img
alt=""
aria-hidden="true"
class="mx_BaseAvatar_image"
data-testid="avatar-img"
src="data:image/png;base64,00"
style="width: 24px; height: 24px;"
/>
</span> </span>
</div> </div>
</div> </div>
@ -636,17 +672,23 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
aria-live="off" aria-live="off"
class="mx_AccessibleButton mx_BaseAvatar" class="mx_AccessibleButton mx_BaseAvatar"
role="button" role="button"
style="width: 52px; height: 52px;"
tabindex="0" tabindex="0"
> >
<span <span
aria-hidden="true" aria-hidden="true"
class="mx_BaseAvatar_image mx_BaseAvatar_initial" class="mx_BaseAvatar_initial"
data-testid="avatar-img" style="font-size: 33.800000000000004px; width: 52px; line-height: 52px;"
style="background-color: rgb(172, 59, 168); width: 52px; height: 52px; font-size: 33.800000000000004px; line-height: 52px;"
> >
U U
</span> </span>
<img
alt=""
aria-hidden="true"
class="mx_BaseAvatar_image"
data-testid="avatar-img"
src="data:image/png;base64,00"
style="width: 52px; height: 52px;"
/>
</span> </span>
<h2> <h2>
@user:example.com @user:example.com

View file

@ -20,16 +20,22 @@ exports[`<UserMenu> when rendered should render as expected 1`] = `
<span <span
class="mx_BaseAvatar mx_UserMenu_userAvatar_BaseAvatar" class="mx_BaseAvatar mx_UserMenu_userAvatar_BaseAvatar"
role="presentation" role="presentation"
style="width: 32px; height: 32px;"
> >
<span <span
aria-hidden="true" aria-hidden="true"
class="mx_BaseAvatar_image mx_BaseAvatar_initial" class="mx_BaseAvatar_initial"
data-testid="avatar-img" style="font-size: 20.8px; width: 32px; line-height: 32px;"
style="background-color: rgb(54, 139, 214); width: 32px; height: 32px; font-size: 20.8px; line-height: 32px;"
> >
U U
</span> </span>
<img
alt=""
aria-hidden="true"
class="mx_BaseAvatar_image"
data-testid="avatar-img"
src="data:image/png;base64,00"
style="width: 32px; height: 32px;"
/>
</span> </span>
</div> </div>
</div> </div>

View file

@ -1,201 +0,0 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { fireEvent, render } from "@testing-library/react";
import { ClientEvent, PendingEventOrdering } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import React from "react";
import { act } from "react-dom/test-utils";
import { SyncState } from "matrix-js-sdk/src/sync";
import type { MatrixClient } from "matrix-js-sdk/src/client";
import RoomContext from "../../../../src/contexts/RoomContext";
import { getRoomContext } from "../../../test-utils/room";
import { stubClient } from "../../../test-utils/test-utils";
import BaseAvatar from "../../../../src/components/views/avatars/BaseAvatar";
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
type Props = React.ComponentPropsWithoutRef<typeof BaseAvatar>;
describe("<BaseAvatar />", () => {
let client: MatrixClient;
let room: Room;
let member: RoomMember;
function getComponent(props: Partial<Props>) {
return (
<MatrixClientContext.Provider value={client}>
<RoomContext.Provider value={getRoomContext(room, {})}>
<BaseAvatar name="" {...props} />
</RoomContext.Provider>
</MatrixClientContext.Provider>
);
}
function failLoadingImg(container: HTMLElement): void {
const img = container.querySelector<HTMLImageElement>("img")!;
expect(img).not.toBeNull();
act(() => {
fireEvent.error(img);
});
}
function emitReconnect(): void {
act(() => {
client.emit(ClientEvent.Sync, SyncState.Prepared, SyncState.Reconnecting);
});
}
beforeEach(() => {
client = stubClient();
room = new Room("!room:example.com", client, client.getUserId() ?? "", {
pendingEventOrdering: PendingEventOrdering.Detached,
});
member = new RoomMember(room.roomId, "@bob:example.org");
jest.spyOn(room, "getMember").mockReturnValue(member);
});
it("renders with minimal properties", () => {
const { container } = render(getComponent({}));
expect(container.querySelector(".mx_BaseAvatar")).not.toBeNull();
});
it("matches snapshot (avatar)", () => {
const { container } = render(
getComponent({
name: "CoolUser22",
title: "Hover title",
url: "https://example.com/images/avatar.gif",
className: "mx_SomethingArbitrary",
}),
);
expect(container).toMatchSnapshot();
});
it("matches snapshot (avatar + click)", () => {
const { container } = render(
getComponent({
name: "CoolUser22",
title: "Hover title",
url: "https://example.com/images/avatar.gif",
className: "mx_SomethingArbitrary",
onClick: () => {},
}),
);
expect(container).toMatchSnapshot();
});
it("matches snapshot (no avatar)", () => {
const { container } = render(
getComponent({
name: "xX_Element_User_Xx",
title: ":kiss:",
defaultToInitialLetter: true,
className: "big-and-bold",
}),
);
expect(container).toMatchSnapshot();
});
it("matches snapshot (no avatar + click)", () => {
const { container } = render(
getComponent({
name: "xX_Element_User_Xx",
title: ":kiss:",
defaultToInitialLetter: true,
className: "big-and-bold",
onClick: () => {},
}),
);
expect(container).toMatchSnapshot();
});
it("uses fallback images", () => {
const images = [...Array(10)].map((_, i) => `https://example.com/images/${i}.webp`);
const { container } = render(
getComponent({
url: images[0],
urls: images.slice(1),
}),
);
for (const image of images) {
expect(container.querySelector("img")!.src).toBe(image);
failLoadingImg(container);
}
});
it("re-renders on reconnect", () => {
const primary = "https://example.com/image.jpeg";
const fallback = "https://example.com/fallback.png";
const { container } = render(
getComponent({
url: primary,
urls: [fallback],
}),
);
failLoadingImg(container);
expect(container.querySelector("img")!.src).toBe(fallback);
emitReconnect();
expect(container.querySelector("img")!.src).toBe(primary);
});
it("renders with an image", () => {
const url = "https://example.com/images/small/avatar.gif?size=realBig";
const { container } = render(getComponent({ url }));
const img = container.querySelector("img");
expect(img!.src).toBe(url);
});
it("renders the initial letter", () => {
const { container } = render(getComponent({ name: "Yellow", defaultToInitialLetter: true }));
const avatar = container.querySelector<HTMLSpanElement>(".mx_BaseAvatar_initial")!;
expect(avatar.innerHTML).toBe("Y");
});
it.each([{}, { name: "CoolUser22" }, { name: "XxElement_FanxX", defaultToInitialLetter: true }])(
"includes a click handler",
(props: Partial<Props>) => {
const onClick = jest.fn();
const { container } = render(
getComponent({
...props,
onClick,
}),
);
act(() => {
fireEvent.click(container.querySelector(".mx_BaseAvatar")!);
});
expect(onClick).toHaveBeenCalled();
},
);
});

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C. Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,25 +14,19 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { fireEvent, getByTestId, render } from "@testing-library/react"; import { getByTestId, render, waitFor } from "@testing-library/react";
import { mocked } from "jest-mock";
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client"; import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import React from "react"; import React from "react";
import { act } from "react-dom/test-utils";
import MemberAvatar from "../../../../src/components/views/avatars/MemberAvatar"; import MemberAvatar from "../../../../src/components/views/avatars/MemberAvatar";
import RoomContext from "../../../../src/contexts/RoomContext"; import RoomContext from "../../../../src/contexts/RoomContext";
import { mediaFromMxc } from "../../../../src/customisations/Media"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import { ViewUserPayload } from "../../../../src/dispatcher/payloads/ViewUserPayload";
import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
import SettingsStore from "../../../../src/settings/SettingsStore"; import SettingsStore from "../../../../src/settings/SettingsStore";
import { getRoomContext } from "../../../test-utils/room"; import { getRoomContext } from "../../../test-utils/room";
import { stubClient } from "../../../test-utils/test-utils"; import { stubClient } from "../../../test-utils/test-utils";
import { Action } from "../../../../src/dispatcher/actions";
type Props = React.ComponentPropsWithoutRef<typeof MemberAvatar>;
describe("MemberAvatar", () => { describe("MemberAvatar", () => {
const ROOM_ID = "roomId"; const ROOM_ID = "roomId";
@ -41,7 +35,7 @@ describe("MemberAvatar", () => {
let room: Room; let room: Room;
let member: RoomMember; let member: RoomMember;
function getComponent(props: Partial<Props>) { function getComponent(props) {
return ( return (
<RoomContext.Provider value={getRoomContext(room, {})}> <RoomContext.Provider value={getRoomContext(room, {})}>
<MemberAvatar member={null} width={35} height={35} {...props} /> <MemberAvatar member={null} width={35} height={35} {...props} />
@ -50,7 +44,10 @@ describe("MemberAvatar", () => {
} }
beforeEach(() => { beforeEach(() => {
mockClient = stubClient(); jest.clearAllMocks();
stubClient();
mockClient = mocked(MatrixClientPeg.get());
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", { room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
pendingEventOrdering: PendingEventOrdering.Detached, pendingEventOrdering: PendingEventOrdering.Detached,
@ -58,77 +55,22 @@ describe("MemberAvatar", () => {
member = new RoomMember(ROOM_ID, "@bob:example.org"); member = new RoomMember(ROOM_ID, "@bob:example.org");
jest.spyOn(room, "getMember").mockReturnValue(member); jest.spyOn(room, "getMember").mockReturnValue(member);
});
it("supports 'null' members", () => {
const { container } = render(getComponent({ member: null }));
expect(container.querySelector("img")).not.toBeNull();
});
it("matches the snapshot", () => {
jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue("http://placekitten.com/400/400"); jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue("http://placekitten.com/400/400");
const { container } = render(
getComponent({
member,
fallbackUserId: "Fallback User ID",
title: "Hover title",
style: {
color: "pink",
},
}),
);
expect(container).toMatchSnapshot();
}); });
it("shows an avatar for useOnlyCurrentProfiles", () => { it("shows an avatar for useOnlyCurrentProfiles", async () => {
jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue("http://placekitten.com/400/400"); jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => {
return settingName === "useOnlyCurrentProfiles";
SettingsStore.setValue("useOnlyCurrentProfiles", null, SettingLevel.DEVICE, true); });
const { container } = render(getComponent({})); const { container } = render(getComponent({}));
const avatar = getByTestId<HTMLImageElement>(container, "avatar-img"); let avatar: HTMLElement;
await waitFor(() => {
avatar = getByTestId(container, "avatar-img");
expect(avatar).toBeInTheDocument(); expect(avatar).toBeInTheDocument();
expect(avatar.getAttribute("src")).not.toBe("");
}); });
it("uses the member's configured avatar", () => { expect(avatar!.getAttribute("src")).not.toBe("");
const mxcUrl = "mxc://example.com/avatars/user.tiff";
jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue(mxcUrl);
const { container } = render(getComponent({ member }));
const img = container.querySelector("img");
expect(img).not.toBeNull();
expect(img!.src).toBe(mediaFromMxc(mxcUrl).srcHttp);
});
it("uses a fallback when the member has no avatar", () => {
jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue(undefined);
const { container } = render(getComponent({ member }));
const img = container.querySelector(".mx_BaseAvatar_image");
expect(img).not.toBeNull();
});
it("dispatches on click", () => {
const { container } = render(getComponent({ member, viewUserOnClick: true }));
const spy = jest.spyOn(defaultDispatcher, "dispatch");
act(() => {
fireEvent.click(container.querySelector(".mx_BaseAvatar")!);
});
expect(spy).toHaveBeenCalled();
const [payload] = spy.mock.lastCall!;
expect(payload).toStrictEqual<ViewUserPayload>({
action: Action.ViewUser,
member,
push: false,
});
}); });
}); });

View file

@ -39,7 +39,7 @@ describe("RoomAvatar", () => {
const dmRoomMap = new DMRoomMap(client); const dmRoomMap = new DMRoomMap(client);
jest.spyOn(dmRoomMap, "getUserIdForRoomId"); jest.spyOn(dmRoomMap, "getUserIdForRoomId");
jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap); jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
jest.spyOn(AvatarModule, "getColorForString"); jest.spyOn(AvatarModule, "defaultAvatarUrlForString");
}); });
afterAll(() => { afterAll(() => {
@ -48,14 +48,14 @@ describe("RoomAvatar", () => {
afterEach(() => { afterEach(() => {
mocked(DMRoomMap.shared().getUserIdForRoomId).mockReset(); mocked(DMRoomMap.shared().getUserIdForRoomId).mockReset();
mocked(AvatarModule.getColorForString).mockClear(); mocked(AvatarModule.defaultAvatarUrlForString).mockClear();
}); });
it("should render as expected for a Room", () => { it("should render as expected for a Room", () => {
const room = new Room("!room:example.com", client, client.getSafeUserId()); const room = new Room("!room:example.com", client, client.getSafeUserId());
room.name = "test room"; room.name = "test room";
expect(render(<RoomAvatar room={room} />).container).toMatchSnapshot(); expect(render(<RoomAvatar room={room} />).container).toMatchSnapshot();
expect(AvatarModule.getColorForString).toHaveBeenCalledWith(room.roomId); expect(AvatarModule.defaultAvatarUrlForString).toHaveBeenCalledWith(room.roomId);
}); });
it("should render as expected for a DM room", () => { it("should render as expected for a DM room", () => {
@ -64,7 +64,7 @@ describe("RoomAvatar", () => {
room.name = "DM room"; room.name = "DM room";
mocked(DMRoomMap.shared().getUserIdForRoomId).mockReturnValue(userId); mocked(DMRoomMap.shared().getUserIdForRoomId).mockReturnValue(userId);
expect(render(<RoomAvatar room={room} />).container).toMatchSnapshot(); expect(render(<RoomAvatar room={room} />).container).toMatchSnapshot();
expect(AvatarModule.getColorForString).toHaveBeenCalledWith(userId); expect(AvatarModule.defaultAvatarUrlForString).toHaveBeenCalledWith(userId);
}); });
it("should render as expected for a LocalRoom", () => { it("should render as expected for a LocalRoom", () => {
@ -73,6 +73,6 @@ describe("RoomAvatar", () => {
localRoom.name = "local test room"; localRoom.name = "local test room";
localRoom.targets.push(new DirectoryMember({ user_id: userId })); localRoom.targets.push(new DirectoryMember({ user_id: userId }));
expect(render(<RoomAvatar room={localRoom} />).container).toMatchSnapshot(); expect(render(<RoomAvatar room={localRoom} />).container).toMatchSnapshot();
expect(AvatarModule.getColorForString).toHaveBeenCalledWith(userId); expect(AvatarModule.defaultAvatarUrlForString).toHaveBeenCalledWith(userId);
}); });
}); });

View file

@ -1,72 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<BaseAvatar /> matches snapshot (avatar + click) 1`] = `
<div>
<img
alt="Avatar"
class="mx_AccessibleButton mx_BaseAvatar mx_BaseAvatar_image mx_SomethingArbitrary"
data-testid="avatar-img"
role="button"
src="https://example.com/images/avatar.gif"
style="width: 40px; height: 40px;"
tabindex="0"
title="Hover title"
/>
</div>
`;
exports[`<BaseAvatar /> matches snapshot (avatar) 1`] = `
<div>
<img
alt=""
class="mx_BaseAvatar mx_BaseAvatar_image mx_SomethingArbitrary"
data-testid="avatar-img"
src="https://example.com/images/avatar.gif"
style="width: 40px; height: 40px;"
title="Hover title"
/>
</div>
`;
exports[`<BaseAvatar /> matches snapshot (no avatar + click) 1`] = `
<div>
<span
aria-label="Avatar"
aria-live="off"
class="mx_AccessibleButton mx_BaseAvatar big-and-bold"
role="button"
style="width: 40px; height: 40px;"
tabindex="0"
>
<span
aria-hidden="true"
class="mx_BaseAvatar_image mx_BaseAvatar_initial"
data-testid="avatar-img"
style="background-color: rgb(13, 189, 139); width: 40px; height: 40px; font-size: 26px; line-height: 40px;"
title=":kiss:"
>
X
</span>
</span>
</div>
`;
exports[`<BaseAvatar /> matches snapshot (no avatar) 1`] = `
<div>
<span
class="mx_BaseAvatar big-and-bold"
role="presentation"
style="width: 40px; height: 40px;"
>
<span
aria-hidden="true"
class="mx_BaseAvatar_image mx_BaseAvatar_initial"
data-testid="avatar-img"
style="background-color: rgb(13, 189, 139); width: 40px; height: 40px; font-size: 26px; line-height: 40px;"
title=":kiss:"
>
X
</span>
</span>
</div>
`;

View file

@ -1,14 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MemberAvatar matches the snapshot 1`] = `
<div>
<img
alt=""
class="mx_BaseAvatar mx_BaseAvatar_image"
data-testid="avatar-img"
src="http://this.is.a.url//placekitten.com/400/400"
style="color: pink; width: 35px; height: 35px;"
title="Hover title"
/>
</div>
`;

View file

@ -5,16 +5,22 @@ exports[`RoomAvatar should render as expected for a DM room 1`] = `
<span <span
class="mx_BaseAvatar" class="mx_BaseAvatar"
role="presentation" role="presentation"
style="width: 36px; height: 36px;"
> >
<span <span
aria-hidden="true" aria-hidden="true"
class="mx_BaseAvatar_image mx_BaseAvatar_initial" class="mx_BaseAvatar_initial"
data-testid="avatar-img" style="font-size: 23.400000000000002px; width: 36px; line-height: 36px;"
style="background-color: rgb(13, 189, 139); width: 36px; height: 36px; font-size: 23.400000000000002px; line-height: 36px;"
> >
D D
</span> </span>
<img
alt=""
aria-hidden="true"
class="mx_BaseAvatar_image"
data-testid="avatar-img"
src="data:image/png;base64,00"
style="width: 36px; height: 36px;"
/>
</span> </span>
</div> </div>
`; `;
@ -24,16 +30,22 @@ exports[`RoomAvatar should render as expected for a LocalRoom 1`] = `
<span <span
class="mx_BaseAvatar" class="mx_BaseAvatar"
role="presentation" role="presentation"
style="width: 36px; height: 36px;"
> >
<span <span
aria-hidden="true" aria-hidden="true"
class="mx_BaseAvatar_image mx_BaseAvatar_initial" class="mx_BaseAvatar_initial"
data-testid="avatar-img" style="font-size: 23.400000000000002px; width: 36px; line-height: 36px;"
style="background-color: rgb(172, 59, 168); width: 36px; height: 36px; font-size: 23.400000000000002px; line-height: 36px;"
> >
L L
</span> </span>
<img
alt=""
aria-hidden="true"
class="mx_BaseAvatar_image"
data-testid="avatar-img"
src="data:image/png;base64,00"
style="width: 36px; height: 36px;"
/>
</span> </span>
</div> </div>
`; `;
@ -43,16 +55,22 @@ exports[`RoomAvatar should render as expected for a Room 1`] = `
<span <span
class="mx_BaseAvatar" class="mx_BaseAvatar"
role="presentation" role="presentation"
style="width: 36px; height: 36px;"
> >
<span <span
aria-hidden="true" aria-hidden="true"
class="mx_BaseAvatar_image mx_BaseAvatar_initial" class="mx_BaseAvatar_initial"
data-testid="avatar-img" style="font-size: 23.400000000000002px; width: 36px; line-height: 36px;"
style="background-color: rgb(172, 59, 168); width: 36px; height: 36px; font-size: 23.400000000000002px; line-height: 36px;"
> >
T T
</span> </span>
<img
alt=""
aria-hidden="true"
class="mx_BaseAvatar_image"
data-testid="avatar-img"
src="data:image/png;base64,00"
style="width: 36px; height: 36px;"
/>
</span> </span>
</div> </div>
`; `;

View file

@ -13,17 +13,23 @@ exports[`<BeaconMarker /> renders marker when beacon has location 1`] = `
<span <span
class="mx_BaseAvatar" class="mx_BaseAvatar"
role="presentation" role="presentation"
style="width: 36px; height: 36px;"
> >
<span <span
aria-hidden="true" aria-hidden="true"
class="mx_BaseAvatar_image mx_BaseAvatar_initial" class="mx_BaseAvatar_initial"
data-testid="avatar-img" style="font-size: 23.400000000000002px; width: 36px; line-height: 36px;"
style="background-color: rgb(172, 59, 168); width: 36px; height: 36px; font-size: 23.400000000000002px; line-height: 36px;"
title="@alice:server"
> >
A A
</span> </span>
<img
alt=""
aria-hidden="true"
class="mx_BaseAvatar_image"
data-testid="avatar-img"
src="data:image/png;base64,00"
style="width: 36px; height: 36px;"
title="@alice:server"
/>
</span> </span>
</div> </div>
</div> </div>

View file

@ -22,16 +22,22 @@ exports[`<RoomSummaryCard /> renders the room summary 1`] = `
<span <span
class="mx_BaseAvatar" class="mx_BaseAvatar"
role="presentation" role="presentation"
style="width: 54px; height: 54px;"
> >
<span <span
aria-hidden="true" aria-hidden="true"
class="mx_BaseAvatar_image mx_BaseAvatar_initial" class="mx_BaseAvatar_initial"
data-testid="avatar-img" style="font-size: 35.1px; width: 54px; line-height: 54px;"
style="background-color: rgb(13, 189, 139); width: 54px; height: 54px; font-size: 35.1px; line-height: 54px;"
> >
! !
</span> </span>
<img
alt=""
aria-hidden="true"
class="mx_BaseAvatar_image"
data-testid="avatar-img"
src="data:image/png;base64,00"
style="width: 54px; height: 54px;"
/>
</span> </span>
<div <div
class="mx_TextWithTooltip_target mx_RoomSummaryCard_e2ee" class="mx_TextWithTooltip_target mx_RoomSummaryCard_e2ee"

View file

@ -72,7 +72,7 @@ describe("RoomHeader (Enzyme)", () => {
// And there is no image avatar (because it's not set on this room) // And there is no image avatar (because it's not set on this room)
const image = findImg(rendered, ".mx_BaseAvatar_image"); const image = findImg(rendered, ".mx_BaseAvatar_image");
expect(image).toBeTruthy(); expect(image.prop("src")).toEqual("data:image/png;base64,00");
}); });
it("shows the room avatar in a room with 2 people", () => { it("shows the room avatar in a room with 2 people", () => {
@ -86,7 +86,7 @@ describe("RoomHeader (Enzyme)", () => {
// And there is no image avatar (because it's not set on this room) // And there is no image avatar (because it's not set on this room)
const image = findImg(rendered, ".mx_BaseAvatar_image"); const image = findImg(rendered, ".mx_BaseAvatar_image");
expect(image).toBeTruthy(); expect(image.prop("src")).toEqual("data:image/png;base64,00");
}); });
it("shows the room avatar in a room with >2 people", () => { it("shows the room avatar in a room with >2 people", () => {
@ -100,7 +100,7 @@ describe("RoomHeader (Enzyme)", () => {
// And there is no image avatar (because it's not set on this room) // And there is no image avatar (because it's not set on this room)
const image = findImg(rendered, ".mx_BaseAvatar_image"); const image = findImg(rendered, ".mx_BaseAvatar_image");
expect(image).toBeTruthy(); expect(image.prop("src")).toEqual("data:image/png;base64,00");
}); });
it("shows the room avatar in a DM with only ourselves", () => { it("shows the room avatar in a DM with only ourselves", () => {
@ -114,7 +114,7 @@ describe("RoomHeader (Enzyme)", () => {
// And there is no image avatar (because it's not set on this room) // And there is no image avatar (because it's not set on this room)
const image = findImg(rendered, ".mx_BaseAvatar_image"); const image = findImg(rendered, ".mx_BaseAvatar_image");
expect(image).toBeTruthy(); expect(image.prop("src")).toEqual("data:image/png;base64,00");
}); });
it("shows the user avatar in a DM with 2 people", () => { it("shows the user avatar in a DM with 2 people", () => {
@ -148,7 +148,7 @@ describe("RoomHeader (Enzyme)", () => {
// And there is no image avatar (because it's not set on this room) // And there is no image avatar (because it's not set on this room)
const image = findImg(rendered, ".mx_BaseAvatar_image"); const image = findImg(rendered, ".mx_BaseAvatar_image");
expect(image).toBeTruthy(); expect(image.prop("src")).toEqual("data:image/png;base64,00");
}); });
it("renders call buttons normally", () => { it("renders call buttons normally", () => {

View file

@ -161,16 +161,22 @@ exports[`<RoomPreviewBar /> with an invite without an invited email for a dm roo
<span <span
class="mx_BaseAvatar" class="mx_BaseAvatar"
role="presentation" role="presentation"
style="width: 36px; height: 36px;"
> >
<span <span
aria-hidden="true" aria-hidden="true"
class="mx_BaseAvatar_image mx_BaseAvatar_initial" class="mx_BaseAvatar_initial"
data-testid="avatar-img" style="font-size: 23.400000000000002px; width: 36px; line-height: 36px;"
style="background-color: rgb(172, 59, 168); width: 36px; height: 36px; font-size: 23.400000000000002px; line-height: 36px;"
> >
R R
</span> </span>
<img
alt=""
aria-hidden="true"
class="mx_BaseAvatar_image"
data-testid="avatar-img"
src="data:image/png;base64,00"
style="width: 36px; height: 36px;"
/>
</span> </span>
</p> </p>
<p> <p>
@ -230,16 +236,22 @@ exports[`<RoomPreviewBar /> with an invite without an invited email for a non-dm
<span <span
class="mx_BaseAvatar" class="mx_BaseAvatar"
role="presentation" role="presentation"
style="width: 36px; height: 36px;"
> >
<span <span
aria-hidden="true" aria-hidden="true"
class="mx_BaseAvatar_image mx_BaseAvatar_initial" class="mx_BaseAvatar_initial"
data-testid="avatar-img" style="font-size: 23.400000000000002px; width: 36px; line-height: 36px;"
style="background-color: rgb(172, 59, 168); width: 36px; height: 36px; font-size: 23.400000000000002px; line-height: 36px;"
> >
R R
</span> </span>
<img
alt=""
aria-hidden="true"
class="mx_BaseAvatar_image"
data-testid="avatar-img"
src="data:image/png;base64,00"
style="width: 36px; height: 36px;"
/>
</span> </span>
</p> </p>
<p> <p>

View file

@ -15,16 +15,22 @@ exports[`RoomTile should render the room 1`] = `
<span <span
class="mx_BaseAvatar" class="mx_BaseAvatar"
role="presentation" role="presentation"
style="width: 32px; height: 32px;"
> >
<span <span
aria-hidden="true" aria-hidden="true"
class="mx_BaseAvatar_image mx_BaseAvatar_initial" class="mx_BaseAvatar_initial"
data-testid="avatar-img" style="font-size: 20.8px; width: 32px; line-height: 32px;"
style="background-color: rgb(172, 59, 168); width: 32px; height: 32px; font-size: 20.8px; line-height: 32px;"
> >
! !
</span> </span>
<img
alt=""
aria-hidden="true"
class="mx_BaseAvatar_image"
data-testid="avatar-img"
src="data:image/png;base64,00"
style="width: 32px; height: 32px;"
/>
</span> </span>
</div> </div>
<div <div

View file

@ -1,45 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RoomPillPart matches snapshot (avatar) 1`] = `
<span
class="mx_Pill mx_RoomPill"
contenteditable="false"
spellcheck="false"
style="--avatar-background: url('http://this.is.a.url/www.example.com/avatars/room1.jpeg'); --avatar-letter: '';"
>
!room:example.com
</span>
`;
exports[`RoomPillPart matches snapshot (no avatar) 1`] = `
<span
class="mx_Pill mx_RoomPill"
contenteditable="false"
spellcheck="false"
style="--avatar-background: #ac3ba8; --avatar-letter: '!';"
>
!room:example.com
</span>
`;
exports[`UserPillPart matches snapshot (avatar) 1`] = `
<span
class="mx_UserPill mx_Pill"
contenteditable="false"
spellcheck="false"
style="--avatar-background: url('http://this.is.a.url/www.example.com/avatar.png'); --avatar-letter: '';"
>
DisplayName
</span>
`;
exports[`UserPillPart matches snapshot (no avatar) 1`] = `
<span
class="mx_UserPill mx_Pill"
contenteditable="false"
spellcheck="false"
style="--avatar-background: #ac3ba8; --avatar-letter: 'U';"
>
DisplayName
</span>
`;

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C. Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,11 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix"; import { EmojiPart, PlainPart } from "../../src/editor/parts";
import { EmojiPart, PartCreator, PlainPart } from "../../src/editor/parts";
import DMRoomMap from "../../src/utils/DMRoomMap";
import { stubClient } from "../test-utils";
import { createPartCreator } from "./mock"; import { createPartCreator } from "./mock";
describe("editor/parts", () => { describe("editor/parts", () => {
@ -44,67 +40,3 @@ describe("editor/parts", () => {
expect(() => part.toDOMNode()).not.toThrow(); expect(() => part.toDOMNode()).not.toThrow();
}); });
}); });
describe("UserPillPart", () => {
const roomId = "!room:example.com";
let client: MatrixClient;
let room: Room;
let creator: PartCreator;
beforeEach(() => {
client = stubClient();
room = new Room(roomId, client, "@me:example.com");
creator = new PartCreator(room, client);
});
it("matches snapshot (no avatar)", () => {
jest.spyOn(room, "getMember").mockReturnValue(new RoomMember(room.roomId, "@user:example.com"));
const pill = creator.userPill("DisplayName", "@user:example.com");
const el = pill.toDOMNode();
expect(el).toMatchSnapshot();
});
it("matches snapshot (avatar)", () => {
const member = new RoomMember(room.roomId, "@user:example.com");
jest.spyOn(room, "getMember").mockReturnValue(member);
jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue("mxc://www.example.com/avatar.png");
const pill = creator.userPill("DisplayName", "@user:example.com");
const el = pill.toDOMNode();
expect(el).toMatchSnapshot();
});
});
describe("RoomPillPart", () => {
const roomId = "!room:example.com";
let client: jest.Mocked<MatrixClient>;
let room: Room;
let creator: PartCreator;
beforeEach(() => {
client = stubClient() as jest.Mocked<MatrixClient>;
DMRoomMap.makeShared();
room = new Room(roomId, client, "@me:example.com");
client.getRoom.mockReturnValue(room);
creator = new PartCreator(room, client);
});
it("matches snapshot (no avatar)", () => {
jest.spyOn(room, "getMxcAvatarUrl").mockReturnValue(null);
const pill = creator.roomPill("super-secret clubhouse");
const el = pill.toDOMNode();
expect(el).toMatchSnapshot();
});
it("matches snapshot (avatar)", () => {
jest.spyOn(room, "getMxcAvatarUrl").mockReturnValue("mxc://www.example.com/avatars/room1.jpeg");
const pill = creator.roomPill("cool chat club");
const el = pill.toDOMNode();
expect(el).toMatchSnapshot();
});
});

File diff suppressed because one or more lines are too long