From 4c90e4775d455ac033c66b51e2b908889ec8bbee Mon Sep 17 00:00:00 2001 From: Tim Vahlbrock Date: Fri, 25 Oct 2024 15:33:51 +0200 Subject: [PATCH 01/14] Show Profile Pictures according to Votes on Poll Options --- src/components/views/messages/MPollBody.tsx | 58 +++++++++---------- src/components/views/polls/PollOption.tsx | 37 ++++++++---- .../polls/pollHistory/PollListItemEnded.tsx | 22 +++---- 3 files changed, 65 insertions(+), 52 deletions(-) diff --git a/src/components/views/messages/MPollBody.tsx b/src/components/views/messages/MPollBody.tsx index de9be6cf91d..fb919bd1d9c 100644 --- a/src/components/views/messages/MPollBody.tsx +++ b/src/components/views/messages/MPollBody.tsx @@ -6,34 +6,34 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { ReactNode } from "react"; +import { PollResponseEvent } from "matrix-js-sdk/src/extensible_events_v1/PollResponseEvent"; +import { PollAnswerSubevent, PollStartEvent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent"; import { logger } from "matrix-js-sdk/src/logger"; import { - MatrixEvent, - MatrixClient, - Relations, - Poll, - PollEvent, M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE, M_POLL_START, + MatrixClient, + MatrixEvent, + Poll, + PollEvent, + Relations, TimelineEvents, } from "matrix-js-sdk/src/matrix"; import { RelatedRelations } from "matrix-js-sdk/src/models/related-relations"; -import { PollStartEvent, PollAnswerSubevent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent"; -import { PollResponseEvent } from "matrix-js-sdk/src/extensible_events_v1/PollResponseEvent"; +import React, { ReactNode } from "react"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { _t } from "../../../languageHandler"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; import Modal from "../../../Modal"; -import { IBodyProps } from "./IBodyProps"; import { formatList } from "../../../utils/FormattingUtils"; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; import ErrorDialog from "../dialogs/ErrorDialog"; -import { GetRelationsForEvent } from "../rooms/EventTile"; import PollCreateDialog from "../elements/PollCreateDialog"; -import { MatrixClientPeg } from "../../../MatrixClientPeg"; import Spinner from "../elements/Spinner"; import { PollOption } from "../polls/PollOption"; +import { GetRelationsForEvent } from "../rooms/EventTile"; +import { IBodyProps } from "./IBodyProps"; interface IState { poll?: Poll; @@ -81,12 +81,12 @@ export function findTopAnswer(pollEvent: MatrixEvent, voteRelations: Relations): const userVotes: Map = collectUserVotes(allVotes(voteRelations)); - const votes: Map = countVotes(userVotes, poll); - const highestScore: number = Math.max(...votes.values()); + const votes: Map = countVotes(userVotes, poll); + const highestScore: number = Math.max(...Array.from(votes.values()).map((votes) => votes.length)); const bestAnswerIds: string[] = []; - for (const [answerId, score] of votes) { - if (score == highestScore) { + for (const [answerId, answerVotes] of votes) { + if (answerVotes.length == highestScore) { bestAnswerIds.push(answerId); } } @@ -243,7 +243,7 @@ export default class MPollBody extends React.Component { if (!this.state.voteRelations || !this.context) { return new Map(); } - return collectUserVotes(allVotes(this.state.voteRelations), this.context.getUserId(), this.state.selected); + return collectUserVotes(allVotes(this.state.voteRelations), null, this.state.selected); } /** @@ -273,10 +273,10 @@ export default class MPollBody extends React.Component { this.setState({ selected: newSelected }); } - private totalVotes(collectedVotes: Map): number { + private totalVotes(collectedVotes: Map): number { let sum = 0; for (const v of collectedVotes.values()) { - sum += v; + sum += v.length; } return sum; } @@ -294,7 +294,7 @@ export default class MPollBody extends React.Component { const userVotes = this.collectUserVotes(); const votes = countVotes(userVotes, pollEvent); const totalVotes = this.totalVotes(votes); - const winCount = Math.max(...votes.values()); + const winCount = Math.max(...Array.from(votes.values()).map((votes) => votes.length)); const userId = this.context.getSafeUserId(); const myVote = userVotes?.get(userId)?.answers[0]; const disclosed = M_POLL_KIND_DISCLOSED.matches(pollEvent.kind.name); @@ -335,7 +335,7 @@ export default class MPollBody extends React.Component { let answerVotes = 0; if (showResults) { - answerVotes = votes.get(answer.id) ?? 0; + answerVotes = votes.get(answer.id)?.length ?? 0; } const checked = @@ -348,7 +348,7 @@ export default class MPollBody extends React.Component { answer={answer} isChecked={checked} isEnded={poll.isEnded} - voteCount={answerVotes} + votes={votes.get(answer.id) ?? []} totalVoteCount={totalVotes} displayVoteCount={showResults} onOptionSelected={this.selectOption.bind(this)} @@ -392,7 +392,7 @@ export function allVotes(voteRelations: Relations): Array { /** * Figure out the correct vote for each user. * @param userResponses current vote responses in the poll - * @param {string?} userId The userId for which the `selected` option will apply to. + * @param {string?} user The userId for which the `selected` option will apply to. * Should be set to the current user ID. * @param {string?} selected Local echo selected option for the userId * @returns a Map of user ID to their vote info @@ -418,19 +418,17 @@ export function collectUserVotes( return userVotes; } -export function countVotes(userVotes: Map, pollStart: PollStartEvent): Map { - const collected = new Map(); +export function countVotes(userVotes: Map, pollStart: PollStartEvent): Map { + const collected = new Map(); for (const response of userVotes.values()) { const tempResponse = PollResponseEvent.from(response.answers, "$irrelevant"); tempResponse.validateAgainst(pollStart); if (!tempResponse.spoiled) { for (const answerId of tempResponse.answerIds) { - if (collected.has(answerId)) { - collected.set(answerId, collected.get(answerId)! + 1); - } else { - collected.set(answerId, 1); - } + const previousVotes = collected.get(answerId) ?? []; + previousVotes.push(response); + collected.set(answerId, previousVotes); } } } diff --git a/src/components/views/polls/PollOption.tsx b/src/components/views/polls/PollOption.tsx index c84653c2a12..c0b6beef97c 100644 --- a/src/components/views/polls/PollOption.tsx +++ b/src/components/views/polls/PollOption.tsx @@ -6,28 +6,43 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { ReactNode } from "react"; import classNames from "classnames"; import { PollAnswerSubevent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent"; +import React, { ReactNode, useContext } from "react"; -import { _t } from "../../../languageHandler"; import { Icon as TrophyIcon } from "../../../../res/img/element-icons/trophy.svg"; +import RoomContext from "../../../contexts/RoomContext"; +import { useRoomMembers } from "../../../hooks/useRoomMembers"; +import { _t } from "../../../languageHandler"; +import FacePile from "../elements/FacePile"; import StyledRadioButton from "../elements/StyledRadioButton"; +import { UserVote } from "../messages/MPollBody"; type PollOptionContentProps = { answer: PollAnswerSubevent; - voteCount: number; + votes: UserVote[]; displayVoteCount?: boolean; isWinner?: boolean; }; -const PollOptionContent: React.FC = ({ isWinner, answer, voteCount, displayVoteCount }) => { - const votesText = displayVoteCount ? _t("timeline|m.poll|count_of_votes", { count: voteCount }) : ""; +const PollOptionContent: React.FC = ({ isWinner, answer, votes, displayVoteCount }) => { + const votesText = displayVoteCount ? _t("timeline|m.poll|count_of_votes", { count: votes.length }) : ""; + const room = useContext(RoomContext).room!; + const members = useRoomMembers(room); + return (
{answer.text}
{isWinner && } - {votesText} +
+ votes.some((v) => v.sender === m.userId))} + size="24px" + overflow={false} + style={{ marginRight: "10px" }} + /> + {votesText} +
); @@ -42,7 +57,7 @@ interface PollOptionProps extends PollOptionContentProps { children?: ReactNode; } -const EndedPollOption: React.FC> = ({ +const EndedPollOption: React.FC> = ({ isChecked, children, answer, @@ -57,7 +72,7 @@ const EndedPollOption: React.FC ); -const ActivePollOption: React.FC> = ({ +const ActivePollOption: React.FC> = ({ pollId, isChecked, children, @@ -78,7 +93,7 @@ const ActivePollOption: React.FC = ({ pollId, answer, - voteCount, + votes: voteCount, totalVoteCount, displayVoteCount, isEnded, @@ -91,7 +106,7 @@ export const PollOption: React.FC = ({ mx_PollOption_ended: isEnded, }); const isWinner = isEnded && isChecked; - const answerPercent = totalVoteCount === 0 ? 0 : Math.round((100.0 * voteCount) / totalVoteCount); + const answerPercent = totalVoteCount === 0 ? 0 : Math.round((100.0 * voteCount.length) / totalVoteCount); const PollOptionWrapper = isEnded ? EndedPollOption : ActivePollOption; return (
onOptionSelected?.(answer.id)}> @@ -104,7 +119,7 @@ export const PollOption: React.FC = ({ diff --git a/src/components/views/polls/pollHistory/PollListItemEnded.tsx b/src/components/views/polls/pollHistory/PollListItemEnded.tsx index e2f80e8eba5..41dcc27ef4b 100644 --- a/src/components/views/polls/pollHistory/PollListItemEnded.tsx +++ b/src/components/views/polls/pollHistory/PollListItemEnded.tsx @@ -6,15 +6,15 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { useEffect, useState } from "react"; +import { Tooltip } from "@vector-im/compound-web"; import { PollAnswerSubevent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent"; import { MatrixEvent, Poll, PollEvent, Relations } from "matrix-js-sdk/src/matrix"; -import { Tooltip } from "@vector-im/compound-web"; +import React, { useEffect, useState } from "react"; import { Icon as PollIcon } from "../../../../../res/img/element-icons/room/composer/poll.svg"; -import { _t } from "../../../../languageHandler"; import { formatLocalDateShort } from "../../../../DateUtils"; -import { allVotes, collectUserVotes, countVotes } from "../../messages/MPollBody"; +import { _t } from "../../../../languageHandler"; +import { allVotes, collectUserVotes, countVotes, UserVote } from "../../messages/MPollBody"; import { PollOption } from "../../polls/PollOption"; import { Caption } from "../../typography/Caption"; @@ -27,23 +27,23 @@ interface Props { type EndedPollState = { winningAnswers: { answer: PollAnswerSubevent; - voteCount: number; + votes: UserVote[]; }[]; totalVoteCount: number; }; const getWinningAnswers = (poll: Poll, responseRelations: Relations): EndedPollState => { const userVotes = collectUserVotes(allVotes(responseRelations)); const votes = countVotes(userVotes, poll.pollEvent); - const totalVoteCount = [...votes.values()].reduce((sum, vote) => sum + vote, 0); - const winCount = Math.max(...votes.values()); + const totalVoteCount = [...votes.values()].reduce((sum, vote) => sum + vote.length, 0); + const winCount = Math.max(...Array.from(votes.values()).map(v => v.length)); return { totalVoteCount, winningAnswers: poll.pollEvent.answers - .filter((answer) => votes.get(answer.id) === winCount) + .filter((answer) => votes.get(answer.id)?.length === winCount) .map((answer) => ({ answer, - voteCount: votes.get(answer.id) || 0, + votes: votes.get(answer.id) || [], })), }; }; @@ -100,11 +100,11 @@ export const PollListItemEnded: React.FC = ({ event, poll, onClick }) =>
{!!winningAnswers?.length && (
- {winningAnswers?.map(({ answer, voteCount }) => ( + {winningAnswers?.map(({ answer, votes }) => ( Date: Sat, 26 Oct 2024 13:08:52 +0200 Subject: [PATCH 02/14] Show vote face pile only if there are less than five members in the room --- src/components/views/polls/PollOption.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/components/views/polls/PollOption.tsx b/src/components/views/polls/PollOption.tsx index c0b6beef97c..0c624eac9b0 100644 --- a/src/components/views/polls/PollOption.tsx +++ b/src/components/views/polls/PollOption.tsx @@ -18,6 +18,8 @@ import FacePile from "../elements/FacePile"; import StyledRadioButton from "../elements/StyledRadioButton"; import { UserVote } from "../messages/MPollBody"; +const MAXIMUM_MEMBERS_FOR_FACE_PILE = 5; + type PollOptionContentProps = { answer: PollAnswerSubevent; votes: UserVote[]; @@ -25,7 +27,7 @@ type PollOptionContentProps = { isWinner?: boolean; }; const PollOptionContent: React.FC = ({ isWinner, answer, votes, displayVoteCount }) => { - const votesText = displayVoteCount ? _t("timeline|m.poll|count_of_votes", { count: votes.length }) : ""; + const votesText = displayVoteCount ? : ""; const room = useContext(RoomContext).room!; const members = useRoomMembers(room); @@ -35,12 +37,15 @@ const PollOptionContent: React.FC = ({ isWinner, answer,
{isWinner && }
- votes.some((v) => v.sender === m.userId))} - size="24px" - overflow={false} - style={{ marginRight: "10px" }} - /> + {displayVoteCount + && members.length <= MAXIMUM_MEMBERS_FOR_FACE_PILE + && votes.some((v) => v.sender === m.userId))} + size="24px" + overflow={false} + style={{ marginRight: "10px" }} + /> + } {votesText}
From aa4666469ead046530ba5bfcac43854a17d10837 Mon Sep 17 00:00:00 2001 From: Tim Vahlbrock Date: Sat, 26 Oct 2024 22:11:45 +0200 Subject: [PATCH 03/14] Fix winner icon and votes text --- src/components/views/polls/PollOption.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/views/polls/PollOption.tsx b/src/components/views/polls/PollOption.tsx index 0c624eac9b0..45c06cfbec2 100644 --- a/src/components/views/polls/PollOption.tsx +++ b/src/components/views/polls/PollOption.tsx @@ -27,7 +27,7 @@ type PollOptionContentProps = { isWinner?: boolean; }; const PollOptionContent: React.FC = ({ isWinner, answer, votes, displayVoteCount }) => { - const votesText = displayVoteCount ? : ""; + const votesText = displayVoteCount ? _t("timeline|m.poll|count_of_votes", { count: votes.length }) : ""; const room = useContext(RoomContext).room!; const members = useRoomMembers(room); @@ -35,7 +35,6 @@ const PollOptionContent: React.FC = ({ isWinner, answer,
{answer.text}
- {isWinner && }
{displayVoteCount && members.length <= MAXIMUM_MEMBERS_FOR_FACE_PILE @@ -46,7 +45,10 @@ const PollOptionContent: React.FC = ({ isWinner, answer, style={{ marginRight: "10px" }} /> } - {votesText} + + {isWinner && } + {votesText} +
From fa76ae64f867efe8e6df1a1b5de9ebce5011d698 Mon Sep 17 00:00:00 2001 From: Tim Vahlbrock Date: Sun, 27 Oct 2024 16:23:45 +0100 Subject: [PATCH 04/14] Revert "Enable React StrictMode (#28258)" This reverts commit da5c97f9fafe7e92f00a30ed49f1a15c2386defb. --- src/Modal.tsx | 54 +++++++++---------- .../views/elements/PersistedElement.tsx | 18 +++---- src/components/views/messages/TextualBody.tsx | 17 ++---- src/utils/pillify.tsx | 28 +++++----- src/utils/tooltipify.tsx | 14 +++-- src/vector/app.tsx | 26 +++++---- src/vector/init.tsx | 10 ++-- 7 files changed, 71 insertions(+), 96 deletions(-) diff --git a/src/Modal.tsx b/src/Modal.tsx index 9e6a7672bea..53a1935294f 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { StrictMode } from "react"; +import React from "react"; import ReactDOM from "react-dom"; import classNames from "classnames"; import { IDeferred, defer, sleep } from "matrix-js-sdk/src/utils"; @@ -416,20 +416,18 @@ export class ModalManager extends TypedEventEmitter - -
- -
{this.staticModal.elem}
-
-
-
- - + +
+ +
{this.staticModal.elem}
+
+
+
+ ); ReactDOM.render(staticDialog, ModalManager.getOrCreateStaticContainer()); @@ -445,20 +443,18 @@ export class ModalManager extends TypedEventEmitter - -
- -
{modal.elem}
-
-
-
- - + +
+ +
{modal.elem}
+
+
+
+ ); setTimeout(() => ReactDOM.render(dialog, ModalManager.getOrCreateContainer()), 0); diff --git a/src/components/views/elements/PersistedElement.tsx b/src/components/views/elements/PersistedElement.tsx index 20584f3794c..1b7b6543e95 100644 --- a/src/components/views/elements/PersistedElement.tsx +++ b/src/components/views/elements/PersistedElement.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { MutableRefObject, ReactNode, StrictMode } from "react"; +import React, { MutableRefObject, ReactNode } from "react"; import ReactDOM from "react-dom"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { TooltipProvider } from "@vector-im/compound-web"; @@ -167,15 +167,13 @@ export default class PersistedElement extends React.Component { private renderApp(): void { const content = ( - - - -
- {this.props.children} -
-
-
-
+ + +
+ {this.props.children} +
+
+
); ReactDOM.render(content, getOrCreateContainer("mx_persistedElement_" + this.props.persistKey)); diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 7955d964a32..0e0c29747f9 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { createRef, SyntheticEvent, MouseEvent, StrictMode } from "react"; +import React, { createRef, SyntheticEvent, MouseEvent } from "react"; import ReactDOM from "react-dom"; import { MsgType } from "matrix-js-sdk/src/matrix"; import { TooltipProvider } from "@vector-im/compound-web"; @@ -118,12 +118,7 @@ export default class TextualBody extends React.Component { // Insert containing div in place of
 block
         pre.parentNode?.replaceChild(root, pre);
 
-        ReactDOM.render(
-            
-                {pre}
-            ,
-            root,
-        );
+        ReactDOM.render({pre}, root);
     }
 
     public componentDidUpdate(prevProps: Readonly): void {
@@ -197,11 +192,9 @@ export default class TextualBody extends React.Component {
                 const reason = node.getAttribute("data-mx-spoiler") ?? undefined;
                 node.removeAttribute("data-mx-spoiler"); // we don't want to recurse
                 const spoiler = (
-                    
-                        
-                            
-                        
-                    
+                    
+                        
+                    
                 );
 
                 ReactDOM.render(spoiler, spoilerContainer);
diff --git a/src/utils/pillify.tsx b/src/utils/pillify.tsx
index 063012d16f3..2c19f114917 100644
--- a/src/utils/pillify.tsx
+++ b/src/utils/pillify.tsx
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
 Please see LICENSE files in the repository root for full details.
 */
 
-import React, { StrictMode } from "react";
+import React from "react";
 import ReactDOM from "react-dom";
 import { PushProcessor } from "matrix-js-sdk/src/pushprocessor";
 import { MatrixClient, MatrixEvent, RuleId } from "matrix-js-sdk/src/matrix";
@@ -76,11 +76,9 @@ export function pillifyLinks(
                 const pillContainer = document.createElement("span");
 
                 const pill = (
-                    
-                        
-                            
-                        
-                    
+                    
+                        
+                    
                 );
 
                 ReactDOM.render(pill, pillContainer);
@@ -135,16 +133,14 @@ export function pillifyLinks(
 
                         const pillContainer = document.createElement("span");
                         const pill = (
-                            
-                                
-                                    
-                                
-                            
+                            
+                                
+                            
                         );
 
                         ReactDOM.render(pill, pillContainer);
diff --git a/src/utils/tooltipify.tsx b/src/utils/tooltipify.tsx
index bcda256a9c8..65ce431a976 100644
--- a/src/utils/tooltipify.tsx
+++ b/src/utils/tooltipify.tsx
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
 Please see LICENSE files in the repository root for full details.
 */
 
-import React, { StrictMode } from "react";
+import React from "react";
 import ReactDOM from "react-dom";
 import { TooltipProvider } from "@vector-im/compound-web";
 
@@ -53,13 +53,11 @@ export function tooltipifyLinks(rootNodes: ArrayLike, ignoredNodes: Ele
             // wrapping the link with the LinkWithTooltip component, keeping the same children. Ideally we'd do this
             // without the superfluous span but this is not something React trivially supports at this time.
             const tooltip = (
-                
-                    
-                        
-                            
-                        
-                    
-                
+                
+                    
+                        
+                    
+                
             );
 
             ReactDOM.render(tooltip, node);
diff --git a/src/vector/app.tsx b/src/vector/app.tsx
index da0f3f3941f..0c2230bbb8a 100644
--- a/src/vector/app.tsx
+++ b/src/vector/app.tsx
@@ -12,7 +12,7 @@ Please see LICENSE files in the repository root for full details.
 
 // To ensure we load the browser-matrix version first
 import "matrix-js-sdk/src/browser-index";
-import React, { ReactElement, StrictMode } from "react";
+import React, { ReactElement } from "react";
 import { logger } from "matrix-js-sdk/src/logger";
 import { createClient, AutoDiscovery, ClientConfig } from "matrix-js-sdk/src/matrix";
 import { WrapperLifecycle, WrapperOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WrapperLifecycle";
@@ -111,19 +111,17 @@ export async function loadApp(fragParams: {}, matrixChatRef: React.Ref
-            
-                
-            
+            
         
     );
 }
diff --git a/src/vector/init.tsx b/src/vector/init.tsx
index 2028f9af365..da9827cb55b 100644
--- a/src/vector/init.tsx
+++ b/src/vector/init.tsx
@@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
 */
 
 import * as ReactDOM from "react-dom";
-import React, { StrictMode } from "react";
+import * as React from "react";
 import { logger } from "matrix-js-sdk/src/logger";
 
 import * as languageHandler from "../languageHandler";
@@ -105,9 +105,7 @@ export async function showError(title: string, messages?: string[]): Promise
-            
-        ,
+        ,
         document.getElementById("matrixchat"),
     );
 }
@@ -118,9 +116,7 @@ export async function showIncompatibleBrowser(onAccept: () => void): Promise
-            
-        ,
+        ,
         document.getElementById("matrixchat"),
     );
 }

From a5fdd41b03ee31c99c7227605a3bd304030a5218 Mon Sep 17 00:00:00 2001
From: Tim Vahlbrock 
Date: Fri, 1 Nov 2024 00:33:14 +0100
Subject: [PATCH 05/14] Add dialog to display detailed votes

---
 .../views/dialogs/PollResultsDialog.tsx       | 60 +++++++++++++++++++
 src/components/views/messages/MPollBody.tsx   | 13 +++-
 2 files changed, 72 insertions(+), 1 deletion(-)
 create mode 100644 src/components/views/dialogs/PollResultsDialog.tsx

diff --git a/src/components/views/dialogs/PollResultsDialog.tsx b/src/components/views/dialogs/PollResultsDialog.tsx
new file mode 100644
index 00000000000..d16c1ba5840
--- /dev/null
+++ b/src/components/views/dialogs/PollResultsDialog.tsx
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 New Vector Ltd.
+Copyright 2021, 2022 The Matrix.org Foundation C.I.C.
+
+SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
+Please see LICENSE files in the repository root for full details.
+*/
+
+import { PollStartEvent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent";
+import { RoomMember } from "matrix-js-sdk/src/matrix";
+import React from "react";
+
+import Modal from "../../../Modal";
+import MemberAvatar from "../avatars/MemberAvatar";
+import { UserVote } from "../messages/MPollBody";
+import BaseDialog from "./BaseDialog";
+
+interface IProps {
+    pollEvent: PollStartEvent;
+    votes: Map;
+    members: RoomMember[];
+}
+
+export default function PollResultsDialog (props: IProps): JSX.Element {
+    return (
+         Modal.closeCurrentModal()}
+        >
+            {
+                props.pollEvent.answers.map((answer, answerIndex) => {
+                    const votes = props.votes.get(answer.id) || [];
+
+                    if(votes.length === 0) return;
+
+                    return (
+                        
+
+ {answer.text} + {votes.length} votes +
+ {votes.length === 0 &&
No one voted for this.
} + {votes.map((vote) => { + const member = props.members.find(m => m.userId === vote.sender); + if (!member) return null; + return
+
+
+ {member.name} +
; + })} + {answerIndex < props.pollEvent.answers.length - 1 &&
} +
+ ); + }) + } +
+ ); +} diff --git a/src/components/views/messages/MPollBody.tsx b/src/components/views/messages/MPollBody.tsx index fb919bd1d9c..7bf921f5dd7 100644 --- a/src/components/views/messages/MPollBody.tsx +++ b/src/components/views/messages/MPollBody.tsx @@ -29,6 +29,7 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg"; import Modal from "../../../Modal"; import { formatList } from "../../../utils/FormattingUtils"; import ErrorDialog from "../dialogs/ErrorDialog"; +import PollResultsDialog from "../dialogs/PollResultsDialog"; import PollCreateDialog from "../elements/PollCreateDialog"; import Spinner from "../elements/Spinner"; import { PollOption } from "../polls/PollOption"; @@ -324,6 +325,16 @@ export default class MPollBody extends React.Component { ({_t("common|edited")}) ) : null; + const showDetailedVotes = (): void => { + if(!showResults) return; + + Modal.createDialog(PollResultsDialog, { + pollEvent, + votes, + members: this.context.getRoom(this.props.mxEvent.getRoomId())?.getJoinedMembers() ?? [], + }); + }; + return (

@@ -357,7 +368,7 @@ export default class MPollBody extends React.Component { })}

- {totalText} + showDetailedVotes()}>{totalText} {isFetchingResponses && }
From baabfbcfb93aad2003bf7e97cde6316b5cfdd3ff Mon Sep 17 00:00:00 2001 From: Tim Vahlbrock Date: Fri, 1 Nov 2024 00:41:59 +0100 Subject: [PATCH 06/14] Refactor PollResultsDialog --- .../views/dialogs/PollResultsDialog.tsx | 62 ++++++++++++------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/src/components/views/dialogs/PollResultsDialog.tsx b/src/components/views/dialogs/PollResultsDialog.tsx index d16c1ba5840..f1c57fac17e 100644 --- a/src/components/views/dialogs/PollResultsDialog.tsx +++ b/src/components/views/dialogs/PollResultsDialog.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import { PollStartEvent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent"; +import { PollAnswerSubevent, PollStartEvent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent"; import { RoomMember } from "matrix-js-sdk/src/matrix"; import React from "react"; @@ -28,33 +28,49 @@ export default function PollResultsDialog (props: IProps): JSX.Element { onFinished={() => Modal.closeCurrentModal()} > { - props.pollEvent.answers.map((answer, answerIndex) => { + props.pollEvent.answers.map((answer) => { const votes = props.votes.get(answer.id) || []; - if(votes.length === 0) return; - return ( -
-
- {answer.text} - {votes.length} votes -
- {votes.length === 0 &&
No one voted for this.
} - {votes.map((vote) => { - const member = props.members.find(m => m.userId === vote.sender); - if (!member) return null; - return
-
-
- {member.name} -
; - })} - {answerIndex < props.pollEvent.answers.length - 1 &&
} -
- ); + return ; }) } ); } + +function AnswerEntry(props: { + answer: PollAnswerSubevent; + members: RoomMember[]; + votes: UserVote[]; +}): JSX.Element { + const {answer, members, votes} = props; + return ( +
+
+ {answer.text} + {votes.length} votes +
+ {votes.length === 0 &&
No one voted for this.
} + {votes.map((vote) => { + const member = members.find(m => m.userId === vote.sender); + if(member) return ; + })} +
+ ); +} + +function VoterEntry(props: {vote: UserVote; member: RoomMember}): JSX.Element { + const {vote, member} = props; + return
+
+
+ {member.name} +
; +} From b739f23d295dda561272a58fcef8b2ebfad5edbe Mon Sep 17 00:00:00 2001 From: Tim Vahlbrock Date: Fri, 1 Nov 2024 01:03:54 +0100 Subject: [PATCH 07/14] Move styling of results dialog to css --- res/css/_components.pcss | 1 + .../dialogs/polls/_PollResultsDialog.pcss | 24 +++++++++++++++++ .../views/dialogs/PollResultsDialog.tsx | 26 +++++++++++-------- 3 files changed, 40 insertions(+), 11 deletions(-) create mode 100644 res/css/components/views/dialogs/polls/_PollResultsDialog.pcss diff --git a/res/css/_components.pcss b/res/css/_components.pcss index c0dd2ee0b02..15dededcd33 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -18,6 +18,7 @@ @import "./components/views/dialogs/polls/_PollDetailHeader.pcss"; @import "./components/views/dialogs/polls/_PollListItem.pcss"; @import "./components/views/dialogs/polls/_PollListItemEnded.pcss"; +@import "./components/views/dialogs/polls/_PollResultsDialog.pcss"; @import "./components/views/elements/_AppPermission.pcss"; @import "./components/views/elements/_AppWarning.pcss"; @import "./components/views/elements/_FilterDropdown.pcss"; diff --git a/res/css/components/views/dialogs/polls/_PollResultsDialog.pcss b/res/css/components/views/dialogs/polls/_PollResultsDialog.pcss new file mode 100644 index 00000000000..3e349c33eae --- /dev/null +++ b/res/css/components/views/dialogs/polls/_PollResultsDialog.pcss @@ -0,0 +1,24 @@ +.mx_AnswerEntry:not(:last-child) { + margin-bottom: $spacing-8; +} + +.mx_AnswerEntry_Header { + display: flex; + align-items: center; + margin-bottom: $spacing-8; +} + +.mx_AnswerEntry_Header_answerName { + font-weight: bolder; + flex-grow: 1 +} + +.mx_VoterEntry { + display: flex; + align-items: center; + margin-left: $spacing-16; +} + +.mx_VoterEntry_AvatarWrapper { + margin-right: $spacing-8; +} diff --git a/src/components/views/dialogs/PollResultsDialog.tsx b/src/components/views/dialogs/PollResultsDialog.tsx index f1c57fac17e..9367c143c2e 100644 --- a/src/components/views/dialogs/PollResultsDialog.tsx +++ b/src/components/views/dialogs/PollResultsDialog.tsx @@ -21,7 +21,7 @@ interface IProps { members: RoomMember[]; } -export default function PollResultsDialog (props: IProps): JSX.Element { +export default function PollResultsDialog(props: IProps): JSX.Element { return ( { const votes = props.votes.get(answer.id) || []; - if(votes.length === 0) return; + if (votes.length === 0) return; return -
- {answer.text} +
+
+ {answer.text} {votes.length} votes
{votes.length === 0 &&
No one voted for this.
} {votes.map((vote) => { const member = members.find(m => m.userId === vote.sender); - if(member) return ; + if (member) return ; })}
); } -function VoterEntry(props: {vote: UserVote; member: RoomMember}): JSX.Element { - const {vote, member} = props; - return
-
+function VoterEntry(props: { vote: UserVote; member: RoomMember }): JSX.Element { + const { vote, member } = props; + return
+
{member.name} From 653be23a0d390f3e03dde457ff39f70b5182bc9d Mon Sep 17 00:00:00 2001 From: Tim Vahlbrock Date: Fri, 1 Nov 2024 01:26:43 +0100 Subject: [PATCH 08/14] Show text using i18n and modify existing usages --- src/components/views/dialogs/PollResultsDialog.tsx | 3 ++- src/i18n/strings/en_EN.json | 14 ++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/components/views/dialogs/PollResultsDialog.tsx b/src/components/views/dialogs/PollResultsDialog.tsx index 9367c143c2e..7e8e9929d40 100644 --- a/src/components/views/dialogs/PollResultsDialog.tsx +++ b/src/components/views/dialogs/PollResultsDialog.tsx @@ -10,6 +10,7 @@ import { PollAnswerSubevent, PollStartEvent } from "matrix-js-sdk/src/extensible import { RoomMember } from "matrix-js-sdk/src/matrix"; import React from "react"; +import { _t } from "../../../languageHandler"; import Modal from "../../../Modal"; import MemberAvatar from "../avatars/MemberAvatar"; import { UserVote } from "../messages/MPollBody"; @@ -54,7 +55,7 @@ function AnswerEntry(props: {
{answer.text} - {votes.length} votes + {_t("poll|result_dialog|count_of_votes", { count: votes.length })}
{votes.length === 0 &&
No one voted for this.
} {votes.map((vote) => { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7ba141c784d..77d2e79be13 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1714,6 +1714,12 @@ "options_heading": "Create options", "options_label": "Option %(number)s", "options_placeholder": "Write an option", + "result_dialog": { + "count_of_votes": { + "one": "%(count)s vote", + "other": "%(count)s votes" + } + }, "topic_heading": "What is your poll question or topic?", "topic_label": "Question or topic", "topic_placeholder": "Write something…", @@ -1723,8 +1729,8 @@ "other": "%(count)s votes cast. Vote to see the results" }, "total_n_votes_voted": { - "one": "Based on %(count)s vote", - "other": "Based on %(count)s votes" + "one": "Based on %(count)s vote. Click here to see the results", + "other": "Based on %(count)s votes. Click here to see the results" }, "total_no_votes": "No votes cast", "total_not_ended": "Results will be visible when the poll is ended", @@ -1875,8 +1881,8 @@ "other": "There are no past polls for the past %(count)s days. Load more polls to view polls for previous months" }, "final_result": { - "one": "Final result based on %(count)s vote", - "other": "Final result based on %(count)s votes" + "one": "Final result based on %(count)s vote. Click here to see the results", + "other": "Final result based on %(count)s votes. Click here to see the results" }, "load_more": "Load more polls", "loading": "Loading polls", From 20832131311a8c5c6c72757b722cac6e93a0bd63 Mon Sep 17 00:00:00 2001 From: Tim Vahlbrock Date: Fri, 1 Nov 2024 12:15:53 +0100 Subject: [PATCH 09/14] Fix existing tests but 3 --- src/components/views/messages/MPollBody.tsx | 4 +- src/components/views/polls/PollOption.tsx | 9 +- src/i18n/strings/en_EN.json | 8 +- .../views/messages/MPollBody-test.tsx | 56 +- .../__snapshots__/MPollBody-test.tsx.snap | 811 ++++++++++++++---- 5 files changed, 685 insertions(+), 203 deletions(-) diff --git a/src/components/views/messages/MPollBody.tsx b/src/components/views/messages/MPollBody.tsx index 7bf921f5dd7..67c94fd988e 100644 --- a/src/components/views/messages/MPollBody.tsx +++ b/src/components/views/messages/MPollBody.tsx @@ -367,8 +367,8 @@ export default class MPollBody extends React.Component { ); })}
-
- showDetailedVotes()}>{totalText} +
+ showDetailedVotes()}>{totalText} {isFetchingResponses && }
diff --git a/src/components/views/polls/PollOption.tsx b/src/components/views/polls/PollOption.tsx index 45c06cfbec2..47c805e7186 100644 --- a/src/components/views/polls/PollOption.tsx +++ b/src/components/views/polls/PollOption.tsx @@ -12,7 +12,6 @@ import React, { ReactNode, useContext } from "react"; import { Icon as TrophyIcon } from "../../../../res/img/element-icons/trophy.svg"; import RoomContext from "../../../contexts/RoomContext"; -import { useRoomMembers } from "../../../hooks/useRoomMembers"; import { _t } from "../../../languageHandler"; import FacePile from "../elements/FacePile"; import StyledRadioButton from "../elements/StyledRadioButton"; @@ -28,13 +27,13 @@ type PollOptionContentProps = { }; const PollOptionContent: React.FC = ({ isWinner, answer, votes, displayVoteCount }) => { const votesText = displayVoteCount ? _t("timeline|m.poll|count_of_votes", { count: votes.length }) : ""; - const room = useContext(RoomContext).room!; - const members = useRoomMembers(room); + const room = useContext(RoomContext).room; + const members = room?.getJoinedMembers() || []; return (
{answer.text}
-
+
{displayVoteCount && members.length <= MAXIMUM_MEMBERS_FOR_FACE_PILE @@ -45,7 +44,7 @@ const PollOptionContent: React.FC = ({ isWinner, answer, style={{ marginRight: "10px" }} /> } - + {isWinner && } {votesText} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f31af3983ac..8ed4e17ec7e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1727,8 +1727,8 @@ "other": "%(count)s votes cast. Vote to see the results" }, "total_n_votes_voted": { - "one": "Based on %(count)s vote. Click here to see the results", - "other": "Based on %(count)s votes. Click here to see the results" + "one": "Based on %(count)s vote. Click here to see full results", + "other": "Based on %(count)s votes. Click here to see full results" }, "total_no_votes": "No votes cast", "total_not_ended": "Results will be visible when the poll is ended", @@ -1879,8 +1879,8 @@ "other": "There are no past polls for the past %(count)s days. Load more polls to view polls for previous months" }, "final_result": { - "one": "Final result based on %(count)s vote. Click here to see the results", - "other": "Final result based on %(count)s votes. Click here to see the results" + "one": "Final result based on %(count)s vote. Click here to see full results", + "other": "Final result based on %(count)s votes. Click here to see full results" }, "load_more": "Load more polls", "loading": "Loading polls", diff --git a/test/unit-tests/components/views/messages/MPollBody-test.tsx b/test/unit-tests/components/views/messages/MPollBody-test.tsx index a4e3fc1e106..eb94b2906ee 100644 --- a/test/unit-tests/components/views/messages/MPollBody-test.tsx +++ b/test/unit-tests/components/views/messages/MPollBody-test.tsx @@ -6,26 +6,30 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React from "react"; import { fireEvent, render, RenderResult } from "jest-matrix-react"; import { - MatrixEvent, - Relations, M_POLL_KIND_DISCLOSED, M_POLL_KIND_UNDISCLOSED, M_POLL_RESPONSE, M_POLL_START, - PollStartEventContent, - PollAnswer, M_TEXT, + MatrixEvent, + PollAnswer, + PollStartEventContent, + Relations } from "matrix-js-sdk/src/matrix"; +import React from "react"; +import { IBodyProps } from "../../../../../src/components/views/messages/IBodyProps"; import MPollBody, { allVotes, findTopAnswer, isPollEnded, } from "../../../../../src/components/views/messages/MPollBody"; -import { IBodyProps } from "../../../../../src/components/views/messages/IBodyProps"; +import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; +import * as languageHandler from "../../../../../src/languageHandler"; +import { MediaEventHelper } from "../../../../../src/utils/MediaEventHelper"; +import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks"; import { flushPromises, getMockClientWithEventEmitter, @@ -33,10 +37,6 @@ import { mockClientMethodsUser, setupRoomWithPollEvents, } from "../../../../test-utils"; -import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; -import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks"; -import { MediaEventHelper } from "../../../../../src/utils/MediaEventHelper"; -import * as languageHandler from "../../../../../src/languageHandler"; const CHECKED = "mx_PollOption_checked"; const userId = "@me:example.com"; @@ -99,7 +99,7 @@ describe("MPollBody", () => { expect(votesCount(renderResult, "poutine")).toBe("1 vote"); expect(votesCount(renderResult, "italian")).toBe("0 votes"); expect(votesCount(renderResult, "wings")).toBe("1 vote"); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 4 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 4 votes. Click here to see full results"); }); it("ignores end poll events from unauthorised users", async () => { @@ -118,7 +118,7 @@ describe("MPollBody", () => { expect(votesCount(renderResult, "poutine")).toBe("1 vote"); expect(votesCount(renderResult, "italian")).toBe("0 votes"); expect(votesCount(renderResult, "wings")).toBe("1 vote"); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 4 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 4 votes. Click here to see full results"); }); it("hides scores if I have not voted", async () => { @@ -159,7 +159,7 @@ describe("MPollBody", () => { expect(votesCount(renderResult, "poutine")).toBe("1 vote"); expect(votesCount(renderResult, "italian")).toBe("0 votes"); expect(votesCount(renderResult, "wings")).toBe("1 vote"); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 2 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 2 votes. Click here to see full results"); }); it("uses my local vote", async () => { @@ -180,7 +180,7 @@ describe("MPollBody", () => { expect(votesCount(renderResult, "italian")).toBe("1 vote"); expect(votesCount(renderResult, "wings")).toBe("0 votes"); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 4 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 4 votes. Click here to see full results"); }); it("overrides my other votes with my local vote", async () => { @@ -202,7 +202,7 @@ describe("MPollBody", () => { expect(votesCount(renderResult, "italian")).toBe("1 vote"); expect(votesCount(renderResult, "wings")).toBe("1 vote"); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 2 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 2 votes. Click here to see full results"); // And my vote is highlighted expect(voteButton(renderResult, "wings").className.includes(CHECKED)).toBe(true); @@ -234,7 +234,7 @@ describe("MPollBody", () => { expect(votesCount(renderResult, "italian")).toBe("0 votes"); expect(votesCount(renderResult, "wings")).toBe("1 vote"); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 1 vote"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 1 vote. Click here to see full results"); }); it("doesn't cancel my local vote if someone else votes", async () => { @@ -266,7 +266,7 @@ describe("MPollBody", () => { expect(votesCount(renderResult, "italian")).toBe("0 votes"); expect(votesCount(renderResult, "wings")).toBe("1 vote"); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 2 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 2 votes. Click here to see full results"); // And my vote is highlighted expect(voteButton(renderResult, "pizza").className.includes(CHECKED)).toBe(true); @@ -293,7 +293,7 @@ describe("MPollBody", () => { expect(votesCount(renderResult, "poutine")).toBe("0 votes"); expect(votesCount(renderResult, "italian")).toBe("0 votes"); expect(votesCount(renderResult, "wings")).toBe("1 vote"); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 2 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 2 votes. Click here to see full results"); }); it("allows un-voting by passing an empty vote", async () => { @@ -307,7 +307,7 @@ describe("MPollBody", () => { expect(votesCount(renderResult, "poutine")).toBe("0 votes"); expect(votesCount(renderResult, "italian")).toBe("1 vote"); expect(votesCount(renderResult, "wings")).toBe("0 votes"); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 1 vote"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 1 vote. Click here to see full results"); }); it("allows re-voting after un-voting", async () => { @@ -322,7 +322,7 @@ describe("MPollBody", () => { expect(votesCount(renderResult, "poutine")).toBe("0 votes"); expect(votesCount(renderResult, "italian")).toBe("2 votes"); expect(votesCount(renderResult, "wings")).toBe("0 votes"); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 2 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 2 votes. Click here to see full results"); }); it("treats any invalid answer as a spoiled ballot", async () => { @@ -340,7 +340,7 @@ describe("MPollBody", () => { expect(votesCount(renderResult, "poutine")).toBe("0 votes"); expect(votesCount(renderResult, "italian")).toBe("0 votes"); expect(votesCount(renderResult, "wings")).toBe("0 votes"); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 0 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 0 votes. Click here to see full results"); }); it("allows re-voting after a spoiled ballot", async () => { @@ -357,7 +357,7 @@ describe("MPollBody", () => { expect(votesCount(renderResult, "poutine")).toBe("1 vote"); expect(votesCount(renderResult, "italian")).toBe("0 votes"); expect(votesCount(renderResult, "wings")).toBe("0 votes"); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 1 vote"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Based on 1 vote. Click here to see full results"); }); it("renders nothing if poll has no answers", async () => { @@ -526,7 +526,7 @@ describe("MPollBody", () => { expect(endedVotesCount(renderResult, "poutine")).toBe('
1 vote'); expect(endedVotesCount(renderResult, "italian")).toBe("0 votes"); expect(endedVotesCount(renderResult, "wings")).toBe('
1 vote'); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 2 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 2 votes. Click here to see full results"); }); it("counts a single vote as normal if the poll is ended", async () => { @@ -537,7 +537,7 @@ describe("MPollBody", () => { expect(endedVotesCount(renderResult, "poutine")).toBe('
1 vote'); expect(endedVotesCount(renderResult, "italian")).toBe("0 votes"); expect(endedVotesCount(renderResult, "wings")).toBe("0 votes"); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 1 vote"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 1 vote. Click here to see full results"); }); it("shows ended vote counts of different numbers", async () => { @@ -557,7 +557,7 @@ describe("MPollBody", () => { expect(endedVotesCount(renderResult, "poutine")).toBe("0 votes"); expect(endedVotesCount(renderResult, "italian")).toBe("0 votes"); expect(endedVotesCount(renderResult, "wings")).toBe('
3 votes'); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 5 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 5 votes. Click here to see full results"); }); it("ignores votes that arrived after poll ended", async () => { @@ -577,7 +577,7 @@ describe("MPollBody", () => { expect(endedVotesCount(renderResult, "poutine")).toBe("0 votes"); expect(endedVotesCount(renderResult, "italian")).toBe("0 votes"); expect(endedVotesCount(renderResult, "wings")).toBe('
3 votes'); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 5 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 5 votes. Click here to see full results"); }); it("counts votes that arrived after an unauthorised poll end event", async () => { @@ -600,7 +600,7 @@ describe("MPollBody", () => { expect(endedVotesCount(renderResult, "poutine")).toBe("0 votes"); expect(endedVotesCount(renderResult, "italian")).toBe("0 votes"); expect(endedVotesCount(renderResult, "wings")).toBe('
3 votes'); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 5 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 5 votes. Click here to see full results"); }); it("ignores votes that arrived after the first end poll event", async () => { @@ -627,7 +627,7 @@ describe("MPollBody", () => { expect(endedVotesCount(renderResult, "poutine")).toBe("0 votes"); expect(endedVotesCount(renderResult, "italian")).toBe("0 votes"); expect(endedVotesCount(renderResult, "wings")).toBe('
3 votes'); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 5 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 5 votes. Click here to see full results"); }); it("highlights the winning vote in an ended poll", async () => { diff --git a/test/unit-tests/components/views/messages/__snapshots__/MPollBody-test.tsx.snap b/test/unit-tests/components/views/messages/__snapshots__/MPollBody-test.tsx.snap index b24f80146d6..80dc66b7c89 100644 --- a/test/unit-tests/components/views/messages/__snapshots__/MPollBody-test.tsx.snap +++ b/test/unit-tests/components/views/messages/__snapshots__/MPollBody-test.tsx.snap @@ -29,10 +29,26 @@ exports[`MPollBody renders a finished poll 1`] = ` > Pizza
-
- 0 votes +
+
+
+
+
+ + 0 votes + +
@@ -61,10 +77,26 @@ exports[`MPollBody renders a finished poll 1`] = ` > Poutine
-
- 0 votes +
+
+
+
+
+ + 0 votes + +
@@ -93,13 +125,29 @@ exports[`MPollBody renders a finished poll 1`] = ` > Italian
-
+
- 2 votes + style="display: flex;" + > +
+
+
+ +
+ 2 votes + +
@@ -128,10 +176,26 @@ exports[`MPollBody renders a finished poll 1`] = ` > Wings
-
- 1 vote +
+
+
+
+
+ + 1 vote + +
@@ -147,9 +211,12 @@ exports[`MPollBody renders a finished poll 1`] = `
- Final result based on 3 votes + + Final result based on 3 votes. Click here to see full results +
@@ -184,13 +251,29 @@ exports[`MPollBody renders a finished poll with multiple winners 1`] = ` > Pizza
-
+
- 2 votes + style="display: flex;" + > +
+
+
+ +
+ 2 votes + +
@@ -219,10 +302,26 @@ exports[`MPollBody renders a finished poll with multiple winners 1`] = ` > Poutine
-
- 0 votes +
+
+
+
+
+ + 0 votes + +
@@ -251,10 +350,26 @@ exports[`MPollBody renders a finished poll with multiple winners 1`] = ` > Italian
-
- 0 votes +
+
+
+
+
+ + 0 votes + +
@@ -283,13 +398,29 @@ exports[`MPollBody renders a finished poll with multiple winners 1`] = ` > Wings
-
+
- 2 votes + style="display: flex;" + > +
+
+
+ +
+ 2 votes + +
@@ -305,9 +436,12 @@ exports[`MPollBody renders a finished poll with multiple winners 1`] = `
- Final result based on 4 votes + + Final result based on 4 votes. Click here to see full results +
@@ -342,10 +476,26 @@ exports[`MPollBody renders a finished poll with no votes 1`] = ` > Pizza
-
- 0 votes +
+
+
+
+
+ + 0 votes + +
@@ -374,10 +524,26 @@ exports[`MPollBody renders a finished poll with no votes 1`] = ` > Poutine -
- 0 votes +
+
+
+
+
+ + 0 votes + +
@@ -406,10 +572,26 @@ exports[`MPollBody renders a finished poll with no votes 1`] = ` > Italian -
- 0 votes +
+
+
+
+
+ + 0 votes + +
@@ -438,10 +620,26 @@ exports[`MPollBody renders a finished poll with no votes 1`] = ` > Wings -
- 0 votes +
+
+
+
+
+ + 0 votes + +
@@ -457,9 +655,12 @@ exports[`MPollBody renders a finished poll with no votes 1`] = `
- Final result based on 0 votes + + Final result based on 0 votes. Click here to see full results +
@@ -504,9 +705,15 @@ exports[`MPollBody renders a poll that I have not voted in 1`] = ` > Pizza -
+
+
+ +
+
Poutine
-
+
+
+ +
+
Italian
-
+
+
+ +
+
@@ -636,9 +855,15 @@ exports[`MPollBody renders a poll that I have not voted in 1`] = ` > Wings -
+
+
+ +
+
- 3 votes cast. Vote to see the results + + 3 votes cast. Vote to see the results +
@@ -704,10 +932,26 @@ exports[`MPollBody renders a poll with local, non-local and invalid votes 1`] = > Pizza -
- 1 vote +
+
+
+
+
+ + 1 vote + +
@@ -750,10 +994,26 @@ exports[`MPollBody renders a poll with local, non-local and invalid votes 1`] = > Poutine -
- 0 votes +
+
+
+
+
+ + 0 votes + +
@@ -796,10 +1056,26 @@ exports[`MPollBody renders a poll with local, non-local and invalid votes 1`] = > Italian -
- 3 votes +
+
+
+
+
+ + 3 votes + +
@@ -842,10 +1118,26 @@ exports[`MPollBody renders a poll with local, non-local and invalid votes 1`] = > Wings -
- 1 vote +
+
+
+
+
+ + 1 vote + +
@@ -865,9 +1157,12 @@ exports[`MPollBody renders a poll with local, non-local and invalid votes 1`] =
- Based on 5 votes + + Based on 5 votes. Click here to see full results +
@@ -912,9 +1207,15 @@ exports[`MPollBody renders a poll with no votes 1`] = ` > Pizza -
+
+
+ +
+
Poutine
-
+
+
+ +
+
Italian
-
+
+
+ +
+
Wings
-
+
+
+ +
+
- No votes cast + + No votes cast +
@@ -1112,10 +1434,26 @@ exports[`MPollBody renders a poll with only non-local votes 1`] = ` > Pizza -
- 0 votes +
+
+
+
+
+ + 0 votes + +
@@ -1158,10 +1496,26 @@ exports[`MPollBody renders a poll with only non-local votes 1`] = ` > Poutine -
- 0 votes +
+
+
+
+
+ + 0 votes + +
@@ -1204,10 +1558,26 @@ exports[`MPollBody renders a poll with only non-local votes 1`] = ` > Italian -
- 2 votes +
+
+
+
+
+ + 2 votes + +
@@ -1250,10 +1620,26 @@ exports[`MPollBody renders a poll with only non-local votes 1`] = ` > Wings -
- 1 vote +
+
+
+
+
+ + 1 vote + +
@@ -1273,9 +1659,12 @@ exports[`MPollBody renders a poll with only non-local votes 1`] = `
- Based on 3 votes + + Based on 3 votes. Click here to see full results +
@@ -1310,13 +1699,29 @@ exports[`MPollBody renders an undisclosed, finished poll 1`] = ` > Pizza -
+
- 2 votes + style="display: flex;" + > +
+
+
+ +
+ 2 votes + +
@@ -1345,10 +1750,26 @@ exports[`MPollBody renders an undisclosed, finished poll 1`] = ` > Poutine
-
- 0 votes +
+
+
+
+
+ + 0 votes + +
@@ -1377,10 +1798,26 @@ exports[`MPollBody renders an undisclosed, finished poll 1`] = ` > Italian -
- 0 votes +
+
+
+
+
+ + 0 votes + +
@@ -1409,13 +1846,29 @@ exports[`MPollBody renders an undisclosed, finished poll 1`] = ` > Wings -
+
- 2 votes + style="display: flex;" + > +
+
+
+ +
+ 2 votes + +
@@ -1431,9 +1884,12 @@ exports[`MPollBody renders an undisclosed, finished poll 1`] = `
- Final result based on 4 votes + + Final result based on 4 votes. Click here to see full results +
@@ -1478,9 +1934,15 @@ exports[`MPollBody renders an undisclosed, unfinished poll 1`] = ` > Pizza -
+
+
+ +
+
@@ -1522,9 +1984,15 @@ exports[`MPollBody renders an undisclosed, unfinished poll 1`] = ` > Poutine -
+
+
+ +
+
Italian
-
+
+
+ +
+
Wings
-
+
+
+ +
+
- Results will be visible when the poll is ended + + Results will be visible when the poll is ended +
From 22729f4513af464dab5963d3570adac63c1729e8 Mon Sep 17 00:00:00 2001 From: Tim Vahlbrock Date: Fri, 1 Nov 2024 12:37:37 +0100 Subject: [PATCH 10/14] Fix remaining existing tests --- src/components/views/messages/MPollBody.tsx | 2 +- test/unit-tests/components/views/messages/MPollBody-test.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/messages/MPollBody.tsx b/src/components/views/messages/MPollBody.tsx index 67c94fd988e..38ef6ccdbf4 100644 --- a/src/components/views/messages/MPollBody.tsx +++ b/src/components/views/messages/MPollBody.tsx @@ -244,7 +244,7 @@ export default class MPollBody extends React.Component { if (!this.state.voteRelations || !this.context) { return new Map(); } - return collectUserVotes(allVotes(this.state.voteRelations), null, this.state.selected); + return collectUserVotes(allVotes(this.state.voteRelations), this.context.getUserId(), this.state.selected); } /** diff --git a/test/unit-tests/components/views/messages/MPollBody-test.tsx b/test/unit-tests/components/views/messages/MPollBody-test.tsx index eb94b2906ee..b59450324d9 100644 --- a/test/unit-tests/components/views/messages/MPollBody-test.tsx +++ b/test/unit-tests/components/views/messages/MPollBody-test.tsx @@ -425,7 +425,7 @@ describe("MPollBody", () => { expect(endedVotesCount(renderResult, "poutine")).toBe("1 vote"); expect(endedVotesCount(renderResult, "italian")).toBe("0 votes"); expect(endedVotesCount(renderResult, "wings")).toBe("1 vote"); - expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 5 votes"); + expect(renderResult.getByTestId("totalVotes").innerHTML).toBe("Final result based on 5 votes. Click here to see full results"); }); it("sends a vote event when I choose an option", async () => { From 3730cb4653c8535aee9d07502b4e4f36570d4726 Mon Sep 17 00:00:00 2001 From: Tim Vahlbrock Date: Fri, 1 Nov 2024 12:47:14 +0100 Subject: [PATCH 11/14] Move poll option styling to css file --- .../components/views/polls/_PollOption.pcss | 8 +++++++ src/components/views/polls/PollOption.tsx | 23 +++++++++---------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/res/css/components/views/polls/_PollOption.pcss b/res/css/components/views/polls/_PollOption.pcss index 4ef6c225224..0060906e300 100644 --- a/res/css/components/views/polls/_PollOption.pcss +++ b/res/css/components/views/polls/_PollOption.pcss @@ -35,6 +35,14 @@ Please see LICENSE files in the repository root for full details. justify-content: space-between; } +.mx_PollOption_votesWrapper { + display: flex; +} + +.mx_PollOption_facePile { + margin-right: $spacing-8 +} + .mx_PollOption_optionVoteCount { color: $secondary-content; font-size: $font-12px; diff --git a/src/components/views/polls/PollOption.tsx b/src/components/views/polls/PollOption.tsx index 47c805e7186..a2030a78acf 100644 --- a/src/components/views/polls/PollOption.tsx +++ b/src/components/views/polls/PollOption.tsx @@ -33,22 +33,21 @@ const PollOptionContent: React.FC = ({ isWinner, answer, return (
{answer.text}
-
-
- {displayVoteCount - && members.length <= MAXIMUM_MEMBERS_FOR_FACE_PILE - && + {displayVoteCount + && members.length <= MAXIMUM_MEMBERS_FOR_FACE_PILE + &&
+ votes.some((v) => v.sender === m.userId))} size="24px" overflow={false} - style={{ marginRight: "10px" }} /> - } - - {isWinner && } - {votesText} - -
+
+ } + + {isWinner && } + {votesText} +
); From c53fea0b26406f5b383ae2c49fc518ccd161d1a8 Mon Sep 17 00:00:00 2001 From: Tim Vahlbrock Date: Fri, 1 Nov 2024 13:49:29 +0100 Subject: [PATCH 12/14] Add test that dialog is opened when totalVotes are clicked --- .../views/messages/MPollBody-test.tsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/unit-tests/components/views/messages/MPollBody-test.tsx b/test/unit-tests/components/views/messages/MPollBody-test.tsx index b59450324d9..e047a2896f4 100644 --- a/test/unit-tests/components/views/messages/MPollBody-test.tsx +++ b/test/unit-tests/components/views/messages/MPollBody-test.tsx @@ -20,6 +20,8 @@ import { } from "matrix-js-sdk/src/matrix"; import React from "react"; +import Modal from "../../../../../src/Modal"; +import PollResultsDialog from "../../../../../src/components/views/dialogs/PollResultsDialog"; import { IBodyProps } from "../../../../../src/components/views/messages/IBodyProps"; import MPollBody, { allVotes, @@ -863,6 +865,24 @@ describe("MPollBody", () => { const { container } = await newMPollBody(votes, ends, undefined, false); expect(container).toMatchSnapshot(); }); + + it("opens the full results dialog when the total votes link is clicked", async () => { + const votes = [ + responseEvent("@ed:example.com", "pizza", 12), + responseEvent("@rf:example.com", "pizza", 12), + responseEvent("@th:example.com", "wings", 13), + ]; + const ends = [newPollEndEvent("@me:example.com", 25)]; + const renderResult = await newMPollBody(votes, ends); + const createDialogSpy = jest.spyOn(Modal, "createDialog"); + + fireEvent.click(renderResult.getByTestId("totalVotes")); + + expect(createDialogSpy).toHaveBeenCalledWith( + PollResultsDialog, + expect.anything() + ); + }); }); function newVoteRelations(relationEvents: Array): Relations { From dc038515e4deb483cc14009b24d4ec93657a80de Mon Sep 17 00:00:00 2001 From: Tim Vahlbrock Date: Fri, 1 Nov 2024 13:52:20 +0100 Subject: [PATCH 13/14] Reapply "Enable React StrictMode (#28258)" This reverts commit fa76ae64f867efe8e6df1a1b5de9ebce5011d698. --- src/Modal.tsx | 54 ++++++++++--------- .../views/elements/PersistedElement.tsx | 18 ++++--- src/components/views/messages/TextualBody.tsx | 17 ++++-- src/utils/pillify.tsx | 28 +++++----- src/utils/tooltipify.tsx | 14 ++--- src/vector/app.tsx | 26 ++++----- src/vector/init.tsx | 10 ++-- 7 files changed, 96 insertions(+), 71 deletions(-) diff --git a/src/Modal.tsx b/src/Modal.tsx index 53a1935294f..9e6a7672bea 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React from "react"; +import React, { StrictMode } from "react"; import ReactDOM from "react-dom"; import classNames from "classnames"; import { IDeferred, defer, sleep } from "matrix-js-sdk/src/utils"; @@ -416,18 +416,20 @@ export class ModalManager extends TypedEventEmitter -
- -
{this.staticModal.elem}
-
-
-
- + + +
+ +
{this.staticModal.elem}
+
+
+
+ + ); ReactDOM.render(staticDialog, ModalManager.getOrCreateStaticContainer()); @@ -443,18 +445,20 @@ export class ModalManager extends TypedEventEmitter -
- -
{modal.elem}
-
-
-
- + + +
+ +
{modal.elem}
+
+
+
+ + ); setTimeout(() => ReactDOM.render(dialog, ModalManager.getOrCreateContainer()), 0); diff --git a/src/components/views/elements/PersistedElement.tsx b/src/components/views/elements/PersistedElement.tsx index 1b7b6543e95..20584f3794c 100644 --- a/src/components/views/elements/PersistedElement.tsx +++ b/src/components/views/elements/PersistedElement.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { MutableRefObject, ReactNode } from "react"; +import React, { MutableRefObject, ReactNode, StrictMode } from "react"; import ReactDOM from "react-dom"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { TooltipProvider } from "@vector-im/compound-web"; @@ -167,13 +167,15 @@ export default class PersistedElement extends React.Component { private renderApp(): void { const content = ( - - -
- {this.props.children} -
-
-
+ + + +
+ {this.props.children} +
+
+
+
); ReactDOM.render(content, getOrCreateContainer("mx_persistedElement_" + this.props.persistKey)); diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 0e0c29747f9..7955d964a32 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ -import React, { createRef, SyntheticEvent, MouseEvent } from "react"; +import React, { createRef, SyntheticEvent, MouseEvent, StrictMode } from "react"; import ReactDOM from "react-dom"; import { MsgType } from "matrix-js-sdk/src/matrix"; import { TooltipProvider } from "@vector-im/compound-web"; @@ -118,7 +118,12 @@ export default class TextualBody extends React.Component { // Insert containing div in place of
 block
         pre.parentNode?.replaceChild(root, pre);
 
-        ReactDOM.render({pre}, root);
+        ReactDOM.render(
+            
+                {pre}
+            ,
+            root,
+        );
     }
 
     public componentDidUpdate(prevProps: Readonly): void {
@@ -192,9 +197,11 @@ export default class TextualBody extends React.Component {
                 const reason = node.getAttribute("data-mx-spoiler") ?? undefined;
                 node.removeAttribute("data-mx-spoiler"); // we don't want to recurse
                 const spoiler = (
-                    
-                        
-                    
+                    
+                        
+                            
+                        
+                    
                 );
 
                 ReactDOM.render(spoiler, spoilerContainer);
diff --git a/src/utils/pillify.tsx b/src/utils/pillify.tsx
index 2c19f114917..063012d16f3 100644
--- a/src/utils/pillify.tsx
+++ b/src/utils/pillify.tsx
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
 Please see LICENSE files in the repository root for full details.
 */
 
-import React from "react";
+import React, { StrictMode } from "react";
 import ReactDOM from "react-dom";
 import { PushProcessor } from "matrix-js-sdk/src/pushprocessor";
 import { MatrixClient, MatrixEvent, RuleId } from "matrix-js-sdk/src/matrix";
@@ -76,9 +76,11 @@ export function pillifyLinks(
                 const pillContainer = document.createElement("span");
 
                 const pill = (
-                    
-                        
-                    
+                    
+                        
+                            
+                        
+                    
                 );
 
                 ReactDOM.render(pill, pillContainer);
@@ -133,14 +135,16 @@ export function pillifyLinks(
 
                         const pillContainer = document.createElement("span");
                         const pill = (
-                            
-                                
-                            
+                            
+                                
+                                    
+                                
+                            
                         );
 
                         ReactDOM.render(pill, pillContainer);
diff --git a/src/utils/tooltipify.tsx b/src/utils/tooltipify.tsx
index 65ce431a976..bcda256a9c8 100644
--- a/src/utils/tooltipify.tsx
+++ b/src/utils/tooltipify.tsx
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
 Please see LICENSE files in the repository root for full details.
 */
 
-import React from "react";
+import React, { StrictMode } from "react";
 import ReactDOM from "react-dom";
 import { TooltipProvider } from "@vector-im/compound-web";
 
@@ -53,11 +53,13 @@ export function tooltipifyLinks(rootNodes: ArrayLike, ignoredNodes: Ele
             // wrapping the link with the LinkWithTooltip component, keeping the same children. Ideally we'd do this
             // without the superfluous span but this is not something React trivially supports at this time.
             const tooltip = (
-                
-                    
-                        
-                    
-                
+                
+                    
+                        
+                            
+                        
+                    
+                
             );
 
             ReactDOM.render(tooltip, node);
diff --git a/src/vector/app.tsx b/src/vector/app.tsx
index 0c2230bbb8a..da0f3f3941f 100644
--- a/src/vector/app.tsx
+++ b/src/vector/app.tsx
@@ -12,7 +12,7 @@ Please see LICENSE files in the repository root for full details.
 
 // To ensure we load the browser-matrix version first
 import "matrix-js-sdk/src/browser-index";
-import React, { ReactElement } from "react";
+import React, { ReactElement, StrictMode } from "react";
 import { logger } from "matrix-js-sdk/src/logger";
 import { createClient, AutoDiscovery, ClientConfig } from "matrix-js-sdk/src/matrix";
 import { WrapperLifecycle, WrapperOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WrapperLifecycle";
@@ -111,17 +111,19 @@ export async function loadApp(fragParams: {}, matrixChatRef: React.Ref
-            
+            
+                
+            
         
     );
 }
diff --git a/src/vector/init.tsx b/src/vector/init.tsx
index da9827cb55b..2028f9af365 100644
--- a/src/vector/init.tsx
+++ b/src/vector/init.tsx
@@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
 */
 
 import * as ReactDOM from "react-dom";
-import * as React from "react";
+import React, { StrictMode } from "react";
 import { logger } from "matrix-js-sdk/src/logger";
 
 import * as languageHandler from "../languageHandler";
@@ -105,7 +105,9 @@ export async function showError(title: string, messages?: string[]): Promise,
+        
+            
+        ,
         document.getElementById("matrixchat"),
     );
 }
@@ -116,7 +118,9 @@ export async function showIncompatibleBrowser(onAccept: () => void): Promise,
+        
+            
+        ,
         document.getElementById("matrixchat"),
     );
 }

From ebbdf80cd00ed9990c701c999844244b3eaff236 Mon Sep 17 00:00:00 2001
From: Tim Vahlbrock 
Date: Sat, 16 Nov 2024 12:08:48 +0100
Subject: [PATCH 14/14] Add e2e test for detailed poll results

---
 playwright/e2e/polls/polls.spec.ts            | 96 ++++++++++++++++++-
 .../views/dialogs/PollResultsDialog.tsx       |  3 +-
 2 files changed, 95 insertions(+), 4 deletions(-)

diff --git a/playwright/e2e/polls/polls.spec.ts b/playwright/e2e/polls/polls.spec.ts
index 4fd81955810..858ee38eb13 100644
--- a/playwright/e2e/polls/polls.spec.ts
+++ b/playwright/e2e/polls/polls.spec.ts
@@ -6,11 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
 Please see LICENSE files in the repository root for full details.
 */
 
-import { test, expect } from "../../element-web-test";
-import { Bot } from "../../pages/bot";
+import type { Locator, Page } from "@playwright/test";
 import { SettingLevel } from "../../../src/settings/SettingLevel";
 import { Layout } from "../../../src/settings/enums/Layout";
-import type { Locator, Page } from "@playwright/test";
+import { expect, test } from "../../element-web-test";
+import { Bot } from "../../pages/bot";
 
 test.describe("Polls", () => {
     type CreatePollOptions = {
@@ -59,6 +59,35 @@ test.describe("Polls", () => {
         ).toContainText(`${votes} vote`);
     };
 
+    const getPollResultsDialog = (page: Page): Locator => {
+        return page.locator(".mx_PollResultsDialog");
+    };
+
+    const getPollResultsDialogOption = (page: Page, optionText: string): Locator => {
+        return getPollResultsDialog(page)
+            .locator(".mx_AnswerEntry")
+            .filter({ hasText: optionText });
+    };
+
+    const expectDetailedPollOptionVoteCount = async (
+        page: Page,
+        pollId: string,
+        optionText: string,
+        votes: number,
+        optLocator?: Locator,
+    ): Promise => {
+        await expect(
+            getPollResultsDialogOption(page, optionText)
+                .locator(".mx_AnswerEntry_Header")
+                .locator(".mx_AnswerEntry_Header_answerName"),
+        ).toContainText(optionText);
+        await expect(
+            getPollResultsDialogOption(page, optionText)
+                .locator(".mx_AnswerEntry_Header")
+                .locator(".mx_AnswerEntry_Header_voteCount"),
+        ).toContainText(`${votes} vote`);
+    };
+
     const botVoteForOption = async (
         page: Page,
         bot: Bot,
@@ -219,6 +248,67 @@ test.describe("Polls", () => {
         await expect(page.locator(".mx_ErrorDialog")).toBeAttached();
     });
 
+    test("should allow to view detailed results after voting", async ({ page, app, bot, user }) => {
+        const roomId: string = await app.client.createRoom({});
+        await app.client.inviteUser(roomId, bot.credentials.userId);
+        await page.goto("/#/room/" + roomId);
+        // wait until Bob joined
+        await expect(page.getByText("BotBob joined the room")).toBeAttached();
+
+        const locator = await app.openMessageComposerOptions();
+        await locator.getByRole("menuitem", { name: "Poll" }).click();
+
+        // Disabled because flaky - see https://github.com/vector-im/element-web/issues/24688
+        //cy.get(".mx_CompoundDialog").percySnapshotElement("Polls Composer");
+
+        const pollParams = {
+            title: "Does the polls feature work?",
+            options: ["Yes", "No", "Maybe?"],
+        };
+        await createPoll(page, pollParams);
+
+        // Wait for message to send, get its ID and save as @pollId
+        const pollId = await page
+            .locator(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]")
+            .filter({ hasText: pollParams.title })
+            .getAttribute("data-scroll-tokens");
+        await expect(getPollTile(page, pollId)).toMatchScreenshot("Polls_Timeline_tile_no_votes.png", {
+            mask: [page.locator(".mx_MessageTimestamp")],
+        });
+
+        // Bot votes 'Maybe' in the poll
+        await botVoteForOption(page, bot, roomId, pollId, pollParams.options[2]);
+
+        // no votes shown until I vote, check bots vote has arrived
+        await expect(
+            page.locator(".mx_MPollBody_totalVotes").getByText("1 vote cast. Vote to see the results"),
+        ).toBeAttached();
+
+        // vote 'Maybe'
+        await getPollOption(page, pollId, pollParams.options[2]).click();
+        // both me and bot have voted Maybe
+        await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 2);
+
+        // click the 'vote to see results' message
+        await page.locator(".mx_MPollBody_totalVotes").getByText("Based on 2 votes. Click here to see full results").click();
+
+        // expect the detailed results to be shown
+        await expect(getPollResultsDialog(page)).toBeAttached();
+
+        // expect results to be correctly shown
+        await expectDetailedPollOptionVoteCount(page, pollId, pollParams.options[2], 2);
+        const voterEntries = getPollResultsDialogOption(page, pollParams.options[2]).locator(".mx_VoterEntry");
+        expect((await voterEntries.all()).length).toBe(2);
+        expect(voterEntries.filter({ hasText: bot.credentials.displayName })).not.toBeNull();
+        expect(voterEntries.filter({hasText: user.displayName})).not.toBeNull();
+
+        // close the dialog
+        await page.locator(".mx_Dialog").getByRole("button", { name: "Close" }).click();
+
+        // expect the dialog to be closed
+        await expect(getPollResultsDialog(page)).not.toBeAttached();
+    });
+
     test("should be displayed correctly in thread panel", async ({ page, app, user, bot, homeserver }) => {
         const botCharlie = new Bot(page, homeserver, { displayName: "BotCharlie" });
         await botCharlie.prepareClient();
diff --git a/src/components/views/dialogs/PollResultsDialog.tsx b/src/components/views/dialogs/PollResultsDialog.tsx
index 7e8e9929d40..713f66d98f0 100644
--- a/src/components/views/dialogs/PollResultsDialog.tsx
+++ b/src/components/views/dialogs/PollResultsDialog.tsx
@@ -27,6 +27,7 @@ export default function PollResultsDialog(props: IProps): JSX.Element {
          Modal.closeCurrentModal()}
+            className="mx_PollResultsDialog"
         >
             {
                 props.pollEvent.answers.map((answer) => {
@@ -55,7 +56,7 @@ function AnswerEntry(props: {
         
{answer.text} - {_t("poll|result_dialog|count_of_votes", { count: votes.length })} + {_t("poll|result_dialog|count_of_votes", { count: votes.length })}
{votes.length === 0 &&
No one voted for this.
} {votes.map((vote) => {