Merge branch 'develop' into johannes/latest-room-in-space
This commit is contained in:
commit
f3fd027db4
40 changed files with 948 additions and 868 deletions
|
@ -77,7 +77,7 @@ describe("Spaces", () => {
|
||||||
cy.stopHomeserver(homeserver);
|
cy.stopHomeserver(homeserver);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.only("should allow user to create public space", () => {
|
it("should allow user to create public space", () => {
|
||||||
openSpaceCreateMenu();
|
openSpaceCreateMenu();
|
||||||
cy.get("#mx_ContextualMenu_Container").percySnapshotElement("Space create menu");
|
cy.get("#mx_ContextualMenu_Container").percySnapshotElement("Space create menu");
|
||||||
cy.get(".mx_SpaceCreateMenu_wrapper .mx_ContextualMenu").within(() => {
|
cy.get(".mx_SpaceCreateMenu_wrapper .mx_ContextualMenu").within(() => {
|
||||||
|
@ -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", () => {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -267,3 +267,7 @@ limitations under the License.
|
||||||
.mx_RoomSummaryCard_icon_export::before {
|
.mx_RoomSummaryCard_icon_export::before {
|
||||||
mask-image: url("$(res)/img/element-icons/export.svg");
|
mask-image: url("$(res)/img/element-icons/export.svg");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_RoomSummaryCard_icon_poll::before {
|
||||||
|
mask-image: url("$(res)/img/element-icons/room/composer/poll.svg");
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -635,7 +635,7 @@ $left-gutter: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Make list type disc to match rich text editor */
|
/* Make list type disc to match rich text editor */
|
||||||
> ul {
|
ul {
|
||||||
list-style-type: disc;
|
list-style-type: disc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,6 +58,11 @@ limitations under the License.
|
||||||
padding-inline-start: $spacing-28;
|
padding-inline-start: $spacing-28;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Make list type disc to match rich text editor */
|
||||||
|
ul {
|
||||||
|
list-style-type: disc;
|
||||||
|
}
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
color: #777;
|
color: #777;
|
||||||
border-left: 2px solid $blockquote-bar-color;
|
border-left: 2px solid $blockquote-bar-color;
|
||||||
|
@ -90,6 +95,11 @@ limitations under the License.
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: $spacing-2;
|
padding: $spacing-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
code:empty {
|
||||||
|
border: unset;
|
||||||
|
padding: unset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_WysiwygComposer_Editor_content_placeholder::before {
|
.mx_WysiwygComposer_Editor_content_placeholder::before {
|
||||||
|
|
|
@ -10,5 +10,5 @@ sonar.exclusions=__mocks__,docs
|
||||||
|
|
||||||
sonar.typescript.tsconfigPath=./tsconfig.json
|
sonar.typescript.tsconfigPath=./tsconfig.json
|
||||||
sonar.javascript.lcov.reportPaths=coverage/lcov.info
|
sonar.javascript.lcov.reportPaths=coverage/lcov.info
|
||||||
sonar.coverage.exclusions=test/**/*,cypress/**/*
|
sonar.coverage.exclusions=test/**/*,cypress/**/*,src/components/views/dialogs/devtools/**/*
|
||||||
sonar.testExecutionReportPaths=coverage/jest-sonar-report.xml
|
sonar.testExecutionReportPaths=coverage/jest-sonar-report.xml
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
33
src/components/views/dialogs/polls/PollHistoryDialog.tsx
Normal file
33
src/components/views/dialogs/polls/PollHistoryDialog.tsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
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 React from "react";
|
||||||
|
|
||||||
|
import { _t } from "../../../../languageHandler";
|
||||||
|
import BaseDialog from "../BaseDialog";
|
||||||
|
import { IDialogProps } from "../IDialogProps";
|
||||||
|
|
||||||
|
type PollHistoryDialogProps = Pick<IDialogProps, "onFinished"> & {
|
||||||
|
roomId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PollHistoryDialog: React.FC<PollHistoryDialogProps> = ({ onFinished }) => {
|
||||||
|
return (
|
||||||
|
<BaseDialog title={_t("Polls history")} onFinished={onFinished}>
|
||||||
|
{/* @TODO(kerrya) to be implemented in PSG-906 */}
|
||||||
|
</BaseDialog>
|
||||||
|
);
|
||||||
|
};
|
|
@ -28,6 +28,7 @@ import EventTileBubble from "./EventTileBubble";
|
||||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||||
import RoomContext from "../../../contexts/RoomContext";
|
import RoomContext from "../../../contexts/RoomContext";
|
||||||
import { useRoomState } from "../../../hooks/useRoomState";
|
import { useRoomState } from "../../../hooks/useRoomState";
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
/** The m.room.create MatrixEvent that this tile represents */
|
/** The m.room.create MatrixEvent that this tile represents */
|
||||||
|
@ -40,6 +41,8 @@ interface IProps {
|
||||||
* room.
|
* room.
|
||||||
*/
|
*/
|
||||||
export const RoomCreate: React.FC<IProps> = ({ mxEvent, timestamp }) => {
|
export const RoomCreate: React.FC<IProps> = ({ mxEvent, timestamp }) => {
|
||||||
|
const msc3946ProcessDynamicPredecessor = SettingsStore.getValue("feature_dynamic_room_predecessors");
|
||||||
|
|
||||||
// Note: we ask the room for its predecessor here, instead of directly using
|
// Note: we ask the room for its predecessor here, instead of directly using
|
||||||
// the information inside mxEvent. This allows us the flexibility later to
|
// the information inside mxEvent. This allows us the flexibility later to
|
||||||
// use a different predecessor (e.g. through MSC3946) and still display it
|
// use a different predecessor (e.g. through MSC3946) and still display it
|
||||||
|
@ -47,7 +50,10 @@ export const RoomCreate: React.FC<IProps> = ({ mxEvent, timestamp }) => {
|
||||||
const roomContext = useContext(RoomContext);
|
const roomContext = useContext(RoomContext);
|
||||||
const predecessor = useRoomState(
|
const predecessor = useRoomState(
|
||||||
roomContext.room,
|
roomContext.room,
|
||||||
useCallback((state) => state.findPredecessor(), []),
|
useCallback(
|
||||||
|
(state) => state.findPredecessor(msc3946ProcessDynamicPredecessor),
|
||||||
|
[msc3946ProcessDynamicPredecessor],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const onLinkClicked = useCallback(
|
const onLinkClicked = useCallback(
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
|
||||||
import BaseCard, { Group } from "./BaseCard";
|
import BaseCard, { Group } from "./BaseCard";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import RoomAvatar from "../avatars/RoomAvatar";
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
import AccessibleButton, { ButtonEvent, IAccessibleButtonProps } from "../elements/AccessibleButton";
|
||||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||||
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
|
@ -51,6 +51,7 @@ import ExportDialog from "../dialogs/ExportDialog";
|
||||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||||
import PosthogTrackers from "../../../PosthogTrackers";
|
import PosthogTrackers from "../../../PosthogTrackers";
|
||||||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||||
|
import { PollHistoryDialog } from "../dialogs/polls/PollHistoryDialog";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -61,14 +62,15 @@ interface IAppsSectionProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IButtonProps {
|
interface IButtonProps extends IAccessibleButtonProps {
|
||||||
className: string;
|
className: string;
|
||||||
onClick(ev: ButtonEvent): void;
|
onClick(ev: ButtonEvent): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button: React.FC<IButtonProps> = ({ children, className, onClick }) => {
|
const Button: React.FC<IButtonProps> = ({ children, className, onClick, ...props }) => {
|
||||||
return (
|
return (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
|
{...props}
|
||||||
className={classNames("mx_BaseCard_Button mx_RoomSummaryCard_Button", className)}
|
className={classNames("mx_BaseCard_Button mx_RoomSummaryCard_Button", className)}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
|
@ -281,6 +283,12 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onRoomPollHistoryClick = (): void => {
|
||||||
|
Modal.createDialog(PollHistoryDialog, {
|
||||||
|
roomId: room.roomId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const isRoomEncrypted = useIsEncrypted(cli, room);
|
const isRoomEncrypted = useIsEncrypted(cli, room);
|
||||||
const roomContext = useContext(RoomContext);
|
const roomContext = useContext(RoomContext);
|
||||||
const e2eStatus = roomContext.e2eStatus;
|
const e2eStatus = roomContext.e2eStatus;
|
||||||
|
@ -315,6 +323,8 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
|
||||||
const pinningEnabled = useFeatureEnabled("feature_pinning");
|
const pinningEnabled = useFeatureEnabled("feature_pinning");
|
||||||
const pinCount = usePinnedEvents(pinningEnabled && room)?.length;
|
const pinCount = usePinnedEvents(pinningEnabled && room)?.length;
|
||||||
|
|
||||||
|
const isPollHistoryEnabled = useFeatureEnabled("feature_poll_history");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseCard header={header} className="mx_RoomSummaryCard" onClose={onClose}>
|
<BaseCard header={header} className="mx_RoomSummaryCard" onClose={onClose}>
|
||||||
<Group title={_t("About")} className="mx_RoomSummaryCard_aboutGroup">
|
<Group title={_t("About")} className="mx_RoomSummaryCard_aboutGroup">
|
||||||
|
@ -327,6 +337,11 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
|
||||||
{_t("Files")}
|
{_t("Files")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{!isVideoRoom && isPollHistoryEnabled && (
|
||||||
|
<Button className="mx_RoomSummaryCard_icon_poll" onClick={onRoomPollHistoryClick}>
|
||||||
|
{_t("Polls history")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{pinningEnabled && !isVideoRoom && (
|
{pinningEnabled && !isVideoRoom && (
|
||||||
<Button className="mx_RoomSummaryCard_icon_pins" onClick={onRoomPinsClick}>
|
<Button className="mx_RoomSummaryCard_icon_pins" onClick={onRoomPinsClick}>
|
||||||
{_t("Pinned")}
|
{_t("Pinned")}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -934,7 +934,7 @@
|
||||||
"Keep discussions organised with threads.": "Keep discussions organised with threads.",
|
"Keep discussions organised with threads.": "Keep discussions organised with threads.",
|
||||||
"Threads help keep conversations on-topic and easy to track. <a>Learn more</a>.": "Threads help keep conversations on-topic and easy to track. <a>Learn more</a>.",
|
"Threads help keep conversations on-topic and easy to track. <a>Learn more</a>.": "Threads help keep conversations on-topic and easy to track. <a>Learn more</a>.",
|
||||||
"Rich text editor": "Rich text editor",
|
"Rich text editor": "Rich text editor",
|
||||||
"Use rich text instead of Markdown in the message composer. Plain text mode coming soon.": "Use rich text instead of Markdown in the message composer. Plain text mode coming soon.",
|
"Use rich text instead of Markdown in the message composer.": "Use rich text instead of Markdown in the message composer.",
|
||||||
"Render simple counters in room header": "Render simple counters in room header",
|
"Render simple counters in room header": "Render simple counters in room header",
|
||||||
"New ways to ignore people": "New ways to ignore people",
|
"New ways to ignore people": "New ways to ignore people",
|
||||||
"Currently experimental.": "Currently experimental.",
|
"Currently experimental.": "Currently experimental.",
|
||||||
|
@ -948,6 +948,8 @@
|
||||||
"Use new room breadcrumbs": "Use new room breadcrumbs",
|
"Use new room breadcrumbs": "Use new room breadcrumbs",
|
||||||
"Right panel stays open": "Right panel stays open",
|
"Right panel stays open": "Right panel stays open",
|
||||||
"Defaults to room member list.": "Defaults to room member list.",
|
"Defaults to room member list.": "Defaults to room member list.",
|
||||||
|
"Polls history": "Polls history",
|
||||||
|
"View a list of polls in a room. (Under active development)": "View a list of polls in a room. (Under active development)",
|
||||||
"Jump to date (adds /jumptodate and jump to date headers)": "Jump to date (adds /jumptodate and jump to date headers)",
|
"Jump to date (adds /jumptodate and jump to date headers)": "Jump to date (adds /jumptodate and jump to date headers)",
|
||||||
"Send read receipts": "Send read receipts",
|
"Send read receipts": "Send read receipts",
|
||||||
"Sliding Sync mode": "Sliding Sync mode",
|
"Sliding Sync mode": "Sliding Sync mode",
|
||||||
|
|
|
@ -291,7 +291,7 @@ export const SETTINGS: { [setting: string]: ISetting } = {
|
||||||
isFeature: true,
|
isFeature: true,
|
||||||
labsGroup: LabGroup.Messaging,
|
labsGroup: LabGroup.Messaging,
|
||||||
displayName: _td("Rich text editor"),
|
displayName: _td("Rich text editor"),
|
||||||
description: _td("Use rich text instead of Markdown in the message composer. Plain text mode coming soon."),
|
description: _td("Use rich text instead of Markdown in the message composer."),
|
||||||
supportedLevels: LEVELS_FEATURE,
|
supportedLevels: LEVELS_FEATURE,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
@ -382,6 +382,14 @@ export const SETTINGS: { [setting: string]: ISetting } = {
|
||||||
description: _td("Defaults to room member list."),
|
description: _td("Defaults to room member list."),
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
"feature_poll_history": {
|
||||||
|
isFeature: true,
|
||||||
|
labsGroup: LabGroup.Rooms,
|
||||||
|
supportedLevels: LEVELS_FEATURE,
|
||||||
|
displayName: _td("Polls history"),
|
||||||
|
description: _td("View a list of polls in a room. (Under active development)"),
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
"feature_jump_to_date": {
|
"feature_jump_to_date": {
|
||||||
// We purposely leave out `isFeature: true` so it doesn't show in Labs
|
// We purposely leave out `isFeature: true` so it doesn't show in Labs
|
||||||
// by default. We will conditionally show it depending on whether we can
|
// by default. We will conditionally show it depending on whether we can
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { MatrixClient } 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 { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
|
import { RoomState } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models";
|
import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models";
|
||||||
|
@ -267,44 +268,55 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> implements
|
||||||
}
|
}
|
||||||
this.updateFn.trigger();
|
this.updateFn.trigger();
|
||||||
} else if (payload.action === "MatrixActions.Room.myMembership") {
|
} else if (payload.action === "MatrixActions.Room.myMembership") {
|
||||||
const membershipPayload = <any>payload; // TODO: Type out the dispatcher types
|
this.onDispatchMyMembership(<any>payload);
|
||||||
const oldMembership = getEffectiveMembership(membershipPayload.oldMembership);
|
return;
|
||||||
const newMembership = getEffectiveMembership(membershipPayload.membership);
|
}
|
||||||
if (oldMembership !== EffectiveMembership.Join && newMembership === EffectiveMembership.Join) {
|
}
|
||||||
// If we're joining an upgraded room, we'll want to make sure we don't proliferate
|
|
||||||
// the dead room in the list.
|
|
||||||
const createEvent = membershipPayload.room.currentState.getStateEvents(EventType.RoomCreate, "");
|
|
||||||
if (createEvent && createEvent.getContent()["predecessor"]) {
|
|
||||||
const prevRoom = this.matrixClient.getRoom(createEvent.getContent()["predecessor"]["room_id"]);
|
|
||||||
if (prevRoom) {
|
|
||||||
const isSticky = this.algorithm.stickyRoom === prevRoom;
|
|
||||||
if (isSticky) {
|
|
||||||
this.algorithm.setStickyRoom(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: we hit the algorithm instead of our handleRoomUpdate() function to
|
/**
|
||||||
// avoid redundant updates.
|
* Handle a MatrixActions.Room.myMembership event from the dispatcher.
|
||||||
this.algorithm.handleRoomUpdate(prevRoom, RoomUpdateCause.RoomRemoved);
|
*
|
||||||
|
* Public for test.
|
||||||
|
*/
|
||||||
|
public async onDispatchMyMembership(membershipPayload: any): Promise<void> {
|
||||||
|
// TODO: Type out the dispatcher types so membershipPayload is not any
|
||||||
|
const oldMembership = getEffectiveMembership(membershipPayload.oldMembership);
|
||||||
|
const newMembership = getEffectiveMembership(membershipPayload.membership);
|
||||||
|
if (oldMembership !== EffectiveMembership.Join && newMembership === EffectiveMembership.Join) {
|
||||||
|
// If we're joining an upgraded room, we'll want to make sure we don't proliferate
|
||||||
|
// the dead room in the list.
|
||||||
|
const roomState: RoomState = membershipPayload.room.currentState;
|
||||||
|
const createEvent = roomState.getStateEvents(EventType.RoomCreate, "");
|
||||||
|
if (createEvent && createEvent.getContent()["predecessor"]) {
|
||||||
|
const prevRoom = this.matrixClient.getRoom(createEvent.getContent()["predecessor"]["room_id"]);
|
||||||
|
if (prevRoom) {
|
||||||
|
const isSticky = this.algorithm.stickyRoom === prevRoom;
|
||||||
|
if (isSticky) {
|
||||||
|
this.algorithm.setStickyRoom(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: we hit the algorithm instead of our handleRoomUpdate() function to
|
||||||
|
// avoid redundant updates.
|
||||||
|
this.algorithm.handleRoomUpdate(prevRoom, RoomUpdateCause.RoomRemoved);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.NewRoom);
|
|
||||||
this.updateFn.trigger();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldMembership !== EffectiveMembership.Invite && newMembership === EffectiveMembership.Invite) {
|
await this.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.NewRoom);
|
||||||
await this.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.NewRoom);
|
this.updateFn.trigger();
|
||||||
this.updateFn.trigger();
|
return;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// If it's not a join, it's transitioning into a different list (possibly historical)
|
if (oldMembership !== EffectiveMembership.Invite && newMembership === EffectiveMembership.Invite) {
|
||||||
if (oldMembership !== newMembership) {
|
await this.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.NewRoom);
|
||||||
await this.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.PossibleTagChange);
|
this.updateFn.trigger();
|
||||||
this.updateFn.trigger();
|
return;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
// If it's not a join, it's transitioning into a different list (possibly historical)
|
||||||
|
if (oldMembership !== newMembership) {
|
||||||
|
await this.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.PossibleTagChange);
|
||||||
|
this.updateFn.trigger();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
|
@ -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;
|
||||||
expect(avatar).toBeInTheDocument();
|
await waitFor(() => {
|
||||||
expect(avatar.getAttribute("src")).not.toBe("");
|
avatar = getByTestId(container, "avatar-img");
|
||||||
});
|
expect(avatar).toBeInTheDocument();
|
||||||
|
|
||||||
it("uses the member's configured avatar", () => {
|
|
||||||
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();
|
expect(avatar!.getAttribute("src")).not.toBe("");
|
||||||
const [payload] = spy.mock.lastCall!;
|
|
||||||
expect(payload).toStrictEqual<ViewUserPayload>({
|
|
||||||
action: Action.ViewUser,
|
|
||||||
member,
|
|
||||||
push: false,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>
|
|
||||||
`;
|
|
|
@ -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>
|
|
||||||
`;
|
|
|
@ -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>
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -52,10 +52,35 @@ describe("<RoomCreate />", () => {
|
||||||
content: {},
|
content: {},
|
||||||
event_id: "$create",
|
event_id: "$create",
|
||||||
});
|
});
|
||||||
|
const predecessorEvent = new MatrixEvent({
|
||||||
|
type: EventType.RoomPredecessor,
|
||||||
|
state_key: "",
|
||||||
|
sender: userId,
|
||||||
|
room_id: roomId,
|
||||||
|
content: {
|
||||||
|
predecessor_room_id: "old_room_id_from_predecessor",
|
||||||
|
},
|
||||||
|
event_id: "$create",
|
||||||
|
});
|
||||||
|
const predecessorEventWithEventId = new MatrixEvent({
|
||||||
|
type: EventType.RoomPredecessor,
|
||||||
|
state_key: "",
|
||||||
|
sender: userId,
|
||||||
|
room_id: roomId,
|
||||||
|
content: {
|
||||||
|
predecessor_room_id: "old_room_id_from_predecessor",
|
||||||
|
last_known_event_id: "tombstone_event_id_from_predecessor",
|
||||||
|
},
|
||||||
|
event_id: "$create",
|
||||||
|
});
|
||||||
stubClient();
|
stubClient();
|
||||||
const client = mocked(MatrixClientPeg.get());
|
const client = mocked(MatrixClientPeg.get());
|
||||||
const room = new Room(roomId, client, userId);
|
const roomJustCreate = new Room(roomId, client, userId);
|
||||||
upsertRoomStateEvents(room, [createEvent]);
|
upsertRoomStateEvents(roomJustCreate, [createEvent]);
|
||||||
|
const roomCreateAndPredecessor = new Room(roomId, client, userId);
|
||||||
|
upsertRoomStateEvents(roomCreateAndPredecessor, [createEvent, predecessorEvent]);
|
||||||
|
const roomCreateAndPredecessorWithEventId = new Room(roomId, client, userId);
|
||||||
|
upsertRoomStateEvents(roomCreateAndPredecessorWithEventId, [createEvent, predecessorEventWithEventId]);
|
||||||
const roomNoPredecessors = new Room(roomId, client, userId);
|
const roomNoPredecessors = new Room(roomId, client, userId);
|
||||||
upsertRoomStateEvents(roomNoPredecessors, [createEventWithoutPredecessor]);
|
upsertRoomStateEvents(roomNoPredecessors, [createEventWithoutPredecessor]);
|
||||||
|
|
||||||
|
@ -81,12 +106,12 @@ describe("<RoomCreate />", () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
it("Renders as expected", () => {
|
it("Renders as expected", () => {
|
||||||
const roomCreate = renderRoomCreate(room);
|
const roomCreate = renderRoomCreate(roomJustCreate);
|
||||||
expect(roomCreate.asFragment()).toMatchSnapshot();
|
expect(roomCreate.asFragment()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Links to the old version of the room", () => {
|
it("Links to the old version of the room", () => {
|
||||||
renderRoomCreate(room);
|
renderRoomCreate(roomJustCreate);
|
||||||
expect(screen.getByText("Click here to see older messages.")).toHaveAttribute(
|
expect(screen.getByText("Click here to see older messages.")).toHaveAttribute(
|
||||||
"href",
|
"href",
|
||||||
"https://matrix.to/#/old_room_id/tombstone_event_id",
|
"https://matrix.to/#/old_room_id/tombstone_event_id",
|
||||||
|
@ -99,7 +124,7 @@ describe("<RoomCreate />", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Opens the old room on click", async () => {
|
it("Opens the old room on click", async () => {
|
||||||
renderRoomCreate(room);
|
renderRoomCreate(roomJustCreate);
|
||||||
const link = screen.getByText("Click here to see older messages.");
|
const link = screen.getByText("Click here to see older messages.");
|
||||||
|
|
||||||
await act(() => userEvent.click(link));
|
await act(() => userEvent.click(link));
|
||||||
|
@ -115,4 +140,48 @@ describe("<RoomCreate />", () => {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Ignores m.predecessor if labs flag is off", () => {
|
||||||
|
renderRoomCreate(roomCreateAndPredecessor);
|
||||||
|
expect(screen.getByText("Click here to see older messages.")).toHaveAttribute(
|
||||||
|
"href",
|
||||||
|
"https://matrix.to/#/old_room_id/tombstone_event_id",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("When feature_dynamic_room_predecessors = true", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(SettingsStore, "getValue").mockImplementation(
|
||||||
|
(settingName) => settingName === "feature_dynamic_room_predecessors",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.spyOn(SettingsStore, "getValue").mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Uses the create event if there is no m.predecessor", () => {
|
||||||
|
renderRoomCreate(roomJustCreate);
|
||||||
|
expect(screen.getByText("Click here to see older messages.")).toHaveAttribute(
|
||||||
|
"href",
|
||||||
|
"https://matrix.to/#/old_room_id/tombstone_event_id",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Uses m.predecessor when it's there", () => {
|
||||||
|
renderRoomCreate(roomCreateAndPredecessor);
|
||||||
|
expect(screen.getByText("Click here to see older messages.")).toHaveAttribute(
|
||||||
|
"href",
|
||||||
|
"https://matrix.to/#/old_room_id_from_predecessor",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Links to the event in the room if event ID is provided", () => {
|
||||||
|
renderRoomCreate(roomCreateAndPredecessorWithEventId);
|
||||||
|
expect(screen.getByText("Click here to see older messages.")).toHaveAttribute(
|
||||||
|
"href",
|
||||||
|
"https://matrix.to/#/old_room_id_from_predecessor/tombstone_event_id_from_predecessor",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
182
test/components/views/right_panel/RoomSummaryCard-test.tsx
Normal file
182
test/components/views/right_panel/RoomSummaryCard-test.tsx
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
/*
|
||||||
|
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 React from "react";
|
||||||
|
import { render, fireEvent } from "@testing-library/react";
|
||||||
|
import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||||
|
import RoomSummaryCard from "../../../../src/components/views/right_panel/RoomSummaryCard";
|
||||||
|
import ShareDialog from "../../../../src/components/views/dialogs/ShareDialog";
|
||||||
|
import ExportDialog from "../../../../src/components/views/dialogs/ExportDialog";
|
||||||
|
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||||
|
import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
|
||||||
|
import * as settingsHooks from "../../../../src/hooks/useSettings";
|
||||||
|
import Modal from "../../../../src/Modal";
|
||||||
|
import RightPanelStore from "../../../../src/stores/right-panel/RightPanelStore";
|
||||||
|
import { RightPanelPhases } from "../../../../src/stores/right-panel/RightPanelStorePhases";
|
||||||
|
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../test-utils";
|
||||||
|
import { PollHistoryDialog } from "../../../../src/components/views/dialogs/polls/PollHistoryDialog";
|
||||||
|
|
||||||
|
describe("<RoomSummaryCard />", () => {
|
||||||
|
const userId = "@alice:domain.org";
|
||||||
|
const mockClient = getMockClientWithEventEmitter({
|
||||||
|
...mockClientMethodsUser(userId),
|
||||||
|
isRoomEncrypted: jest.fn(),
|
||||||
|
getRoom: jest.fn(),
|
||||||
|
});
|
||||||
|
const roomId = "!room:domain.org";
|
||||||
|
const room = new Room(roomId, mockClient, userId);
|
||||||
|
const roomCreateEvent = new MatrixEvent({
|
||||||
|
type: "m.room.create",
|
||||||
|
room_id: roomId,
|
||||||
|
sender: userId,
|
||||||
|
content: {
|
||||||
|
creator: userId,
|
||||||
|
room_version: "5",
|
||||||
|
},
|
||||||
|
state_key: "",
|
||||||
|
});
|
||||||
|
room.currentState.setStateEvents([roomCreateEvent]);
|
||||||
|
const defaultProps = {
|
||||||
|
room,
|
||||||
|
onClose: jest.fn(),
|
||||||
|
};
|
||||||
|
const getComponent = (props = {}) =>
|
||||||
|
render(<RoomSummaryCard {...defaultProps} {...props} />, {
|
||||||
|
wrapper: ({ children }) => (
|
||||||
|
<MatrixClientContext.Provider value={mockClient}>{children}</MatrixClientContext.Provider>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const modalSpy = jest.spyOn(Modal, "createDialog");
|
||||||
|
const dispatchSpy = jest.spyOn(defaultDispatcher, "dispatch");
|
||||||
|
const rightPanelCardSpy = jest.spyOn(RightPanelStore.instance, "pushCard");
|
||||||
|
const featureEnabledSpy = jest.spyOn(settingsHooks, "useFeatureEnabled");
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
DMRoomMap.makeShared();
|
||||||
|
|
||||||
|
mockClient.getRoom.mockReturnValue(room);
|
||||||
|
jest.spyOn(room, "isElementVideoRoom").mockRestore();
|
||||||
|
jest.spyOn(room, "isCallRoom").mockRestore();
|
||||||
|
featureEnabledSpy.mockReset().mockReturnValue(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the room summary", () => {
|
||||||
|
const { container } = getComponent();
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opens room members list on button click", () => {
|
||||||
|
const { getByText } = getComponent();
|
||||||
|
|
||||||
|
fireEvent.click(getByText("People"));
|
||||||
|
|
||||||
|
expect(rightPanelCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.RoomMemberList }, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opens room file panel on button click", () => {
|
||||||
|
const { getByText } = getComponent();
|
||||||
|
|
||||||
|
fireEvent.click(getByText("Files"));
|
||||||
|
|
||||||
|
expect(rightPanelCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.FilePanel }, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opens room export dialog on button click", () => {
|
||||||
|
const { getByText } = getComponent();
|
||||||
|
|
||||||
|
fireEvent.click(getByText("Export chat"));
|
||||||
|
|
||||||
|
expect(modalSpy).toHaveBeenCalledWith(ExportDialog, { room });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opens share room dialog on button click", () => {
|
||||||
|
const { getByText } = getComponent();
|
||||||
|
|
||||||
|
fireEvent.click(getByText("Share room"));
|
||||||
|
|
||||||
|
expect(modalSpy).toHaveBeenCalledWith(ShareDialog, { target: room });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opens room settings on button click", () => {
|
||||||
|
const { getByText } = getComponent();
|
||||||
|
|
||||||
|
fireEvent.click(getByText("Room settings"));
|
||||||
|
|
||||||
|
expect(dispatchSpy).toHaveBeenCalledWith({ action: "open_room_settings" });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("pinning", () => {
|
||||||
|
it("renders pins options when pinning feature is enabled", () => {
|
||||||
|
featureEnabledSpy.mockImplementation((feature) => feature === "feature_pinning");
|
||||||
|
const { getByText } = getComponent();
|
||||||
|
|
||||||
|
expect(getByText("Pinned")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("poll history", () => {
|
||||||
|
it("renders poll history option when feature is enabled", () => {
|
||||||
|
featureEnabledSpy.mockImplementation((feature) => feature === "feature_poll_history");
|
||||||
|
const { getByText } = getComponent();
|
||||||
|
|
||||||
|
expect(getByText("Polls history")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opens poll history dialog on button click", () => {
|
||||||
|
featureEnabledSpy.mockImplementation((feature) => feature === "feature_poll_history");
|
||||||
|
const { getByText } = getComponent();
|
||||||
|
|
||||||
|
fireEvent.click(getByText("Polls history"));
|
||||||
|
|
||||||
|
expect(modalSpy).toHaveBeenCalledWith(PollHistoryDialog, { roomId });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("video rooms", () => {
|
||||||
|
it("does not render irrelevant options for element video room", () => {
|
||||||
|
jest.spyOn(room, "isElementVideoRoom").mockReturnValue(true);
|
||||||
|
featureEnabledSpy.mockImplementation(
|
||||||
|
(feature) => feature === "feature_video_rooms" || feature === "feature_pinning",
|
||||||
|
);
|
||||||
|
const { queryByText } = getComponent();
|
||||||
|
|
||||||
|
// options not rendered
|
||||||
|
expect(queryByText("Files")).not.toBeInTheDocument();
|
||||||
|
expect(queryByText("Pinned")).not.toBeInTheDocument();
|
||||||
|
expect(queryByText("Export chat")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render irrelevant options for element call room", () => {
|
||||||
|
jest.spyOn(room, "isCallRoom").mockReturnValue(true);
|
||||||
|
featureEnabledSpy.mockImplementation(
|
||||||
|
(feature) =>
|
||||||
|
feature === "feature_element_call_video_rooms" ||
|
||||||
|
feature === "feature_video_rooms" ||
|
||||||
|
feature === "feature_pinning",
|
||||||
|
);
|
||||||
|
const { queryByText } = getComponent();
|
||||||
|
|
||||||
|
// options not rendered
|
||||||
|
expect(queryByText("Files")).not.toBeInTheDocument();
|
||||||
|
expect(queryByText("Pinned")).not.toBeInTheDocument();
|
||||||
|
expect(queryByText("Export chat")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,125 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<RoomSummaryCard /> renders the room summary 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_BaseCard mx_RoomSummaryCard"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_BaseCard_header"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_BaseCard_close"
|
||||||
|
data-testid="base-card-close-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
title="Close"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="mx_RoomSummaryCard_avatar"
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_BaseAvatar"
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mx_BaseAvatar_initial"
|
||||||
|
style="font-size: 35.1px; width: 54px; line-height: 54px;"
|
||||||
|
>
|
||||||
|
!
|
||||||
|
</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>
|
||||||
|
<div
|
||||||
|
class="mx_TextWithTooltip_target mx_RoomSummaryCard_e2ee"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h2
|
||||||
|
title="!room:domain.org"
|
||||||
|
>
|
||||||
|
!room:domain.org
|
||||||
|
</h2>
|
||||||
|
<div
|
||||||
|
class="mx_RoomSummaryCard_alias"
|
||||||
|
title=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_AutoHideScrollbar"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_BaseCard_Group mx_RoomSummaryCard_aboutGroup"
|
||||||
|
>
|
||||||
|
<h1>
|
||||||
|
About
|
||||||
|
</h1>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_BaseCard_Button mx_RoomSummaryCard_Button mx_RoomSummaryCard_icon_people"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
People
|
||||||
|
<span
|
||||||
|
class="mx_BaseCard_Button_sublabel"
|
||||||
|
>
|
||||||
|
0
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_BaseCard_Button mx_RoomSummaryCard_Button mx_RoomSummaryCard_icon_files"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Files
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_BaseCard_Button mx_RoomSummaryCard_Button mx_RoomSummaryCard_icon_export"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Export chat
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_BaseCard_Button mx_RoomSummaryCard_Button mx_RoomSummaryCard_icon_share"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Share room
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_BaseCard_Button mx_RoomSummaryCard_Button mx_RoomSummaryCard_icon_settings"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Room settings
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_BaseCard_Group mx_RoomSummaryCard_appsGroup"
|
||||||
|
>
|
||||||
|
<h1>
|
||||||
|
Widgets
|
||||||
|
</h1>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Add widgets, bridges & bots
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
|
@ -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", () => {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
|
||||||
`;
|
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
|
@ -14,14 +14,55 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { EventType, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { MatrixDispatcher } from "../../../src/dispatcher/dispatcher";
|
||||||
import { ListAlgorithm, SortAlgorithm } from "../../../src/stores/room-list/algorithms/models";
|
import { ListAlgorithm, SortAlgorithm } from "../../../src/stores/room-list/algorithms/models";
|
||||||
import { OrderedDefaultTagIDs } from "../../../src/stores/room-list/models";
|
import { OrderedDefaultTagIDs, RoomUpdateCause } from "../../../src/stores/room-list/models";
|
||||||
import RoomListStore, { RoomListStoreClass } from "../../../src/stores/room-list/RoomListStore";
|
import RoomListStore, { RoomListStoreClass } from "../../../src/stores/room-list/RoomListStore";
|
||||||
import { stubClient } from "../../test-utils";
|
import { stubClient, upsertRoomStateEvents } from "../../test-utils";
|
||||||
|
|
||||||
describe("RoomListStore", () => {
|
describe("RoomListStore", () => {
|
||||||
|
const client = stubClient();
|
||||||
|
const roomWithCreatePredecessorId = "!roomid:example.com";
|
||||||
|
const roomNoPredecessorId = "!roomnopreid:example.com";
|
||||||
|
const oldRoomId = "!oldroomid:example.com";
|
||||||
|
const userId = "@user:example.com";
|
||||||
|
const createWithPredecessor = new MatrixEvent({
|
||||||
|
type: EventType.RoomCreate,
|
||||||
|
sender: userId,
|
||||||
|
room_id: roomWithCreatePredecessorId,
|
||||||
|
content: {
|
||||||
|
predecessor: { room_id: oldRoomId, event_id: "tombstone_event_id" },
|
||||||
|
},
|
||||||
|
event_id: "$create",
|
||||||
|
state_key: "",
|
||||||
|
});
|
||||||
|
const createNoPredecessor = new MatrixEvent({
|
||||||
|
type: EventType.RoomCreate,
|
||||||
|
sender: userId,
|
||||||
|
room_id: roomWithCreatePredecessorId,
|
||||||
|
content: {},
|
||||||
|
event_id: "$create",
|
||||||
|
state_key: "",
|
||||||
|
});
|
||||||
|
const roomWithCreatePredecessor = new Room(roomWithCreatePredecessorId, client, userId, {});
|
||||||
|
upsertRoomStateEvents(roomWithCreatePredecessor, [createWithPredecessor]);
|
||||||
|
const roomNoPredecessor = new Room(roomNoPredecessorId, client, userId, {});
|
||||||
|
upsertRoomStateEvents(roomNoPredecessor, [createNoPredecessor]);
|
||||||
|
const oldRoom = new Room(oldRoomId, client, userId, {});
|
||||||
|
client.getRoom = jest.fn().mockImplementation((roomId) => {
|
||||||
|
switch (roomId) {
|
||||||
|
case roomWithCreatePredecessorId:
|
||||||
|
return roomWithCreatePredecessor;
|
||||||
|
case oldRoomId:
|
||||||
|
return oldRoom;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const client = stubClient();
|
|
||||||
await (RoomListStore.instance as RoomListStoreClass).makeReady(client);
|
await (RoomListStore.instance as RoomListStoreClass).makeReady(client);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -32,4 +73,54 @@ describe("RoomListStore", () => {
|
||||||
it.each(OrderedDefaultTagIDs)("defaults to activity ordering for %s=", (tagId) => {
|
it.each(OrderedDefaultTagIDs)("defaults to activity ordering for %s=", (tagId) => {
|
||||||
expect(RoomListStore.instance.getListOrder(tagId)).toBe(ListAlgorithm.Importance);
|
expect(RoomListStore.instance.getListOrder(tagId)).toBe(ListAlgorithm.Importance);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function createStore(): { store: RoomListStoreClass; handleRoomUpdate: jest.Mock<any, any> } {
|
||||||
|
const fakeDispatcher = { register: jest.fn() } as unknown as MatrixDispatcher;
|
||||||
|
const store = new RoomListStoreClass(fakeDispatcher);
|
||||||
|
// @ts-ignore accessing private member to set client
|
||||||
|
store.readyStore.matrixClient = client;
|
||||||
|
const handleRoomUpdate = jest.fn();
|
||||||
|
// @ts-ignore accessing private member to mock it
|
||||||
|
store.algorithm.handleRoomUpdate = handleRoomUpdate;
|
||||||
|
|
||||||
|
return { store, handleRoomUpdate };
|
||||||
|
}
|
||||||
|
|
||||||
|
it("Removes old room if it finds a predecessor in the create event", () => {
|
||||||
|
// Given a store we can spy on
|
||||||
|
const { store, handleRoomUpdate } = createStore();
|
||||||
|
|
||||||
|
// When we tell it we joined a new room that has an old room as
|
||||||
|
// predecessor in the create event
|
||||||
|
const payload = {
|
||||||
|
oldMembership: "invite",
|
||||||
|
membership: "join",
|
||||||
|
room: roomWithCreatePredecessor,
|
||||||
|
};
|
||||||
|
store.onDispatchMyMembership(payload);
|
||||||
|
|
||||||
|
// Then the old room is removed
|
||||||
|
expect(handleRoomUpdate).toHaveBeenCalledWith(oldRoom, RoomUpdateCause.RoomRemoved);
|
||||||
|
|
||||||
|
// And the new room is added
|
||||||
|
expect(handleRoomUpdate).toHaveBeenCalledWith(roomWithCreatePredecessor, RoomUpdateCause.NewRoom);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Does not remove old room if there is no predecessor in the create event", () => {
|
||||||
|
// Given a store we can spy on
|
||||||
|
const { store, handleRoomUpdate } = createStore();
|
||||||
|
|
||||||
|
// When we tell it we joined a new room with no predecessor
|
||||||
|
const payload = {
|
||||||
|
oldMembership: "invite",
|
||||||
|
membership: "join",
|
||||||
|
room: roomNoPredecessor,
|
||||||
|
};
|
||||||
|
store.onDispatchMyMembership(payload);
|
||||||
|
|
||||||
|
// Then the new room is added
|
||||||
|
expect(handleRoomUpdate).toHaveBeenCalledWith(roomNoPredecessor, RoomUpdateCause.NewRoom);
|
||||||
|
// And no other updates happen
|
||||||
|
expect(handleRoomUpdate).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue