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 = ( + + ); + 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 ( - - ); -}; 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 && (