Skip to content

Commit

Permalink
Show Profile Pictures according to Votes on Poll Options
Browse files Browse the repository at this point in the history
  • Loading branch information
timvahlbrock committed Oct 25, 2024
1 parent 7de5c84 commit 4c90e47
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 52 deletions.
58 changes: 28 additions & 30 deletions src/components/views/messages/MPollBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -81,12 +81,12 @@ export function findTopAnswer(pollEvent: MatrixEvent, voteRelations: Relations):

const userVotes: Map<string, UserVote> = collectUserVotes(allVotes(voteRelations));

const votes: Map<string, number> = countVotes(userVotes, poll);
const highestScore: number = Math.max(...votes.values());
const votes: Map<string, UserVote[]> = 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);
}
}
Expand Down Expand Up @@ -243,7 +243,7 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
if (!this.state.voteRelations || !this.context) {
return new Map<string, UserVote>();
}
return collectUserVotes(allVotes(this.state.voteRelations), this.context.getUserId(), this.state.selected);
return collectUserVotes(allVotes(this.state.voteRelations), null, this.state.selected);
}

/**
Expand Down Expand Up @@ -273,10 +273,10 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
this.setState({ selected: newSelected });
}

private totalVotes(collectedVotes: Map<string, number>): number {
private totalVotes(collectedVotes: Map<string, UserVote[]>): number {
let sum = 0;
for (const v of collectedVotes.values()) {
sum += v;
sum += v.length;
}
return sum;
}
Expand All @@ -294,7 +294,7 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
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);
Expand Down Expand Up @@ -335,7 +335,7 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
let answerVotes = 0;

if (showResults) {
answerVotes = votes.get(answer.id) ?? 0;
answerVotes = votes.get(answer.id)?.length ?? 0;
}

const checked =
Expand All @@ -348,7 +348,7 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
answer={answer}
isChecked={checked}
isEnded={poll.isEnded}
voteCount={answerVotes}
votes={votes.get(answer.id) ?? []}
totalVoteCount={totalVotes}
displayVoteCount={showResults}
onOptionSelected={this.selectOption.bind(this)}
Expand Down Expand Up @@ -392,7 +392,7 @@ export function allVotes(voteRelations: Relations): Array<UserVote> {
/**
* 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
Expand All @@ -418,19 +418,17 @@ export function collectUserVotes(
return userVotes;
}

export function countVotes(userVotes: Map<string, UserVote>, pollStart: PollStartEvent): Map<string, number> {
const collected = new Map<string, number>();
export function countVotes(userVotes: Map<string, UserVote>, pollStart: PollStartEvent): Map<string, UserVote[]> {
const collected = new Map<string, UserVote[]>();

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);
}
}
}
Expand Down
37 changes: 26 additions & 11 deletions src/components/views/polls/PollOption.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<PollOptionContentProps> = ({ isWinner, answer, voteCount, displayVoteCount }) => {
const votesText = displayVoteCount ? _t("timeline|m.poll|count_of_votes", { count: voteCount }) : "";
const PollOptionContent: React.FC<PollOptionContentProps> = ({ 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 (
<div className="mx_PollOption_content">
<div className="mx_PollOption_optionText">{answer.text}</div>
<div className="mx_PollOption_optionVoteCount">
{isWinner && <TrophyIcon className="mx_PollOption_winnerIcon" />}
{votesText}
<div style={{ display: "flex" }}>
<FacePile
members={members.filter((m) => votes.some((v) => v.sender === m.userId))}
size="24px"
overflow={false}
style={{ marginRight: "10px" }}
/>
{votesText}
</div>
</div>
</div>
);
Expand All @@ -42,7 +57,7 @@ interface PollOptionProps extends PollOptionContentProps {
children?: ReactNode;
}

const EndedPollOption: React.FC<Omit<PollOptionProps, "voteCount" | "totalVoteCount">> = ({
const EndedPollOption: React.FC<Omit<PollOptionProps, "votes" | "totalVoteCount">> = ({
isChecked,
children,
answer,
Expand All @@ -57,7 +72,7 @@ const EndedPollOption: React.FC<Omit<PollOptionProps, "voteCount" | "totalVoteCo
</div>
);

const ActivePollOption: React.FC<Omit<PollOptionProps, "voteCount" | "totalVoteCount">> = ({
const ActivePollOption: React.FC<Omit<PollOptionProps, "votes" | "totalVoteCount">> = ({
pollId,
isChecked,
children,
Expand All @@ -78,7 +93,7 @@ const ActivePollOption: React.FC<Omit<PollOptionProps, "voteCount" | "totalVoteC
export const PollOption: React.FC<PollOptionProps> = ({
pollId,
answer,
voteCount,
votes: voteCount,
totalVoteCount,
displayVoteCount,
isEnded,
Expand All @@ -91,7 +106,7 @@ export const PollOption: React.FC<PollOptionProps> = ({
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 (
<div data-testid={`pollOption-${answer.id}`} className={cls} onClick={() => onOptionSelected?.(answer.id)}>
Expand All @@ -104,7 +119,7 @@ export const PollOption: React.FC<PollOptionProps> = ({
<PollOptionContent
isWinner={isWinner}
answer={answer}
voteCount={voteCount}
votes={voteCount}
displayVoteCount={displayVoteCount}
/>
</PollOptionWrapper>
Expand Down
22 changes: 11 additions & 11 deletions src/components/views/polls/pollHistory/PollListItemEnded.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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) || [],
})),
};
};
Expand Down Expand Up @@ -100,11 +100,11 @@ export const PollListItemEnded: React.FC<Props> = ({ event, poll, onClick }) =>
</div>
{!!winningAnswers?.length && (
<div className="mx_PollListItemEnded_answers">
{winningAnswers?.map(({ answer, voteCount }) => (
{winningAnswers?.map(({ answer, votes }) => (
<PollOption
key={answer.id}
answer={answer}
voteCount={voteCount}
votes={votes}
totalVoteCount={totalVoteCount!}
pollId={poll.pollId}
displayVoteCount
Expand Down

0 comments on commit 4c90e47

Please sign in to comment.