Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Poll history - ended polls list items #10119

Merged
merged 58 commits into from
Feb 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
1fd5654
wip
Jan 9, 2023
2c01d2e
remove dupe
Jan 9, 2023
4ac6f5f
Merge branch 'develop' into kerry/poll-model
Jan 11, 2023
567e59e
Merge branch 'develop' into kerry/poll-model
Jan 16, 2023
6adc726
Merge branch 'develop' into kerry/poll-model
Jan 26, 2023
563bffb
use poll model relations in all cases
Jan 26, 2023
c87bf54
update mpollbody tests to use poll instance
Jan 26, 2023
4865dd5
update poll fetching login in pinned messages card
Jan 27, 2023
962eb5d
add pinned polls to room polls state
Jan 27, 2023
19de598
add spinner while relations are still loading
Jan 29, 2023
f97f70c
Merge branch 'develop' into kerry/poll-model
Jan 29, 2023
42a31ce
handle no poll in end poll dialog
Jan 29, 2023
903a2e0
Merge branch 'kerry/poll-model' of https://github.com/matrix-org/matr…
Jan 29, 2023
8512e51
strict errors
Jan 30, 2023
7ffd6e9
render a poll body that errors for poll end events
Jan 18, 2023
dca5835
add fetching logic to pollend tile
Jan 31, 2023
def87ec
extract poll testing utilities
Jan 31, 2023
6dbcd43
test mpollend
Jan 31, 2023
6d7cff6
Merge branch 'develop' into kerry/poll-model
Feb 1, 2023
08d2b3e
strict fix
Feb 1, 2023
a76d6a4
Merge branch 'develop' into kerry/poll-model
Feb 1, 2023
c345b7c
more strict fix
Feb 2, 2023
bc2f596
Merge branch 'kerry/poll-model' into psg-905/poll-end-in-timeline
Feb 2, 2023
7d818c1
strict fix for forwardref
Feb 2, 2023
282c0cb
Merge branch 'develop' into psg-905/poll-end-in-timeline
Feb 2, 2023
82c28b7
add filter component
Feb 3, 2023
6aa4288
Merge branch 'develop' into psg-905/poll-end-in-timeline
Feb 3, 2023
a6a6128
update poll test utils
Feb 3, 2023
7a103b7
Merge branch 'psg-905/poll-end-in-timeline' into psg-906/poll-history…
Feb 3, 2023
335f4ca
add unstyled filter tab group
Feb 3, 2023
81f9117
filtertabgroup snapshot
Feb 3, 2023
44c8305
Merge branch 'develop' into psg-906/poll-history-active-filter
Feb 7, 2023
fcba7fa
lint
Feb 7, 2023
b477168
update test util setupRoomWithPollEvents to allow testing multiple po…
Feb 7, 2023
9e7b017
style filter tabs
Feb 7, 2023
5dbab27
test error message for past polls
Feb 7, 2023
a912577
sort polls list by latest
Feb 7, 2023
80e10c4
Merge branch 'develop' into psg-906/poll-history-active-filter
Feb 7, 2023
aadb338
Merge branch 'develop' into psg-906/poll-history-active-filter
Feb 7, 2023
4a41ee4
extract poll option display components from pollbody
Feb 8, 2023
eeffebc
Merge branch 'develop' into psg-906/poll-history-active-filter
Feb 8, 2023
74ce582
add ended poll list item component
Feb 9, 2023
af3d83a
use named export for polllistitem
Feb 9, 2023
6456da4
test POllListItemEnded
Feb 9, 2023
802fbb9
comments
Feb 9, 2023
21e1e15
strict fixes
Feb 9, 2023
909745d
extract poll option display components
Feb 9, 2023
fdd2428
Merge branch 'psg-1030/poll-history-extract-poll-option' into psg-103…
Feb 9, 2023
f7e795b
strict fixes
Feb 9, 2023
8b68e32
Merge branch 'psg-1030/poll-history-extract-poll-option' into psg-103…
Feb 9, 2023
286a64b
Merge branch 'develop' into psg-1030/poll-history-ended-poll-list
Feb 13, 2023
43d8c7d
Merge branch 'psg-1030/poll-history-ended-poll-list' of https://githu…
Feb 13, 2023
469ab65
strict
Feb 13, 2023
01200a7
Merge branch 'develop' into psg-1030/poll-history-ended-poll-list
Feb 14, 2023
7d8041d
Merge branch 'develop' into psg-1030/poll-history-ended-poll-list
Feb 16, 2023
6a2799d
Merge branch 'develop' into psg-1030/poll-history-ended-poll-list
Feb 16, 2023
05805bd
Merge branch 'develop' into psg-1030/poll-history-ended-poll-list
Feb 19, 2023
6a065fe
Merge branch 'develop' into psg-1030/poll-history-ended-poll-list
Feb 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions res/css/_components.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
@import "./components/views/beacon/_StyledLiveBeaconIcon.pcss";
@import "./components/views/context_menus/_KebabContextMenu.pcss";
@import "./components/views/dialogs/polls/_PollListItem.pcss";
@import "./components/views/dialogs/polls/_PollListItemEnded.pcss";
@import "./components/views/elements/_FilterDropdown.pcss";
@import "./components/views/elements/_FilterTabGroup.pcss";
@import "./components/views/elements/_LearnMore.pcss";
Expand Down
60 changes: 60 additions & 0 deletions res/css/components/views/dialogs/polls/_PollListItemEnded.pcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

.mx_PollListItemEnded {
width: 100%;
display: flex;
flex-direction: column;
color: $primary-content;
}

.mx_PollListItemEnded_title {
display: grid;
justify-content: left;
align-items: center;
grid-gap: $spacing-8;
grid-template-columns: min-content 1fr min-content;
grid-template-rows: auto;
}

.mx_PollListItemEnded_icon {
height: 14px;
width: 14px;
color: $quaternary-content;
padding-left: $spacing-8;
}

.mx_PollListItemEnded_date {
font-size: $font-12px;
color: $secondary-content;
}

.mx_PollListItemEnded_question {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.mx_PollListItemEnded_answers {
display: grid;
grid-gap: $spacing-8;
margin-top: $spacing-12;
}

.mx_PollListItemEnded_voteCount {
// 6px to match PollOption padding
margin: $spacing-8 0 0 6px;
}
1 change: 0 additions & 1 deletion res/css/components/views/polls/_PollOption.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ limitations under the License.
border: 1px solid $quinary-content;
border-radius: 8px;
padding: 6px 12px;
max-width: 550px;
background-color: $background;

.mx_StyledRadioButton_content,
Expand Down
4 changes: 4 additions & 0 deletions res/css/views/dialogs/polls/_PollHistoryList.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ limitations under the License.
grid-gap: $spacing-20;
padding-right: $spacing-64;
margin: $spacing-32 0;

&.mx_PollHistoryList_list_ENDED {
grid-gap: $spacing-32;
}
}

.mx_PollHistoryList_noResults {
Expand Down
1 change: 1 addition & 0 deletions res/css/views/messages/_MPollBody.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,5 @@ limitations under the License.
display: grid;
grid-gap: $spacing-16;
margin-bottom: $spacing-8;
max-width: 550px;
}
7 changes: 6 additions & 1 deletion src/components/views/dialogs/polls/PollHistoryDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,12 @@ export const PollHistoryDialog: React.FC<PollHistoryDialogProps> = ({ roomId, ma
return (
<BaseDialog title={_t("Polls history")} onFinished={onFinished}>
<div className="mx_PollHistoryDialog_content">
<PollHistoryList pollStartEvents={pollStartEvents} filter={filter} onFilterChange={setFilter} />
<PollHistoryList
pollStartEvents={pollStartEvents}
polls={polls}
filter={filter}
onFilterChange={setFilter}
/>
</div>
</BaseDialog>
);
Expand Down
27 changes: 19 additions & 8 deletions src/components/views/dialogs/polls/PollHistoryList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,22 @@ limitations under the License.
*/

import React from "react";
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
import classNames from "classnames";
import { MatrixEvent, Poll } from "matrix-js-sdk/src/matrix";

import PollListItem from "./PollListItem";
import { _t } from "../../../../languageHandler";
import { FilterTabGroup } from "../../elements/FilterTabGroup";
import { PollHistoryFilter } from "./types";
import { PollListItem } from "./PollListItem";
import { PollListItemEnded } from "./PollListItemEnded";
import { FilterTabGroup } from "../../elements/FilterTabGroup";

type PollHistoryListProps = {
pollStartEvents: MatrixEvent[];
polls: Map<string, Poll>;
filter: PollHistoryFilter;
onFilterChange: (filter: PollHistoryFilter) => void;
};
export const PollHistoryList: React.FC<PollHistoryListProps> = ({ pollStartEvents, filter, onFilterChange }) => {
export const PollHistoryList: React.FC<PollHistoryListProps> = ({ pollStartEvents, polls, filter, onFilterChange }) => {
return (
<div className="mx_PollHistoryList">
<FilterTabGroup<PollHistoryFilter>
Expand All @@ -40,10 +43,18 @@ export const PollHistoryList: React.FC<PollHistoryListProps> = ({ pollStartEvent
]}
/>
{!!pollStartEvents.length ? (
<ol className="mx_PollHistoryList_list">
{pollStartEvents.map((pollStartEvent) => (
<PollListItem key={pollStartEvent.getId()!} event={pollStartEvent} />
))}
<ol className={classNames("mx_PollHistoryList_list", `mx_PollHistoryList_list_${filter}`)}>
{pollStartEvents.map((pollStartEvent) =>
filter === "ACTIVE" ? (
<PollListItem key={pollStartEvent.getId()!} event={pollStartEvent} />
) : (
<PollListItemEnded
key={pollStartEvent.getId()!}
event={pollStartEvent}
poll={polls.get(pollStartEvent.getId()!)!}
/>
),
)}
</ol>
) : (
<span className="mx_PollHistoryList_noResults">
Expand Down
4 changes: 1 addition & 3 deletions src/components/views/dialogs/polls/PollListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ interface Props {
event: MatrixEvent;
}

const PollListItem: React.FC<Props> = ({ event }) => {
export const PollListItem: React.FC<Props> = ({ event }) => {
const pollEvent = event.unstableExtensibleEvent as unknown as PollStartEvent;
if (!pollEvent) {
return null;
Expand All @@ -39,5 +39,3 @@ const PollListItem: React.FC<Props> = ({ event }) => {
</li>
);
};

export default PollListItem;
127 changes: 127 additions & 0 deletions src/components/views/dialogs/polls/PollListItemEnded.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React, { useEffect, useState } from "react";
import { PollAnswerSubevent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent";
import { MatrixEvent, Poll, PollEvent } from "matrix-js-sdk/src/matrix";
import { Relations } from "matrix-js-sdk/src/models/relations";

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 { PollOption } from "../../polls/PollOption";
import { Caption } from "../../typography/Caption";

interface Props {
event: MatrixEvent;
poll: Poll;
}

type EndedPollState = {
winningAnswers: {
answer: PollAnswerSubevent;
voteCount: number;
}[];
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());

return {
totalVoteCount,
winningAnswers: poll.pollEvent.answers
.filter((answer) => votes.get(answer.id) === winCount)
.map((answer) => ({
answer,
voteCount: votes.get(answer.id) || 0,
})),
};
};

/**
* Get deduplicated and validated poll responses
* Will use cached responses from Poll instance when existing
* Updates on changes to Poll responses (paging relations or from sync)
* Returns winning answers and total vote count
*/
const usePollVotes = (poll: Poll): Partial<EndedPollState> => {
const [results, setResults] = useState({ totalVoteCount: 0 });

useEffect(() => {
const getResponses = async (): Promise<void> => {
const responseRelations = await poll.getResponses();
setResults(getWinningAnswers(poll, responseRelations));
};
const onPollResponses = (responseRelations: Relations): void =>
setResults(getWinningAnswers(poll, responseRelations));
poll.on(PollEvent.Responses, onPollResponses);

getResponses();

return () => {
poll.off(PollEvent.Responses, onPollResponses);
};
}, [poll]);

return results;
};

/**
* Render an ended poll with the winning answer and vote count
* @param event - the poll start MatrixEvent
* @param poll - Poll instance
*/
export const PollListItemEnded: React.FC<Props> = ({ event, poll }) => {
const pollEvent = poll.pollEvent;
const { winningAnswers, totalVoteCount } = usePollVotes(poll);
if (!pollEvent) {
return null;
}
const formattedDate = formatLocalDateShort(event.getTs());

return (
<li data-testid={`pollListItem-${event.getId()!}`} className="mx_PollListItemEnded">
<div className="mx_PollListItemEnded_title">
<PollIcon className="mx_PollListItemEnded_icon" />
<span className="mx_PollListItemEnded_question">{pollEvent.question.text}</span>
<Caption>{formattedDate}</Caption>
</div>
{!!winningAnswers?.length && (
<div className="mx_PollListItemEnded_answers">
{winningAnswers?.map(({ answer, voteCount }) => (
<PollOption
key={answer.id}
answer={answer}
voteCount={voteCount}
totalVoteCount={totalVoteCount!}
pollId={poll.pollId}
displayVoteCount
isChecked
isEnded
/>
))}
</div>
)}
<div className="mx_PollListItemEnded_voteCount">
<Caption>{_t("Final result based on %(count)s votes", { count: totalVoteCount })}</Caption>
</div>
</li>
);
};
4 changes: 2 additions & 2 deletions src/components/views/messages/MPollBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ export function allVotes(voteRelations: Relations): Array<UserVote> {
* @param {string?} selected Local echo selected option for the userId
* @returns a Map of user ID to their vote info
*/
function collectUserVotes(
export function collectUserVotes(
userResponses: Array<UserVote>,
userId?: string | null | undefined,
selected?: string | null | undefined,
Expand All @@ -405,7 +405,7 @@ function collectUserVotes(
return userVotes;
}

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

for (const response of userVotes.values()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import React from "react";
import { render } from "@testing-library/react";
import { MatrixEvent } from "matrix-js-sdk/src/matrix";

import PollListItem from "../../../../../src/components/views/dialogs/polls/PollListItem";
import { PollListItem } from "../../../../../src/components/views/dialogs/polls/PollListItem";
import { makePollStartEvent, mockIntlDateTimeFormat, unmockIntlDateTimeFormat } from "../../../../test-utils";

describe("<PollListItem />", () => {
Expand Down
Loading