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
1 change: 1 addition & 0 deletions cypress/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface ApplicationWindow {
mxSettingsStore: any; // XXX: Importing SettingsStore causes a bunch of type lint errors
SimonBrandner marked this conversation as resolved.
Show resolved Hide resolved
mxMatrixClientPeg: {
matrixClient?: MatrixClient;
};
Expand Down
141 changes: 141 additions & 0 deletions cypress/integration/13-timeline/timeline.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
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);
cy.wait(500);
SimonBrandner marked this conversation as resolved.
Show resolved Hide resolved
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);
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;
}
}
}
Expand Down Expand Up @@ -103,3 +164,32 @@ Cypress.Commands.add("setAccountData", (type: string, data: object): Chainable<{
return cli.setAccountData(type, data);
});
});

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);
});
});
54 changes: 54 additions & 0 deletions cypress/support/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,16 @@ limitations under the License.
/// <reference types="cypress" />

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

declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
/**
* Returns the SettingsStore
*/
getSettingsStore(): Chainable<any | 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 +68,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<any> => {
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: any) => {
return store.setValue(name, roomId, level, value);
});
});

Cypress.Commands.add("getSettingValue", <T = any>(name: string, roomId?: string): Chainable<T> => {
return cy.getSettingsStore().then((store: any) => {
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
6 changes: 2 additions & 4 deletions src/settings/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -347,10 +347,8 @@ export const SETTINGS: {[setting: string]: ISetting} = {
displayName: _td("Show extensible event representation of events"),
default: false,
},
"feature_use_only_current_profiles": {
isFeature: true,
labsGroup: LabGroup.Rooms,
supportedLevels: LEVELS_FEATURE,
"useOnlyCurrentProfiles": {
supportedLevels: [SettingLevel.ACCOUNT],
SimonBrandner marked this conversation as resolved.
Show resolved Hide resolved
displayName: _td("Show current avatar and name for users in message history"),
default: false,
},
Expand Down