Show Profile Pictures according to Votes on Poll Options
This commit is contained in:
parent
7de5c84b3d
commit
4c90e4775d
3 changed files with 68 additions and 55 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue