Show Profile Pictures according to Votes on Poll Options

This commit is contained in:
Tim Vahlbrock 2024-10-25 15:33:51 +02:00
parent 7de5c84b3d
commit 4c90e4775d
No known key found for this signature in database
3 changed files with 68 additions and 55 deletions

View file

@ -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 { _t } from "../../../languageHandler";
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 { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import Modal from "../../../Modal";
import { formatList } from "../../../utils/FormattingUtils";
import ErrorDialog from "../dialogs/ErrorDialog";
import PollCreateDialog from "../elements/PollCreateDialog";
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<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);
}
}
@ -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);
}
/**
@ -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;
}
@ -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);
@ -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 =
@ -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)}
@ -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
@ -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);
}
}
}

View file

@ -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>
);
@ -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,
@ -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,
@ -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,
@ -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)}>
@ -104,7 +119,7 @@ export const PollOption: React.FC<PollOptionProps> = ({
<PollOptionContent
isWinner={isWinner}
answer={answer}
voteCount={voteCount}
votes={voteCount}
displayVoteCount={displayVoteCount}
/>
</PollOptionWrapper>

View file

@ -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<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