diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx index 76eea6cec0..62cf999c94 100644 --- a/src/components/views/avatars/BaseAvatar.tsx +++ b/src/components/views/avatars/BaseAvatar.tsx @@ -48,11 +48,11 @@ interface IProps { tabIndex?: number; } -const calculateUrls = (url, urls, lowBandwidth) => { +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: // imageUrls: [ props.url, ...props.urls ] - let _urls = []; + let _urls: string[] = []; if (!lowBandwidth) { _urls = urls || []; @@ -119,7 +119,7 @@ const BaseAvatar = (props: IProps) => { const [imageUrl, onError] = useImageUrl({ url, urls }); - if (!imageUrl && defaultToInitialLetter) { + if (!imageUrl && defaultToInitialLetter && name) { const initialLetter = AvatarLogic.getInitialLetter(name); const textNode = ( { width: toPx(width), height: toPx(height), }} - aria-hidden="true" /> + aria-hidden="true" + data-testid="avatar-img" /> ); if (onClick) { @@ -193,6 +194,7 @@ const BaseAvatar = (props: IProps) => { title={title} alt={_t("Avatar")} inputRef={inputRef} + data-testid="avatar-img" {...otherProps} /> ); } else { @@ -208,6 +210,7 @@ const BaseAvatar = (props: IProps) => { title={title} alt="" ref={inputRef} + data-testid="avatar-img" {...otherProps} /> ); } diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx index 99a028f82e..959fc84c47 100644 --- a/src/components/views/avatars/MemberAvatar.tsx +++ b/src/components/views/avatars/MemberAvatar.tsx @@ -77,27 +77,24 @@ export default function MemberAvatar({ ) ?? props.fallbackUserId; } } - const userId = member?.userId ?? props.fallbackUserId; - return ( - { - dis.dispatch({ - action: Action.ViewUser, - member: props.member, - push: card.isCard, - }); - } : props.onClick} - /> - ); + return { + dis.dispatch({ + action: Action.ViewUser, + member: props.member, + push: card.isCard, + }); + } : props.onClick} + />; } export class LegacyMemberAvatar extends React.Component { diff --git a/src/hooks/room/useRoomMemberProfile.ts b/src/hooks/room/useRoomMemberProfile.ts index 8afab49050..536572229a 100644 --- a/src/hooks/room/useRoomMemberProfile.ts +++ b/src/hooks/room/useRoomMemberProfile.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { RoomMember } from "matrix-js-sdk/src/models/room-member"; -import { useContext, useEffect, useState } from "react"; +import { useContext, useMemo } from "react"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import { useSettingValue } from "../useSettings"; @@ -29,18 +29,19 @@ export function useRoomMemberProfile({ member?: RoomMember | null; forceHistorical?: boolean; }): RoomMember | undefined | null { - const [member, setMember] = useState(propMember); - const context = useContext(RoomContext); const useOnlyCurrentProfiles = useSettingValue("useOnlyCurrentProfiles"); - useEffect(() => { + const member = useMemo(() => { const threadContexts = [TimelineRenderingType.ThreadsList, TimelineRenderingType.Thread]; - if ((propMember && !forceHistorical && useOnlyCurrentProfiles) - || threadContexts.includes(context?.timelineRenderingType)) { - setMember(context?.room?.getMember(userId)); + if ((!forceHistorical && useOnlyCurrentProfiles) + || threadContexts.includes(context.timelineRenderingType)) { + const currentMember = context.room?.getMember(userId); + if (currentMember) return currentMember; } - }, [forceHistorical, propMember, context.room, context?.timelineRenderingType, useOnlyCurrentProfiles, userId]); + + return propMember; + }, [forceHistorical, propMember, context.room, context.timelineRenderingType, useOnlyCurrentProfiles, userId]); return member; } diff --git a/test/components/structures/__snapshots__/RoomView-test.tsx.snap b/test/components/structures/__snapshots__/RoomView-test.tsx.snap index 33f44f9a37..52804a5136 100644 --- a/test/components/structures/__snapshots__/RoomView-test.tsx.snap +++ b/test/components/structures/__snapshots__/RoomView-test.tsx.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`RoomView for a local room in state CREATING should match the snapshot 1`] = `"
@user:example.com
We're creating a room with @user:example.com
"`; +exports[`RoomView for a local room in state CREATING should match the snapshot 1`] = `"
@user:example.com
We're creating a room with @user:example.com
"`; -exports[`RoomView for a local room in state ERROR should match the snapshot 1`] = `"
@user:example.com
  1. End-to-end encryption isn't enabled
    Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites.

    @user:example.com

    Send your first message to invite @user:example.com to chat

!
Some of your messages have not been sent
Retry
"`; +exports[`RoomView for a local room in state ERROR should match the snapshot 1`] = `"
@user:example.com
  1. End-to-end encryption isn't enabled
    Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites.

    @user:example.com

    Send your first message to invite @user:example.com to chat

!
Some of your messages have not been sent
Retry
"`; -exports[`RoomView for a local room in state NEW should match the snapshot 1`] = `"
@user:example.com
  1. End-to-end encryption isn't enabled
    Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites.

    @user:example.com

    Send your first message to invite @user:example.com to chat


"`; +exports[`RoomView for a local room in state NEW should match the snapshot 1`] = `"
@user:example.com
  1. End-to-end encryption isn't enabled
    Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites.

    @user:example.com

    Send your first message to invite @user:example.com to chat


"`; -exports[`RoomView for a local room in state NEW that is encrypted should match the snapshot 1`] = `"
@user:example.com
    Encryption enabled
    Messages in this chat will be end-to-end encrypted.
  1. @user:example.com

    Send your first message to invite @user:example.com to chat


"`; +exports[`RoomView for a local room in state NEW that is encrypted should match the snapshot 1`] = `"
@user:example.com
    Encryption enabled
    Messages in this chat will be end-to-end encrypted.
  1. @user:example.com

    Send your first message to invite @user:example.com to chat


"`; diff --git a/test/components/views/avatars/MemberAvatar-test.tsx b/test/components/views/avatars/MemberAvatar-test.tsx new file mode 100644 index 0000000000..4ce75636e0 --- /dev/null +++ b/test/components/views/avatars/MemberAvatar-test.tsx @@ -0,0 +1,79 @@ +/* +Copyright 2022 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 { getByTestId, render, waitFor } from "@testing-library/react"; +import { mocked } from "jest-mock"; +import { MatrixClient, 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 MemberAvatar from "../../../../src/components/views/avatars/MemberAvatar"; +import RoomContext from "../../../../src/contexts/RoomContext"; +import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; +import SettingsStore from "../../../../src/settings/SettingsStore"; +import { getRoomContext } from "../../../test-utils/room"; +import { stubClient } from "../../../test-utils/test-utils"; + +describe("MemberAvatar", () => { + const ROOM_ID = "roomId"; + + let mockClient: MatrixClient; + let room: Room; + let member: RoomMember; + + function getComponent(props) { + return + + ; + } + + beforeEach(() => { + jest.clearAllMocks(); + + stubClient(); + mockClient = mocked(MatrixClientPeg.get()); + + room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", { + pendingEventOrdering: PendingEventOrdering.Detached, + }); + + member = new RoomMember(ROOM_ID, "@bob:example.org"); + jest.spyOn(room, "getMember").mockReturnValue(member); + jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue("http://placekitten.com/400/400"); + }); + + it("shows an avatar for useOnlyCurrentProfiles", async () => { + jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => { + return settingName === "useOnlyCurrentProfiles"; + }); + + const { container } = render(getComponent({})); + + let avatar: HTMLElement; + await waitFor(() => { + avatar = getByTestId(container, "avatar-img"); + expect(avatar).toBeInTheDocument(); + }); + + expect(avatar!.getAttribute("src")).not.toBe(""); + }); +}); diff --git a/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap b/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap index 21e471ec42..ad62c932cb 100644 --- a/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap +++ b/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap @@ -262,6 +262,7 @@ exports[` renders marker when beacon has location 1`] = ` alt="" aria-hidden="true" className="mx_BaseAvatar_image" + data-testid="avatar-img" onError={[Function]} src="data:image/png;base64,00" style={ diff --git a/test/components/views/beacon/__snapshots__/DialogSidebar-test.tsx.snap b/test/components/views/beacon/__snapshots__/DialogSidebar-test.tsx.snap index 22199fbc91..9770bfc61b 100644 --- a/test/components/views/beacon/__snapshots__/DialogSidebar-test.tsx.snap +++ b/test/components/views/beacon/__snapshots__/DialogSidebar-test.tsx.snap @@ -31,23 +31,12 @@ exports[` renders sidebar correctly with beacons 1`] = `
  • - - +
    diff --git a/test/components/views/messages/TextualBody-test.tsx b/test/components/views/messages/TextualBody-test.tsx index f07fce014a..0ca41b8fa7 100644 --- a/test/components/views/messages/TextualBody-test.tsx +++ b/test/components/views/messages/TextualBody-test.tsx @@ -335,7 +335,7 @@ describe("", () => { '?via=example.com&via=bob.com"' + '>' + + 'style="width: 16px; height: 16px;" alt="" data-testid="avatar-img" aria-hidden="true">' + 'room name with vias', ); }); diff --git a/test/components/views/messages/__snapshots__/TextualBody-test.tsx.snap b/test/components/views/messages/__snapshots__/TextualBody-test.tsx.snap index 1cd0b17ff8..6f8070eef6 100644 --- a/test/components/views/messages/__snapshots__/TextualBody-test.tsx.snap +++ b/test/components/views/messages/__snapshots__/TextualBody-test.tsx.snap @@ -14,4 +14,4 @@ exports[` renders formatted m.text correctly pills do not appear " `; -exports[` renders formatted m.text correctly pills get injected correctly into the DOM 1`] = `"Hey Member"`; +exports[` renders formatted m.text correctly pills get injected correctly into the DOM 1`] = `"Hey Member"`; diff --git a/test/components/views/rooms/__snapshots__/RoomPreviewBar-test.tsx.snap b/test/components/views/rooms/__snapshots__/RoomPreviewBar-test.tsx.snap index 6a455dc148..f35467e1ef 100644 --- a/test/components/views/rooms/__snapshots__/RoomPreviewBar-test.tsx.snap +++ b/test/components/views/rooms/__snapshots__/RoomPreviewBar-test.tsx.snap @@ -173,6 +173,7 @@ exports[` with an invite without an invited email for a dm roo alt="" aria-hidden="true" class="mx_BaseAvatar_image" + data-testid="avatar-img" src="data:image/png;base64,00" style="width: 36px; height: 36px;" /> @@ -247,6 +248,7 @@ exports[` with an invite without an invited email for a non-dm alt="" aria-hidden="true" class="mx_BaseAvatar_image" + data-testid="avatar-img" src="data:image/png;base64,00" style="width: 36px; height: 36px;" />