From 4c90e4775d455ac033c66b51e2b908889ec8bbee Mon Sep 17 00:00:00 2001 From: Tim Vahlbrock Date: Fri, 25 Oct 2024 15:33:51 +0200 Subject: [PATCH] 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 }) => (