Use the same avatar colour when creating 1:1 DM rooms (#9850)
This commit is contained in:
parent
ecfd1736e5
commit
ab9152044c
8 changed files with 215 additions and 60 deletions
|
@ -29,6 +29,7 @@ import * as Avatar from "../../../Avatar";
|
||||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||||
import { mediaFromMxc } from "../../../customisations/Media";
|
import { mediaFromMxc } from "../../../customisations/Media";
|
||||||
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
||||||
|
import { LocalRoom } from "../../../models/LocalRoom";
|
||||||
|
|
||||||
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
|
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
|
||||||
// Room may be left unset here, but if it is,
|
// Room may be left unset here, but if it is,
|
||||||
|
@ -117,13 +118,26 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
|
||||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
|
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private get roomIdName(): string | undefined {
|
||||||
|
const room = this.props.room;
|
||||||
|
|
||||||
|
if (room) {
|
||||||
|
const dmMapUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
||||||
|
// If the room is a DM, we use the other user's ID for the color hash
|
||||||
|
// in order to match the room avatar with their avatar
|
||||||
|
if (dmMapUserId) return dmMapUserId;
|
||||||
|
|
||||||
|
if (room instanceof LocalRoom && room.targets.length === 1) {
|
||||||
|
return room.targets[0].userId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.room?.roomId || this.props.oobData?.roomId;
|
||||||
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { room, oobData, viewAvatarOnClick, onClick, className, ...otherProps } = this.props;
|
const { room, oobData, viewAvatarOnClick, onClick, className, ...otherProps } = this.props;
|
||||||
|
|
||||||
const roomName = room?.name ?? oobData.name;
|
const roomName = room?.name ?? oobData.name;
|
||||||
// If the room is a DM, we use the other user's ID for the color hash
|
|
||||||
// in order to match the room avatar with their avatar
|
|
||||||
const idName = room ? DMRoomMap.shared().getUserIdForRoomId(room.roomId) ?? room.roomId : oobData.roomId;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseAvatar
|
<BaseAvatar
|
||||||
|
@ -132,7 +146,7 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
|
||||||
mx_RoomAvatar_isSpaceRoom: (room?.getType() ?? this.props.oobData?.roomType) === RoomType.Space,
|
mx_RoomAvatar_isSpaceRoom: (room?.getType() ?? this.props.oobData?.roomType) === RoomType.Space,
|
||||||
})}
|
})}
|
||||||
name={roomName}
|
name={roomName}
|
||||||
idName={idName}
|
idName={this.roomIdName}
|
||||||
urls={this.state.urls}
|
urls={this.state.urls}
|
||||||
onClick={viewAvatarOnClick && this.state.urls[0] ? this.onRoomAvatarClick : onClick}
|
onClick={viewAvatarOnClick && this.state.urls[0] ? this.onRoomAvatarClick : onClick}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -40,7 +40,6 @@ describe("<ForgotPassword>", () => {
|
||||||
let onComplete: () => void;
|
let onComplete: () => void;
|
||||||
let onLoginClick: () => void;
|
let onLoginClick: () => void;
|
||||||
let renderResult: RenderResult;
|
let renderResult: RenderResult;
|
||||||
let restoreConsole: () => void;
|
|
||||||
|
|
||||||
const typeIntoField = async (label: string, value: string): Promise<void> => {
|
const typeIntoField = async (label: string, value: string): Promise<void> => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
@ -63,14 +62,14 @@ describe("<ForgotPassword>", () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
filterConsole(
|
||||||
restoreConsole = filterConsole(
|
// not implemented by js-dom https://github.com/jsdom/jsdom/issues/1937
|
||||||
// not implemented by js-dom https://github.com/jsdom/jsdom/issues/1937
|
"Not implemented: HTMLFormElement.prototype.requestSubmit",
|
||||||
"Not implemented: HTMLFormElement.prototype.requestSubmit",
|
// not of interested for this test
|
||||||
// not of interested for this test
|
"Starting load of AsyncWrapper for modal",
|
||||||
"Starting load of AsyncWrapper for modal",
|
);
|
||||||
);
|
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
client = stubClient();
|
client = stubClient();
|
||||||
mocked(createClient).mockReturnValue(client);
|
mocked(createClient).mockReturnValue(client);
|
||||||
|
|
||||||
|
@ -87,7 +86,6 @@ describe("<ForgotPassword>", () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
// clean up modals
|
// clean up modals
|
||||||
Modal.closeCurrentModal("force");
|
Modal.closeCurrentModal("force");
|
||||||
restoreConsole?.();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
|
78
test/components/views/avatars/RoomAvatar-test.tsx
Normal file
78
test/components/views/avatars/RoomAvatar-test.tsx
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
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 React from "react";
|
||||||
|
import { render } from "@testing-library/react";
|
||||||
|
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { mocked } from "jest-mock";
|
||||||
|
|
||||||
|
import RoomAvatar from "../../../../src/components/views/avatars/RoomAvatar";
|
||||||
|
import { filterConsole, stubClient } from "../../../test-utils";
|
||||||
|
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||||
|
import { LocalRoom } from "../../../../src/models/LocalRoom";
|
||||||
|
import * as AvatarModule from "../../../../src/Avatar";
|
||||||
|
import { DirectoryMember } from "../../../../src/utils/direct-messages";
|
||||||
|
|
||||||
|
describe("RoomAvatar", () => {
|
||||||
|
let client: MatrixClient;
|
||||||
|
|
||||||
|
filterConsole(
|
||||||
|
// unrelated for this test
|
||||||
|
"Room !room:example.com does not have an m.room.create event",
|
||||||
|
);
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
client = stubClient();
|
||||||
|
const dmRoomMap = new DMRoomMap(client);
|
||||||
|
jest.spyOn(dmRoomMap, "getUserIdForRoomId");
|
||||||
|
jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
|
||||||
|
jest.spyOn(AvatarModule, "defaultAvatarUrlForString");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mocked(DMRoomMap.shared().getUserIdForRoomId).mockReset();
|
||||||
|
mocked(AvatarModule.defaultAvatarUrlForString).mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render as expected for a Room", () => {
|
||||||
|
const room = new Room("!room:example.com", client, client.getSafeUserId());
|
||||||
|
room.name = "test room";
|
||||||
|
expect(render(<RoomAvatar room={room} />).container).toMatchSnapshot();
|
||||||
|
expect(AvatarModule.defaultAvatarUrlForString).toHaveBeenCalledWith(room.roomId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render as expected for a DM room", () => {
|
||||||
|
const userId = "@dm_user@example.com";
|
||||||
|
const room = new Room("!room:example.com", client, client.getSafeUserId());
|
||||||
|
room.name = "DM room";
|
||||||
|
mocked(DMRoomMap.shared().getUserIdForRoomId).mockReturnValue(userId);
|
||||||
|
expect(render(<RoomAvatar room={room} />).container).toMatchSnapshot();
|
||||||
|
expect(AvatarModule.defaultAvatarUrlForString).toHaveBeenCalledWith(userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render as expected for a LocalRoom", () => {
|
||||||
|
const userId = "@local_room_user@example.com";
|
||||||
|
const localRoom = new LocalRoom("!room:example.com", client, client.getSafeUserId());
|
||||||
|
localRoom.name = "local test room";
|
||||||
|
localRoom.targets.push(new DirectoryMember({ user_id: userId }));
|
||||||
|
expect(render(<RoomAvatar room={localRoom} />).container).toMatchSnapshot();
|
||||||
|
expect(AvatarModule.defaultAvatarUrlForString).toHaveBeenCalledWith(userId);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,76 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`RoomAvatar should render as expected for a DM room 1`] = `
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
class="mx_BaseAvatar"
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mx_BaseAvatar_initial"
|
||||||
|
style="font-size: 23.400000000000002px; width: 36px; line-height: 36px;"
|
||||||
|
>
|
||||||
|
D
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`RoomAvatar should render as expected for a LocalRoom 1`] = `
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
class="mx_BaseAvatar"
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mx_BaseAvatar_initial"
|
||||||
|
style="font-size: 23.400000000000002px; width: 36px; line-height: 36px;"
|
||||||
|
>
|
||||||
|
L
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`RoomAvatar should render as expected for a Room 1`] = `
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
class="mx_BaseAvatar"
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mx_BaseAvatar_initial"
|
||||||
|
style="font-size: 23.400000000000002px; width: 36px; line-height: 36px;"
|
||||||
|
>
|
||||||
|
T
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
`;
|
|
@ -75,20 +75,19 @@ describe("RoomTile", () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
let client: Mocked<MatrixClient>;
|
let client: Mocked<MatrixClient>;
|
||||||
let restoreConsole: () => void;
|
|
||||||
let voiceBroadcastInfoEvent: MatrixEvent;
|
let voiceBroadcastInfoEvent: MatrixEvent;
|
||||||
let room: Room;
|
let room: Room;
|
||||||
let renderResult: RenderResult;
|
let renderResult: RenderResult;
|
||||||
let sdkContext: TestSdkContext;
|
let sdkContext: TestSdkContext;
|
||||||
|
|
||||||
|
filterConsole(
|
||||||
|
// irrelevant for this test
|
||||||
|
"Room !1:example.org does not have an m.room.create event",
|
||||||
|
);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sdkContext = new TestSdkContext();
|
sdkContext = new TestSdkContext();
|
||||||
|
|
||||||
restoreConsole = filterConsole(
|
|
||||||
// irrelevant for this test
|
|
||||||
"Room !1:example.org does not have an m.room.create event",
|
|
||||||
);
|
|
||||||
|
|
||||||
client = mocked(stubClient());
|
client = mocked(stubClient());
|
||||||
sdkContext.client = client;
|
sdkContext.client = client;
|
||||||
DMRoomMap.makeShared();
|
DMRoomMap.makeShared();
|
||||||
|
@ -105,7 +104,6 @@ describe("RoomTile", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
restoreConsole();
|
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -33,8 +33,6 @@ jest.mock("../../../../src/components/structures/HomePage", () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe("UserOnboardingPage", () => {
|
describe("UserOnboardingPage", () => {
|
||||||
let restoreConsole: () => void;
|
|
||||||
|
|
||||||
const renderComponent = async (): Promise<RenderResult> => {
|
const renderComponent = async (): Promise<RenderResult> => {
|
||||||
const renderResult = render(<UserOnboardingPage />);
|
const renderResult = render(<UserOnboardingPage />);
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
@ -43,12 +41,10 @@ describe("UserOnboardingPage", () => {
|
||||||
return renderResult;
|
return renderResult;
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeAll(() => {
|
filterConsole(
|
||||||
restoreConsole = filterConsole(
|
// unrelated for this test
|
||||||
// unrelated for this test
|
"could not update user onboarding context",
|
||||||
"could not update user onboarding context",
|
);
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
stubClient();
|
stubClient();
|
||||||
|
@ -60,10 +56,6 @@ describe("UserOnboardingPage", () => {
|
||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
restoreConsole();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when the user registered before the cutoff date", () => {
|
describe("when the user registered before the cutoff date", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.spyOn(MatrixClientPeg, "userRegisteredAfter").mockReturnValue(false);
|
jest.spyOn(MatrixClientPeg, "userRegisteredAfter").mockReturnValue(false);
|
||||||
|
|
|
@ -16,36 +16,39 @@ limitations under the License.
|
||||||
|
|
||||||
type FilteredConsole = Pick<Console, "log" | "error" | "info" | "debug" | "warn">;
|
type FilteredConsole = Pick<Console, "log" | "error" | "info" | "debug" | "warn">;
|
||||||
|
|
||||||
const originalFunctions: FilteredConsole = {
|
|
||||||
log: console.log,
|
|
||||||
error: console.error,
|
|
||||||
info: console.info,
|
|
||||||
debug: console.debug,
|
|
||||||
warn: console.warn,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows to filter out specific messages in console.*.
|
* Allows to filter out specific messages in console.*.
|
||||||
|
* Call this from any describe block.
|
||||||
|
* Automagically restores the original function by implementing an afterAll hook.
|
||||||
*
|
*
|
||||||
* @param ignoreList Messages to be filtered
|
* @param ignoreList Messages to be filtered
|
||||||
* @returns function to restore the console
|
|
||||||
*/
|
*/
|
||||||
export const filterConsole = (...ignoreList: string[]): (() => void) => {
|
export const filterConsole = (...ignoreList: string[]): void => {
|
||||||
for (const [key, originalFunction] of Object.entries(originalFunctions)) {
|
const originalFunctions: FilteredConsole = {
|
||||||
window.console[key as keyof FilteredConsole] = (...data: any[]) => {
|
log: console.log,
|
||||||
const message = data?.[0]?.message || data?.[0];
|
error: console.error,
|
||||||
|
info: console.info,
|
||||||
|
debug: console.debug,
|
||||||
|
warn: console.warn,
|
||||||
|
};
|
||||||
|
|
||||||
if (typeof message === "string" && ignoreList.some((i) => message.includes(i))) {
|
beforeAll(() => {
|
||||||
return;
|
for (const [key, originalFunction] of Object.entries(originalFunctions)) {
|
||||||
}
|
window.console[key as keyof FilteredConsole] = (...data: any[]) => {
|
||||||
|
const message = data?.[0]?.message || data?.[0];
|
||||||
|
|
||||||
originalFunction(...data);
|
if (typeof message === "string" && ignoreList.some((i) => message.includes(i))) {
|
||||||
};
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
originalFunction(...data);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
for (const [key, originalFunction] of Object.entries(originalFunctions)) {
|
for (const [key, originalFunction] of Object.entries(originalFunctions)) {
|
||||||
window.console[key as keyof FilteredConsole] = originalFunction;
|
window.console[key as keyof FilteredConsole] = originalFunction;
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -62,7 +62,6 @@ describe("VoiceBroadcastRecordingPip", () => {
|
||||||
let infoEvent: MatrixEvent;
|
let infoEvent: MatrixEvent;
|
||||||
let recording: VoiceBroadcastRecording;
|
let recording: VoiceBroadcastRecording;
|
||||||
let renderResult: RenderResult;
|
let renderResult: RenderResult;
|
||||||
let restoreConsole: () => void;
|
|
||||||
|
|
||||||
const renderPip = async (state: VoiceBroadcastInfoState) => {
|
const renderPip = async (state: VoiceBroadcastInfoState) => {
|
||||||
infoEvent = mkVoiceBroadcastInfoStateEvent(roomId, state, client.getUserId() || "", client.getDeviceId() || "");
|
infoEvent = mkVoiceBroadcastInfoStateEvent(roomId, state, client.getUserId() || "", client.getDeviceId() || "");
|
||||||
|
@ -85,6 +84,8 @@ describe("VoiceBroadcastRecordingPip", () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
filterConsole("Starting load of AsyncWrapper for modal");
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
client = stubClient();
|
client = stubClient();
|
||||||
mocked(requestMediaPermissions).mockResolvedValue({
|
mocked(requestMediaPermissions).mockResolvedValue({
|
||||||
|
@ -105,11 +106,6 @@ describe("VoiceBroadcastRecordingPip", () => {
|
||||||
[MediaDeviceKindEnum.VideoInput]: [],
|
[MediaDeviceKindEnum.VideoInput]: [],
|
||||||
});
|
});
|
||||||
jest.spyOn(MediaDeviceHandler.instance, "setDevice").mockImplementation();
|
jest.spyOn(MediaDeviceHandler.instance, "setDevice").mockImplementation();
|
||||||
restoreConsole = filterConsole("Starting load of AsyncWrapper for modal");
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
restoreConsole();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when rendering a started recording", () => {
|
describe("when rendering a started recording", () => {
|
||||||
|
|
Loading…
Reference in a new issue