Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Delabs Show current avatar and name for users in message history #8764

Merged
merged 14 commits into from
Jul 4, 2022
Merged
2 changes: 2 additions & 0 deletions cypress/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ import type {
} from "matrix-js-sdk/src/matrix";
import type { MatrixDispatcher } from "../src/dispatcher/dispatcher";
import type PerformanceMonitor from "../src/performance";
import type SettingsStore from "../src/settings/SettingsStore";

declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface ApplicationWindow {
mxSettingsStore: typeof SettingsStore;
mxMatrixClientPeg: {
matrixClient?: MatrixClient;
};
Expand Down
145 changes: 145 additions & 0 deletions cypress/integration/14-timeline/timeline.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
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.
*/

/// <reference types="cypress" />

import { MessageEvent } from "matrix-events-sdk";

import type { ISendEventResponse } from "matrix-js-sdk/src/@types/requests";
import type { EventType } from "matrix-js-sdk/src/@types/event";
import type { MatrixClient } from "matrix-js-sdk/src/client";
import { SynapseInstance } from "../../plugins/synapsedocker";
import { SettingLevel } from "../../../src/settings/SettingLevel";
import Chainable = Cypress.Chainable;

// The avatar size used in the timeline
const AVATAR_SIZE = 30;
// The resize method used in the timeline
const AVATAR_RESIZE_METHOD = "crop";

const ROOM_NAME = "Test room";
const OLD_AVATAR = "avatar_image1";
const NEW_AVATAR = "avatar_image2";
const OLD_NAME = "Alan";
const NEW_NAME = "Alan (away)";

const getEventTilesWithBodies = (): Chainable<JQuery> => {
return cy.get(".mx_EventTile").filter((_i, e) => e.getElementsByClassName("mx_EventTile_body").length > 0);
};

const expectDisplayName = (e: JQuery<HTMLElement>, displayName: string): void => {
expect(e.find(".mx_DisambiguatedProfile_displayName").text()).to.equal(displayName);
};

const expectAvatar = (e: JQuery<HTMLElement>, avatarUrl: string): void => {
cy.getClient().then((cli: MatrixClient) => {
expect(e.find(".mx_BaseAvatar_image").attr("src")).to.equal(
// eslint-disable-next-line no-restricted-properties
cli.mxcUrlToHttp(avatarUrl, AVATAR_SIZE, AVATAR_SIZE, AVATAR_RESIZE_METHOD),
);
});
};

const sendEvent = (roomId: string): Chainable<ISendEventResponse> => {
return cy.sendEvent(
roomId,
null,
"m.room.message" as EventType,
MessageEvent.from("Message").serialize().content,
);
};

describe("Timeline", () => {
let synapse: SynapseInstance;

let roomId: string;

let oldAvatarUrl: string;
let newAvatarUrl: string;

describe("useOnlyCurrentProfiles", () => {
beforeEach(() => {
cy.startSynapse("default").then(data => {
synapse = data;
cy.initTestUser(synapse, OLD_NAME).then(() =>
cy.window({ log: false }).then(() => {
cy.createRoom({ name: ROOM_NAME }).then(_room1Id => {
roomId = _room1Id;
});
}),
).then(() => {
cy.uploadContent(OLD_AVATAR).then((url) => {
oldAvatarUrl = url;
cy.setAvatarUrl(url);
});
}).then(() => {
cy.uploadContent(NEW_AVATAR).then((url) => {
newAvatarUrl = url;
});
});
});
});

afterEach(() => {
cy.stopSynapse(synapse);
});

it("should show historical profiles if disabled", () => {
cy.setSettingValue("useOnlyCurrentProfiles", null, SettingLevel.ACCOUNT, false);
sendEvent(roomId);
cy.setDisplayName("Alan (away)");
cy.setAvatarUrl(newAvatarUrl);
// XXX: If we send the second event too quickly, there won't be
// enough time for the client to register the profile change
cy.wait(500);
sendEvent(roomId);
cy.viewRoomByName(ROOM_NAME);

const events = getEventTilesWithBodies();

events.should("have.length", 2);
events.each((e, i) => {
if (i === 0) {
expectDisplayName(e, OLD_NAME);
expectAvatar(e, oldAvatarUrl);
} else if (i === 1) {
expectDisplayName(e, NEW_NAME);
expectAvatar(e, newAvatarUrl);
}
});
});

it("should not show historical profiles if enabled", () => {
cy.setSettingValue("useOnlyCurrentProfiles", null, SettingLevel.ACCOUNT, true);
sendEvent(roomId);
cy.setDisplayName(NEW_NAME);
cy.setAvatarUrl(newAvatarUrl);
// XXX: If we send the second event too quickly, there won't be
// enough time for the client to register the profile change
cy.wait(500);
sendEvent(roomId);
cy.viewRoomByName(ROOM_NAME);

const events = getEventTilesWithBodies();

events.should("have.length", 2);
events.each((e) => {
expectDisplayName(e, NEW_NAME);
expectAvatar(e, newAvatarUrl);
});
});
});
});
92 changes: 91 additions & 1 deletion cypress/support/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ limitations under the License.

/// <reference types="cypress" />

import type { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests";
import type { FileType, UploadContentResponseType } from "matrix-js-sdk/src/http-api";
import type { IAbortablePromise } from "matrix-js-sdk/src/@types/partials";
import type { ICreateRoomOpts, ISendEventResponse, IUploadOpts } from "matrix-js-sdk/src/@types/requests";
import type { MatrixClient } from "matrix-js-sdk/src/client";
import type { Room } from "matrix-js-sdk/src/models/room";
import type { IContent } from "matrix-js-sdk/src/models/event";
import Chainable = Cypress.Chainable;

declare global {
Expand Down Expand Up @@ -53,6 +56,64 @@ declare global {
* @param data The data to store.
*/
setAccountData(type: string, data: object): Chainable<{}>;
/**
* @param {string} roomId
* @param {string} threadId
* @param {string} eventType
* @param {Object} content
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
sendEvent(
roomId: string,
threadId: string | null,
eventType: string,
content: IContent
): Chainable<ISendEventResponse>;
/**
* @param {string} name
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: {} an empty object.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
setDisplayName(name: string): Chainable<{}>;
/**
* @param {string} url
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: {} an empty object.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
setAvatarUrl(url: string): Chainable<{}>;
/**
* Upload a file to the media repository on the homeserver.
*
* @param {object} file The object to upload. On a browser, something that
* can be sent to XMLHttpRequest.send (typically a File). Under node.js,
* a a Buffer, String or ReadStream.
*/
uploadContent<O extends IUploadOpts>(
file: FileType,
opts?: O,
): IAbortablePromise<UploadContentResponseType<O>>;
/**
* Turn an MXC URL into an HTTP one. <strong>This method is experimental and
* may change.</strong>
* @param {string} mxcUrl The MXC URL
* @param {Number} width The desired width of the thumbnail.
* @param {Number} height The desired height of the thumbnail.
* @param {string} resizeMethod The thumbnail resize method to use, either
* "crop" or "scale".
* @param {Boolean} allowDirectLinks If true, return any non-mxc URLs
* directly. Fetching such URLs will leak information about the user to
* anyone they share a room with. If false, will return null for such URLs.
* @return {?string} the avatar URL or null.
*/
mxcUrlToHttp(
mxcUrl: string,
width?: number,
height?: number,
resizeMethod?: string,
allowDirectLinks?: boolean,
): string | null;
/**
* Gets the list of DMs with a given user
* @param userId The ID of the user
Expand Down Expand Up @@ -120,6 +181,35 @@ Cypress.Commands.add("setAccountData", (type: string, data: object): Chainable<{
});
});

Cypress.Commands.add("sendEvent", (
roomId: string,
threadId: string | null,
eventType: string,
content: IContent,
): Chainable<ISendEventResponse> => {
return cy.getClient().then(async (cli: MatrixClient) => {
return cli.sendEvent(roomId, threadId, eventType, content);
});
});

Cypress.Commands.add("setDisplayName", (name: string): Chainable<{}> => {
return cy.getClient().then(async (cli: MatrixClient) => {
return cli.setDisplayName(name);
});
});

Cypress.Commands.add("uploadContent", (file: FileType): Chainable<{}> => {
return cy.getClient().then(async (cli: MatrixClient) => {
return cli.uploadContent(file);
});
});

Cypress.Commands.add("setAvatarUrl", (url: string): Chainable<{}> => {
return cy.getClient().then(async (cli: MatrixClient) => {
return cli.setAvatarUrl(url);
});
});

Cypress.Commands.add("bootstrapCrossSigning", () => {
cy.window({ log: false }).then(win => {
win.mxMatrixClientPeg.matrixClient.bootstrapCrossSigning({
Expand Down
55 changes: 55 additions & 0 deletions cypress/support/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,17 @@ limitations under the License.
/// <reference types="cypress" />

import Chainable = Cypress.Chainable;
import type { SettingLevel } from "../../src/settings/SettingLevel";
import type SettingsStore from "../../src/settings/SettingsStore";

declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
/**
* Returns the SettingsStore
*/
getSettingsStore(): Chainable<typeof SettingsStore | undefined>; // XXX: Importing SettingsStore causes a bunch of type lint errors
/**
* Open the top left user menu, returning a handle to the resulting context menu.
*/
Expand Down Expand Up @@ -63,10 +69,59 @@ declare global {
* @param name the name of the beta to leave.
*/
leaveBeta(name: string): Chainable<JQuery<HTMLElement>>;

/**
* Sets the value for a setting. The room ID is optional if the
* setting is not being set for a particular room, otherwise it
* should be supplied. The value may be null to indicate that the
* level should no longer have an override.
* @param {string} settingName The name of the setting to change.
* @param {String} roomId The room ID to change the value in, may be
* null.
* @param {SettingLevel} level The level to change the value at.
* @param {*} value The new value of the setting, may be null.
* @return {Promise} Resolves when the setting has been changed.
*/
setSettingValue(name: string, roomId: string, level: SettingLevel, value: any): Chainable<void>;

/**
* Gets the value of a setting. The room ID is optional if the
* setting is not to be applied to any particular room, otherwise it
* should be supplied.
* @param {string} settingName The name of the setting to read the
* value of.
* @param {String} roomId The room ID to read the setting value in,
* may be null.
* @param {boolean} excludeDefault True to disable using the default
* value.
* @return {*} The value, or null if not found
*/
getSettingValue<T>(name: string, roomId?: string): Chainable<T>;
}
}
}

Cypress.Commands.add("getSettingsStore", (): Chainable<typeof SettingsStore> => {
return cy.window({ log: false }).then(win => win.mxSettingsStore);
});

Cypress.Commands.add("setSettingValue", (
name: string,
roomId: string,
level: SettingLevel,
value: any,
): Chainable<void> => {
return cy.getSettingsStore().then(async (store: typeof SettingsStore) => {
return store.setValue(name, roomId, level, value);
});
});

Cypress.Commands.add("getSettingValue", <T = any>(name: string, roomId?: string): Chainable<T> => {
return cy.getSettingsStore().then((store: typeof SettingsStore) => {
return store.getValue(name, roomId);
});
});

Cypress.Commands.add("openUserMenu", (): Chainable<JQuery<HTMLElement>> => {
cy.get('[aria-label="User menu"]').click();
return cy.get(".mx_ContextualMenu");
Expand Down
4 changes: 2 additions & 2 deletions src/components/views/avatars/MemberAvatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" |
pushUserOnClick?: boolean;
title?: string;
style?: any;
forceHistorical?: boolean; // true to deny `feature_use_only_current_profiles` usage. Default false.
forceHistorical?: boolean; // true to deny `useOnlyCurrentProfiles` usage. Default false.
hideTitle?: boolean;
}

Expand Down Expand Up @@ -72,7 +72,7 @@ export default class MemberAvatar extends React.PureComponent<IProps, IState> {

private static getState(props: IProps): IState {
let member = props.member;
if (member && !props.forceHistorical && SettingsStore.getValue("feature_use_only_current_profiles")) {
if (member && !props.forceHistorical && SettingsStore.getValue("useOnlyCurrentProfiles")) {
const room = MatrixClientPeg.get().getRoom(member.roomId);
if (room) {
member = room.getMember(member.userId);
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/messages/SenderProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default class SenderProfile extends React.PureComponent<IProps> {
const msgtype = mxEvent.getContent().msgtype;

let member = mxEvent.sender;
if (SettingsStore.getValue("feature_use_only_current_profiles")) {
if (SettingsStore.getValue("useOnlyCurrentProfiles")) {
const room = MatrixClientPeg.get().getRoom(mxEvent.getRoomId());
if (room) {
member = room.getMember(mxEvent.getSender());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
'Pill.shouldShowPillAvatar',
'TextualBody.enableBigEmoji',
'scrollToBottomOnMessageSent',
'useOnlyCurrentProfiles',
];
static GENERAL_SETTINGS = [
'promptBeforeInviteUnknownUsers',
Expand Down
Loading