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.
|
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 { logger } from "matrix-js-sdk/src/logger";
|
||||||
import {
|
import {
|
||||||
MatrixEvent,
|
|
||||||
MatrixClient,
|
|
||||||
Relations,
|
|
||||||
Poll,
|
|
||||||
PollEvent,
|
|
||||||
M_POLL_KIND_DISCLOSED,
|
M_POLL_KIND_DISCLOSED,
|
||||||
M_POLL_RESPONSE,
|
M_POLL_RESPONSE,
|
||||||
M_POLL_START,
|
M_POLL_START,
|
||||||
|
MatrixClient,
|
||||||
|
MatrixEvent,
|
||||||
|
Poll,
|
||||||
|
PollEvent,
|
||||||
|
Relations,
|
||||||
TimelineEvents,
|
TimelineEvents,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import { RelatedRelations } from "matrix-js-sdk/src/models/related-relations";
|
import { RelatedRelations } from "matrix-js-sdk/src/models/related-relations";
|
||||||
import { PollStartEvent, PollAnswerSubevent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent";
|
import React, { ReactNode } from "react";
|
||||||
import { PollResponseEvent } from "matrix-js-sdk/src/extensible_events_v1/PollResponseEvent";
|
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
|
||||||
import Modal from "../../../Modal";
|
|
||||||
import { IBodyProps } from "./IBodyProps";
|
|
||||||
import { formatList } from "../../../utils/FormattingUtils";
|
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
import { _t } from "../../../languageHandler";
|
||||||
import { GetRelationsForEvent } from "../rooms/EventTile";
|
|
||||||
import PollCreateDialog from "../elements/PollCreateDialog";
|
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
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 Spinner from "../elements/Spinner";
|
||||||
import { PollOption } from "../polls/PollOption";
|
import { PollOption } from "../polls/PollOption";
|
||||||
|
import { GetRelationsForEvent } from "../rooms/EventTile";
|
||||||
|
import { IBodyProps } from "./IBodyProps";
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
poll?: Poll;
|
poll?: Poll;
|
||||||
|
@ -81,12 +81,12 @@ export function findTopAnswer(pollEvent: MatrixEvent, voteRelations: Relations):
|
||||||
|
|
||||||
const userVotes: Map<string, UserVote> = collectUserVotes(allVotes(voteRelations));
|
const userVotes: Map<string, UserVote> = collectUserVotes(allVotes(voteRelations));
|
||||||
|
|
||||||
const votes: Map<string, number> = countVotes(userVotes, poll);
|
const votes: Map<string, UserVote[]> = countVotes(userVotes, poll);
|
||||||
const highestScore: number = Math.max(...votes.values());
|
const highestScore: number = Math.max(...Array.from(votes.values()).map((votes) => votes.length));
|
||||||
|
|
||||||
const bestAnswerIds: string[] = [];
|
const bestAnswerIds: string[] = [];
|
||||||
for (const [answerId, score] of votes) {
|
for (const [answerId, answerVotes] of votes) {
|
||||||
if (score == highestScore) {
|
if (answerVotes.length == highestScore) {
|
||||||
bestAnswerIds.push(answerId);
|
bestAnswerIds.push(answerId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -243,7 +243,7 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
|
||||||
if (!this.state.voteRelations || !this.context) {
|
if (!this.state.voteRelations || !this.context) {
|
||||||
return new Map<string, UserVote>();
|
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 });
|
this.setState({ selected: newSelected });
|
||||||
}
|
}
|
||||||
|
|
||||||
private totalVotes(collectedVotes: Map<string, number>): number {
|
private totalVotes(collectedVotes: Map<string, UserVote[]>): number {
|
||||||
let sum = 0;
|
let sum = 0;
|
||||||
for (const v of collectedVotes.values()) {
|
for (const v of collectedVotes.values()) {
|
||||||
sum += v;
|
sum += v.length;
|
||||||
}
|
}
|
||||||
return sum;
|
return sum;
|
||||||
}
|
}
|
||||||
|
@ -294,7 +294,7 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
|
||||||
const userVotes = this.collectUserVotes();
|
const userVotes = this.collectUserVotes();
|
||||||
const votes = countVotes(userVotes, pollEvent);
|
const votes = countVotes(userVotes, pollEvent);
|
||||||
const totalVotes = this.totalVotes(votes);
|
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 userId = this.context.getSafeUserId();
|
||||||
const myVote = userVotes?.get(userId)?.answers[0];
|
const myVote = userVotes?.get(userId)?.answers[0];
|
||||||
const disclosed = M_POLL_KIND_DISCLOSED.matches(pollEvent.kind.name);
|
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;
|
let answerVotes = 0;
|
||||||
|
|
||||||
if (showResults) {
|
if (showResults) {
|
||||||
answerVotes = votes.get(answer.id) ?? 0;
|
answerVotes = votes.get(answer.id)?.length ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const checked =
|
const checked =
|
||||||
|
@ -348,7 +348,7 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
|
||||||
answer={answer}
|
answer={answer}
|
||||||
isChecked={checked}
|
isChecked={checked}
|
||||||
isEnded={poll.isEnded}
|
isEnded={poll.isEnded}
|
||||||
voteCount={answerVotes}
|
votes={votes.get(answer.id) ?? []}
|
||||||
totalVoteCount={totalVotes}
|
totalVoteCount={totalVotes}
|
||||||
displayVoteCount={showResults}
|
displayVoteCount={showResults}
|
||||||
onOptionSelected={this.selectOption.bind(this)}
|
onOptionSelected={this.selectOption.bind(this)}
|
||||||
|
@ -392,7 +392,7 @@ export function allVotes(voteRelations: Relations): Array<UserVote> {
|
||||||
/**
|
/**
|
||||||
* Figure out the correct vote for each user.
|
* Figure out the correct vote for each user.
|
||||||
* @param userResponses current vote responses in the poll
|
* @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.
|
* Should be set to the current user ID.
|
||||||
* @param {string?} selected Local echo selected option for the userId
|
* @param {string?} selected Local echo selected option for the userId
|
||||||
* @returns a Map of user ID to their vote info
|
* @returns a Map of user ID to their vote info
|
||||||
|
@ -418,19 +418,17 @@ export function collectUserVotes(
|
||||||
return userVotes;
|
return userVotes;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function countVotes(userVotes: Map<string, UserVote>, pollStart: PollStartEvent): Map<string, number> {
|
export function countVotes(userVotes: Map<string, UserVote>, pollStart: PollStartEvent): Map<string, UserVote[]> {
|
||||||
const collected = new Map<string, number>();
|
const collected = new Map<string, UserVote[]>();
|
||||||
|
|
||||||
for (const response of userVotes.values()) {
|
for (const response of userVotes.values()) {
|
||||||
const tempResponse = PollResponseEvent.from(response.answers, "$irrelevant");
|
const tempResponse = PollResponseEvent.from(response.answers, "$irrelevant");
|
||||||
tempResponse.validateAgainst(pollStart);
|
tempResponse.validateAgainst(pollStart);
|
||||||
if (!tempResponse.spoiled) {
|
if (!tempResponse.spoiled) {
|
||||||
for (const answerId of tempResponse.answerIds) {
|
for (const answerId of tempResponse.answerIds) {
|
||||||
if (collected.has(answerId)) {
|
const previousVotes = collected.get(answerId) ?? [];
|
||||||
collected.set(answerId, collected.get(answerId)! + 1);
|
previousVotes.push(response);
|
||||||
} else {
|
collected.set(answerId, previousVotes);
|
||||||
collected.set(answerId, 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { ReactNode } from "react";
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { PollAnswerSubevent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent";
|
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 { 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 StyledRadioButton from "../elements/StyledRadioButton";
|
||||||
|
import { UserVote } from "../messages/MPollBody";
|
||||||
|
|
||||||
type PollOptionContentProps = {
|
type PollOptionContentProps = {
|
||||||
answer: PollAnswerSubevent;
|
answer: PollAnswerSubevent;
|
||||||
voteCount: number;
|
votes: UserVote[];
|
||||||
displayVoteCount?: boolean;
|
displayVoteCount?: boolean;
|
||||||
isWinner?: boolean;
|
isWinner?: boolean;
|
||||||
};
|
};
|
||||||
const PollOptionContent: React.FC<PollOptionContentProps> = ({ isWinner, answer, voteCount, displayVoteCount }) => {
|
const PollOptionContent: React.FC<PollOptionContentProps> = ({ isWinner, answer, votes, displayVoteCount }) => {
|
||||||
const votesText = displayVoteCount ? _t("timeline|m.poll|count_of_votes", { count: voteCount }) : "";
|
const votesText = displayVoteCount ? _t("timeline|m.poll|count_of_votes", { count: votes.length }) : "";
|
||||||
|
const room = useContext(RoomContext).room!;
|
||||||
|
const members = useRoomMembers(room);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_PollOption_content">
|
<div className="mx_PollOption_content">
|
||||||
<div className="mx_PollOption_optionText">{answer.text}</div>
|
<div className="mx_PollOption_optionText">{answer.text}</div>
|
||||||
<div className="mx_PollOption_optionVoteCount">
|
<div className="mx_PollOption_optionVoteCount">
|
||||||
{isWinner && <TrophyIcon className="mx_PollOption_winnerIcon" />}
|
{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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -42,7 +57,7 @@ interface PollOptionProps extends PollOptionContentProps {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EndedPollOption: React.FC<Omit<PollOptionProps, "voteCount" | "totalVoteCount">> = ({
|
const EndedPollOption: React.FC<Omit<PollOptionProps, "votes" | "totalVoteCount">> = ({
|
||||||
isChecked,
|
isChecked,
|
||||||
children,
|
children,
|
||||||
answer,
|
answer,
|
||||||
|
@ -57,7 +72,7 @@ const EndedPollOption: React.FC<Omit<PollOptionProps, "voteCount" | "totalVoteCo
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const ActivePollOption: React.FC<Omit<PollOptionProps, "voteCount" | "totalVoteCount">> = ({
|
const ActivePollOption: React.FC<Omit<PollOptionProps, "votes" | "totalVoteCount">> = ({
|
||||||
pollId,
|
pollId,
|
||||||
isChecked,
|
isChecked,
|
||||||
children,
|
children,
|
||||||
|
@ -78,7 +93,7 @@ const ActivePollOption: React.FC<Omit<PollOptionProps, "voteCount" | "totalVoteC
|
||||||
export const PollOption: React.FC<PollOptionProps> = ({
|
export const PollOption: React.FC<PollOptionProps> = ({
|
||||||
pollId,
|
pollId,
|
||||||
answer,
|
answer,
|
||||||
voteCount,
|
votes: voteCount,
|
||||||
totalVoteCount,
|
totalVoteCount,
|
||||||
displayVoteCount,
|
displayVoteCount,
|
||||||
isEnded,
|
isEnded,
|
||||||
|
@ -91,7 +106,7 @@ export const PollOption: React.FC<PollOptionProps> = ({
|
||||||
mx_PollOption_ended: isEnded,
|
mx_PollOption_ended: isEnded,
|
||||||
});
|
});
|
||||||
const isWinner = isEnded && isChecked;
|
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;
|
const PollOptionWrapper = isEnded ? EndedPollOption : ActivePollOption;
|
||||||
return (
|
return (
|
||||||
<div data-testid={`pollOption-${answer.id}`} className={cls} onClick={() => onOptionSelected?.(answer.id)}>
|
<div data-testid={`pollOption-${answer.id}`} className={cls} onClick={() => onOptionSelected?.(answer.id)}>
|
||||||
|
@ -104,7 +119,7 @@ export const PollOption: React.FC<PollOptionProps> = ({
|
||||||
<PollOptionContent
|
<PollOptionContent
|
||||||
isWinner={isWinner}
|
isWinner={isWinner}
|
||||||
answer={answer}
|
answer={answer}
|
||||||
voteCount={voteCount}
|
votes={voteCount}
|
||||||
displayVoteCount={displayVoteCount}
|
displayVoteCount={displayVoteCount}
|
||||||
/>
|
/>
|
||||||
</PollOptionWrapper>
|
</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.
|
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 { PollAnswerSubevent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent";
|
||||||
import { MatrixEvent, Poll, PollEvent, Relations } from "matrix-js-sdk/src/matrix";
|
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 { Icon as PollIcon } from "../../../../../res/img/element-icons/room/composer/poll.svg";
|
||||||
import { _t } from "../../../../languageHandler";
|
|
||||||
import { formatLocalDateShort } from "../../../../DateUtils";
|
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 { PollOption } from "../../polls/PollOption";
|
||||||
import { Caption } from "../../typography/Caption";
|
import { Caption } from "../../typography/Caption";
|
||||||
|
|
||||||
|
@ -27,23 +27,23 @@ interface Props {
|
||||||
type EndedPollState = {
|
type EndedPollState = {
|
||||||
winningAnswers: {
|
winningAnswers: {
|
||||||
answer: PollAnswerSubevent;
|
answer: PollAnswerSubevent;
|
||||||
voteCount: number;
|
votes: UserVote[];
|
||||||
}[];
|
}[];
|
||||||
totalVoteCount: number;
|
totalVoteCount: number;
|
||||||
};
|
};
|
||||||
const getWinningAnswers = (poll: Poll, responseRelations: Relations): EndedPollState => {
|
const getWinningAnswers = (poll: Poll, responseRelations: Relations): EndedPollState => {
|
||||||
const userVotes = collectUserVotes(allVotes(responseRelations));
|
const userVotes = collectUserVotes(allVotes(responseRelations));
|
||||||
const votes = countVotes(userVotes, poll.pollEvent);
|
const votes = countVotes(userVotes, poll.pollEvent);
|
||||||
const totalVoteCount = [...votes.values()].reduce((sum, vote) => sum + vote, 0);
|
const totalVoteCount = [...votes.values()].reduce((sum, vote) => sum + vote.length, 0);
|
||||||
const winCount = Math.max(...votes.values());
|
const winCount = Math.max(...Array.from(votes.values()).map(v => v.length));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalVoteCount,
|
totalVoteCount,
|
||||||
winningAnswers: poll.pollEvent.answers
|
winningAnswers: poll.pollEvent.answers
|
||||||
.filter((answer) => votes.get(answer.id) === winCount)
|
.filter((answer) => votes.get(answer.id)?.length === winCount)
|
||||||
.map((answer) => ({
|
.map((answer) => ({
|
||||||
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>
|
</div>
|
||||||
{!!winningAnswers?.length && (
|
{!!winningAnswers?.length && (
|
||||||
<div className="mx_PollListItemEnded_answers">
|
<div className="mx_PollListItemEnded_answers">
|
||||||
{winningAnswers?.map(({ answer, voteCount }) => (
|
{winningAnswers?.map(({ answer, votes }) => (
|
||||||
<PollOption
|
<PollOption
|
||||||
key={answer.id}
|
key={answer.id}
|
||||||
answer={answer}
|
answer={answer}
|
||||||
voteCount={voteCount}
|
votes={votes}
|
||||||
totalVoteCount={totalVoteCount!}
|
totalVoteCount={totalVoteCount!}
|
||||||
pollId={poll.pollId}
|
pollId={poll.pollId}
|
||||||
displayVoteCount
|
displayVoteCount
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue