diff --git a/cypress/e2e/spaces/spaces.spec.ts b/cypress/e2e/spaces/spaces.spec.ts
index 2deb58574b..acf914c60f 100644
--- a/cypress/e2e/spaces/spaces.spec.ts
+++ b/cypress/e2e/spaces/spaces.spec.ts
@@ -77,7 +77,7 @@ describe("Spaces", () => {
cy.stopHomeserver(homeserver);
});
- it.only("should allow user to create public space", () => {
+ it("should allow user to create public space", () => {
openSpaceCreateMenu();
cy.get("#mx_ContextualMenu_Container").percySnapshotElement("Space create menu");
cy.get(".mx_SpaceCreateMenu_wrapper .mx_ContextualMenu").within(() => {
@@ -153,7 +153,10 @@ describe("Spaces", () => {
openSpaceCreateMenu().within(() => {
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('textarea[label="Description"]').type("This is a personal space to mourn Riot.im...");
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_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", () => {
diff --git a/res/css/structures/_SpacePanel.pcss b/res/css/structures/_SpacePanel.pcss
index 859714bfb9..70c65b3f74 100644
--- a/res/css/structures/_SpacePanel.pcss
+++ b/res/css/structures/_SpacePanel.pcss
@@ -277,11 +277,14 @@ $activeBorderColor: $primary-content;
.mx_BaseAvatar:not(.mx_UserMenu_userAvatar_BaseAvatar) .mx_BaseAvatar_initial {
color: $secondary-content;
border-radius: 8px;
+ background-color: $panel-actions;
+ font-size: $font-15px !important; /* override inline style */
font-weight: $font-semi-bold;
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;
- font-size: $font-15px !important;
+
+ & + .mx_BaseAvatar_image {
+ visibility: hidden;
+ }
}
.mx_SpaceTreeLevel {
diff --git a/res/css/views/avatars/_BaseAvatar.pcss b/res/css/views/avatars/_BaseAvatar.pcss
index 43e273b6ff..a6a4b0b74b 100644
--- a/res/css/views/avatars/_BaseAvatar.pcss
+++ b/res/css/views/avatars/_BaseAvatar.pcss
@@ -16,7 +16,16 @@ limitations under the License.
.mx_BaseAvatar {
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;
&.mx_RoomAvatar_isSpaceRoom {
diff --git a/res/css/views/right_panel/_RoomSummaryCard.pcss b/res/css/views/right_panel/_RoomSummaryCard.pcss
index 9e5f2ce4e8..f75743037b 100644
--- a/res/css/views/right_panel/_RoomSummaryCard.pcss
+++ b/res/css/views/right_panel/_RoomSummaryCard.pcss
@@ -267,3 +267,7 @@ limitations under the License.
.mx_RoomSummaryCard_icon_export::before {
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");
+}
diff --git a/res/css/views/rooms/_BasicMessageComposer.pcss b/res/css/views/rooms/_BasicMessageComposer.pcss
index 32e7c5288f..7b88a05815 100644
--- a/res/css/views/rooms/_BasicMessageComposer.pcss
+++ b/res/css/views/rooms/_BasicMessageComposer.pcss
@@ -78,7 +78,7 @@ limitations under the License.
min-width: $font-16px; /* ensure the avatar is not compressed */
height: $font-16px;
margin-inline-end: 0.24rem;
- background: var(--avatar-background);
+ background: var(--avatar-background), $background;
color: $avatar-initial-color;
background-repeat: no-repeat;
background-size: $font-16px;
diff --git a/res/css/views/rooms/_EventTile.pcss b/res/css/views/rooms/_EventTile.pcss
index 72ee340d4f..3b00103581 100644
--- a/res/css/views/rooms/_EventTile.pcss
+++ b/res/css/views/rooms/_EventTile.pcss
@@ -635,7 +635,7 @@ $left-gutter: 64px;
}
/* Make list type disc to match rich text editor */
- > ul {
+ ul {
list-style-type: disc;
}
diff --git a/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss b/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss
index 77e07ab48b..51a213192c 100644
--- a/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss
+++ b/res/css/views/rooms/wysiwyg_composer/components/_Editor.pcss
@@ -58,6 +58,11 @@ limitations under the License.
padding-inline-start: $spacing-28;
}
+ /* Make list type disc to match rich text editor */
+ ul {
+ list-style-type: disc;
+ }
+
blockquote {
color: #777;
border-left: 2px solid $blockquote-bar-color;
@@ -90,6 +95,11 @@ limitations under the License.
border-radius: 4px;
padding: $spacing-2;
}
+
+ code:empty {
+ border: unset;
+ padding: unset;
+ }
}
.mx_WysiwygComposer_Editor_content_placeholder::before {
diff --git a/sonar-project.properties b/sonar-project.properties
index a48c03603f..a8d8f0cf86 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -10,5 +10,5 @@ sonar.exclusions=__mocks__,docs
sonar.typescript.tsconfigPath=./tsconfig.json
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
diff --git a/src/Avatar.ts b/src/Avatar.ts
index 3e6b18dbc7..8a3f10a22c 100644
--- a/src/Avatar.ts
+++ b/src/Avatar.ts
@@ -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");
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 { isLocalRoom } from "./utils/localRoom/isLocalRoom";
-const DEFAULT_COLORS: Readonly = ["#0DBD8B", "#368bd6", "#ac3ba8"];
-
// Not to be used for BaseAvatar urls as that has similar default avatar fallback already
export function avatarUrlForMember(
- member: RoomMember | null | undefined,
+ member: RoomMember,
width: number,
height: number,
resizeMethod: ResizeMethod,
): string {
- let url: string | undefined;
- const mxcUrl = member?.getMxcAvatarUrl();
- if (mxcUrl) {
- url = mediaFromMxc(mxcUrl).getThumbnailOfSourceHttp(width, height, resizeMethod);
+ let url: string;
+ if (member?.getMxcAvatarUrl()) {
+ url = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
}
if (!url) {
// member can be null here currently since on invites, the JS SDK
@@ -47,17 +44,6 @@ export function avatarUrlForMember(
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(
user: Pick,
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
const colorToDataURLCache = new Map();
-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
-
- const color = getColorForString(s);
+ const defaultColors = ["#0DBD8B", "#368bd6", "#ac3ba8"];
+ 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);
if (!dataUrl) {
// validate color as this can come from account_data
@@ -118,23 +112,13 @@ export function defaultAvatarUrlForString(s: string | undefined): string {
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',
* converted to uppercase
* @param {string} name
* @return {string} the first letter
*/
-export function getInitialLetter(name: string): string | undefined {
+export function getInitialLetter(name: string): string {
if (!name) {
// XXX: We should find out what causes the name to sometimes be falsy.
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
- return split(name, "", 1)[0]!.toUpperCase();
+ return split(name, "", 1)[0].toUpperCase();
}
export function avatarUrlForRoom(
- room: Room | undefined,
+ room: Room,
width: number,
height: number,
resizeMethod?: ResizeMethod,
): string | null {
if (!room) return null; // null-guard
- const mxcUrl = room.getMxcAvatarUrl();
- if (mxcUrl) {
- return mediaFromMxc(mxcUrl).getThumbnailOfSourceHttp(width, height, resizeMethod);
+ if (room.getMxcAvatarUrl()) {
+ return mediaFromMxc(room.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
}
// 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
const otherMember = room.getAvatarFallbackMember();
- const otherMemberMxc = otherMember?.getMxcAvatarUrl();
- if (otherMemberMxc) {
- return mediaFromMxc(otherMemberMxc).getThumbnailOfSourceHttp(width, height, resizeMethod);
+ if (otherMember?.getMxcAvatarUrl()) {
+ return mediaFromMxc(otherMember.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
}
return null;
}
diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx
index 7fb0ba458f..025cb9d271 100644
--- a/src/components/views/avatars/BaseAvatar.tsx
+++ b/src/components/views/avatars/BaseAvatar.tsx
@@ -1,6 +1,8 @@
/*
+Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2018 New Vector Ltd
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");
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.
*/
-import React, { CSSProperties, useCallback, useContext, useEffect, useState } from "react";
+import React, { useCallback, useContext, useEffect, useState } from "react";
import classNames from "classnames";
import { ResizeMethod } from "matrix-js-sdk/src/@types/partials";
import { ClientEvent } from "matrix-js-sdk/src/client";
-import { SyncState } from "matrix-js-sdk/src/sync";
import * as AvatarLogic from "../../../Avatar";
+import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from "../elements/AccessibleButton";
import RoomContext from "../../../contexts/RoomContext";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
import { toPx } from "../../../utils/units";
import { _t } from "../../../languageHandler";
-import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
interface IProps {
- /** The name (first initial used as default) */
- name: string;
- /** ID for generating hash colours */
- idName?: string;
- /** onHover title text */
- title?: string;
- /** highest priority of them all, shortcut to set in urls[0] */
- url?: string;
- /** [highest_priority, ... , lowest_priority] */
- urls?: string[];
+ name: string; // The name (first initial used as default)
+ idName?: string; // ID for generating hash colours
+ title?: string; // onHover title text
+ url?: string; // highest priority of them all, shortcut to set in urls[0]
+ urls?: string[]; // [highest_priority, ... , lowest_priority]
width?: number;
height?: number;
- /** @deprecated not actually used */
+ // XXX: resizeMethod not actually used.
resizeMethod?: ResizeMethod;
- /** true to add default url */
- defaultToInitialLetter?: boolean;
- onClick?: React.ComponentPropsWithoutRef["onClick"];
+ defaultToInitialLetter?: boolean; // true to add default url
+ onClick?: React.MouseEventHandler;
inputRef?: React.RefObject;
className?: string;
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:
// imageUrls: [ props.url, ...props.urls ]
@@ -72,26 +66,11 @@ const calculateUrls = (url: string | undefined, urls: string[] | undefined, lowB
return Array.from(new Set(_urls));
};
-/**
- * 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] => {
+const useImageUrl = ({ url, urls }): [string, () => void] => {
// 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
const roomContext = useContext(RoomContext);
- const lowBandwidth = roomContext.lowBandwidth;
+ const lowBandwidth = roomContext ? roomContext.lowBandwidth : SettingsStore.getValue("lowBandwidth");
const [imageUrls, setUrls] = useState(calculateUrls(url, urls, lowBandwidth));
const [urlsIndex, setIndex] = useState(0);
@@ -106,10 +85,10 @@ const useImageUrl = ({
}, [url, JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps
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.
// 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) {
setIndex(0);
}
@@ -129,25 +108,46 @@ const BaseAvatar: React.FC = (props) => {
urls,
width = 40,
height = 40,
+ resizeMethod = "crop", // eslint-disable-line @typescript-eslint/no-unused-vars
defaultToInitialLetter = true,
onClick,
inputRef,
className,
- style: parentStyle,
- resizeMethod: _unused, // to keep it from being in `otherProps`
...otherProps
} = props;
- const style = {
- ...parentStyle,
- width: toPx(width),
- height: toPx(height),
- };
-
const [imageUrl, onError] = useImageUrl({ url, urls });
if (!imageUrl && defaultToInitialLetter && name) {
- const avatar = ;
+ const initialLetter = AvatarLogic.getInitialLetter(name);
+ const textNode = (
+
+ {initialLetter}
+
+ );
+ const imgNode = (
+
+ );
if (onClick) {
return (
@@ -159,9 +159,9 @@ const BaseAvatar: React.FC = (props) => {
className={classNames("mx_BaseAvatar", className)}
onClick={onClick}
inputRef={inputRef}
- style={style}
>
- {avatar}
+ {textNode}
+ {imgNode}
);
} else {
@@ -170,10 +170,10 @@ const BaseAvatar: React.FC = (props) => {
className={classNames("mx_BaseAvatar", className)}
ref={inputRef}
{...otherProps}
- style={style}
role="presentation"
>
- {avatar}
+ {textNode}
+ {imgNode}
);
}
@@ -187,7 +187,10 @@ const BaseAvatar: React.FC = (props) => {
src={imageUrl}
onClick={onClick}
onError={onError}
- style={style}
+ style={{
+ width: toPx(width),
+ height: toPx(height),
+ }}
title={title}
alt={_t("Avatar")}
inputRef={inputRef}
@@ -201,7 +204,10 @@ const BaseAvatar: React.FC = (props) => {
className={classNames("mx_BaseAvatar mx_BaseAvatar_image", className)}
src={imageUrl}
onError={onError}
- style={style}
+ style={{
+ width: toPx(width),
+ height: toPx(height),
+ }}
title={title}
alt=""
ref={inputRef}
@@ -214,31 +220,3 @@ const BaseAvatar: React.FC = (props) => {
export default BaseAvatar;
export type BaseAvatarType = React.FC;
-
-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 (
-
- {initialLetter}
-
- );
-};
diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx
index f493c58f8c..4813871455 100644
--- a/src/components/views/avatars/MemberAvatar.tsx
+++ b/src/components/views/avatars/MemberAvatar.tsx
@@ -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");
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 UserIdentifierCustomisations from "../../../customisations/UserIdentifier";
import { useRoomMemberProfile } from "../../../hooks/room/useRoomMemberProfile";
-import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload";
interface IProps extends Omit, "name" | "idName" | "url"> {
member: RoomMember | null;
@@ -33,13 +33,14 @@ interface IProps extends Omit, "name" |
width: number;
height: number;
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;
pushUserOnClick?: boolean;
title?: string;
- style?: React.CSSProperties;
- /** true to deny `useOnlyCurrentProfiles` usage. Default false. */
- forceHistorical?: boolean;
+ style?: any;
+ forceHistorical?: boolean; // true to deny `useOnlyCurrentProfiles` usage. Default false.
hideTitle?: boolean;
}
@@ -76,8 +77,8 @@ export default function MemberAvatar({
if (!title) {
title =
- UserIdentifierCustomisations.getDisplayUserIdentifier!(member.userId, {
- roomId: member.roomId,
+ UserIdentifierCustomisations.getDisplayUserIdentifier(member?.userId ?? "", {
+ roomId: member?.roomId ?? "",
}) ?? fallbackUserId;
}
}
@@ -87,6 +88,7 @@ export default function MemberAvatar({
{...props}
width={width}
height={height}
+ resizeMethod={resizeMethod}
name={name ?? ""}
title={hideTitle ? undefined : title}
idName={member?.userId ?? fallbackUserId}
@@ -94,9 +96,9 @@ export default function MemberAvatar({
onClick={
viewUserOnClick
? () => {
- dis.dispatch({
+ dis.dispatch({
action: Action.ViewUser,
- member: propsMember || undefined,
+ member: propsMember,
push: card.isCard,
});
}
diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx
index 4abfdbbf67..50389c7749 100644
--- a/src/components/views/avatars/RoomAvatar.tsx
+++ b/src/components/views/avatars/RoomAvatar.tsx
@@ -109,8 +109,7 @@ export default class RoomAvatar extends React.Component {
}
private onRoomAvatarClick = (): void => {
- const avatarMxc = this.props.room?.getMxcAvatarUrl();
- const avatarUrl = avatarMxc ? mediaFromMxc(avatarMxc).srcHttp : null;
+ const avatarUrl = Avatar.avatarUrlForRoom(this.props.room, null, null, null);
const params = {
src: avatarUrl,
name: this.props.room.name,
diff --git a/src/components/views/dialogs/polls/PollHistoryDialog.tsx b/src/components/views/dialogs/polls/PollHistoryDialog.tsx
new file mode 100644
index 0000000000..364f740c6c
--- /dev/null
+++ b/src/components/views/dialogs/polls/PollHistoryDialog.tsx
@@ -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 & {
+ roomId: string;
+};
+
+export const PollHistoryDialog: React.FC = ({ onFinished }) => {
+ return (
+
+ {/* @TODO(kerrya) to be implemented in PSG-906 */}
+
+ );
+};
diff --git a/src/components/views/messages/RoomCreate.tsx b/src/components/views/messages/RoomCreate.tsx
index ccad5ea11e..ffeb10f3ef 100644
--- a/src/components/views/messages/RoomCreate.tsx
+++ b/src/components/views/messages/RoomCreate.tsx
@@ -28,6 +28,7 @@ import EventTileBubble from "./EventTileBubble";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import RoomContext from "../../../contexts/RoomContext";
import { useRoomState } from "../../../hooks/useRoomState";
+import SettingsStore from "../../../settings/SettingsStore";
interface IProps {
/** The m.room.create MatrixEvent that this tile represents */
@@ -40,6 +41,8 @@ interface IProps {
* room.
*/
export const RoomCreate: React.FC = ({ mxEvent, timestamp }) => {
+ const msc3946ProcessDynamicPredecessor = SettingsStore.getValue("feature_dynamic_room_predecessors");
+
// Note: we ask the room for its predecessor here, instead of directly using
// the information inside mxEvent. This allows us the flexibility later to
// use a different predecessor (e.g. through MSC3946) and still display it
@@ -47,7 +50,10 @@ export const RoomCreate: React.FC = ({ mxEvent, timestamp }) => {
const roomContext = useContext(RoomContext);
const predecessor = useRoomState(
roomContext.room,
- useCallback((state) => state.findPredecessor(), []),
+ useCallback(
+ (state) => state.findPredecessor(msc3946ProcessDynamicPredecessor),
+ [msc3946ProcessDynamicPredecessor],
+ ),
);
const onLinkClicked = useCallback(
diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx
index 7e1974f962..e221106bb9 100644
--- a/src/components/views/right_panel/RoomSummaryCard.tsx
+++ b/src/components/views/right_panel/RoomSummaryCard.tsx
@@ -23,7 +23,7 @@ import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
import BaseCard, { Group } from "./BaseCard";
import { _t } from "../../../languageHandler";
import RoomAvatar from "../avatars/RoomAvatar";
-import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
+import AccessibleButton, { ButtonEvent, IAccessibleButtonProps } from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
import Modal from "../../../Modal";
@@ -51,6 +51,7 @@ import ExportDialog from "../dialogs/ExportDialog";
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
import PosthogTrackers from "../../../PosthogTrackers";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
+import { PollHistoryDialog } from "../dialogs/polls/PollHistoryDialog";
interface IProps {
room: Room;
@@ -61,14 +62,15 @@ interface IAppsSectionProps {
room: Room;
}
-interface IButtonProps {
+interface IButtonProps extends IAccessibleButtonProps {
className: string;
onClick(ev: ButtonEvent): void;
}
-const Button: React.FC = ({ children, className, onClick }) => {
+const Button: React.FC = ({ children, className, onClick, ...props }) => {
return (
@@ -281,6 +283,12 @@ const RoomSummaryCard: React.FC = ({ room, onClose }) => {
});
};
+ const onRoomPollHistoryClick = (): void => {
+ Modal.createDialog(PollHistoryDialog, {
+ roomId: room.roomId,
+ });
+ };
+
const isRoomEncrypted = useIsEncrypted(cli, room);
const roomContext = useContext(RoomContext);
const e2eStatus = roomContext.e2eStatus;
@@ -315,6 +323,8 @@ const RoomSummaryCard: React.FC = ({ room, onClose }) => {
const pinningEnabled = useFeatureEnabled("feature_pinning");
const pinCount = usePinnedEvents(pinningEnabled && room)?.length;
+ const isPollHistoryEnabled = useFeatureEnabled("feature_poll_history");
+
return (
@@ -327,6 +337,11 @@ const RoomSummaryCard: React.FC = ({ room, onClose }) => {
{_t("Files")}
)}
+ {!isVideoRoom && isPollHistoryEnabled && (
+
+ )}
{pinningEnabled && !isVideoRoom && (
@@ -230,16 +236,22 @@ exports[` with an invite without an invited email for a non-dm
R
+
diff --git a/test/components/views/rooms/__snapshots__/RoomTile-test.tsx.snap b/test/components/views/rooms/__snapshots__/RoomTile-test.tsx.snap
index 557d97c243..bcbb7932c6 100644
--- a/test/components/views/rooms/__snapshots__/RoomTile-test.tsx.snap
+++ b/test/components/views/rooms/__snapshots__/RoomTile-test.tsx.snap
@@ -15,16 +15,22 @@ exports[`RoomTile should render the room 1`] = `
!
+
- !room:example.com
-
-`;
-
-exports[`RoomPillPart matches snapshot (no avatar) 1`] = `
-
- !room:example.com
-
-`;
-
-exports[`UserPillPart matches snapshot (avatar) 1`] = `
-
- DisplayName
-
-`;
-
-exports[`UserPillPart matches snapshot (no avatar) 1`] = `
-
- DisplayName
-
-`;
diff --git a/test/editor/parts-test.ts b/test/editor/parts-test.ts
index 31c620c94a..534221ece3 100644
--- a/test/editor/parts-test.ts
+++ b/test/editor/parts-test.ts
@@ -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");
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.
*/
-import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix";
-
-import { EmojiPart, PartCreator, PlainPart } from "../../src/editor/parts";
-import DMRoomMap from "../../src/utils/DMRoomMap";
-import { stubClient } from "../test-utils";
+import { EmojiPart, PlainPart } from "../../src/editor/parts";
import { createPartCreator } from "./mock";
describe("editor/parts", () => {
@@ -44,67 +40,3 @@ describe("editor/parts", () => {
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
;
- let room: Room;
- let creator: PartCreator;
-
- beforeEach(() => {
- client = stubClient() as jest.Mocked;
- 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();
- });
-});
diff --git a/test/stores/room-list/RoomListStore-test.ts b/test/stores/room-list/RoomListStore-test.ts
index 04b99a8035..7ceb6393a2 100644
--- a/test/stores/room-list/RoomListStore-test.ts
+++ b/test/stores/room-list/RoomListStore-test.ts
@@ -14,14 +14,55 @@ See the License for the specific language governing permissions and
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 { 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 { stubClient } from "../../test-utils";
+import { stubClient, upsertRoomStateEvents } from "../../test-utils";
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 () => {
- const client = stubClient();
await (RoomListStore.instance as RoomListStoreClass).makeReady(client);
});
@@ -32,4 +73,54 @@ describe("RoomListStore", () => {
it.each(OrderedDefaultTagIDs)("defaults to activity ordering for %s=", (tagId) => {
expect(RoomListStore.instance.getListOrder(tagId)).toBe(ListAlgorithm.Importance);
});
+
+ function createStore(): { store: RoomListStoreClass; handleRoomUpdate: jest.Mock } {
+ 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);
+ });
});
diff --git a/test/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap b/test/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap
index 0645a993ae..c47170d3ed 100644
--- a/test/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap
+++ b/test/utils/exportUtils/__snapshots__/HTMLExport-test.ts.snap
@@ -25,7 +25,7 @@ exports[`HTMLExport should export 1`] = `