Poll history - filter by active or ended (#10098)

* wip

* remove dupe

* use poll model relations in all cases

* update mpollbody tests to use poll instance

* update poll fetching login in pinned messages card

* add pinned polls to room polls state

* add spinner while relations are still loading

* handle no poll in end poll dialog

* strict errors

* render a poll body that errors for poll end events

* add fetching logic to pollend tile

* extract poll testing utilities

* test mpollend

* strict fix

* more strict fix

* strict fix for forwardref

* add filter component

* update poll test utils

* add unstyled filter tab group

* filtertabgroup snapshot

* lint

* update test util setupRoomWithPollEvents to allow testing multiple polls in one room

* style filter tabs

* test error message for past polls

* sort polls list by latest

* move FilterTabGroup into generic components

* comments

* Update src/components/views/dialogs/polls/types.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
This commit is contained in:
Kerry 2023-02-13 09:19:45 +13:00 committed by GitHub
parent f0f50485d7
commit 18ab325eaf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 388 additions and 61 deletions

View file

@ -14,26 +14,47 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import React, { useEffect, useState } from "react";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { MatrixEvent, Poll } from "matrix-js-sdk/src/matrix";
import { _t } from "../../../../languageHandler";
import BaseDialog from "../BaseDialog";
import { IDialogProps } from "../IDialogProps";
import { PollHistoryList } from "./PollHistoryList";
import { getPolls } from "./usePollHistory";
import { PollHistoryFilter } from "./types";
import { usePolls } from "./usePollHistory";
type PollHistoryDialogProps = Pick<IDialogProps, "onFinished"> & {
roomId: string;
matrixClient: MatrixClient;
};
const sortEventsByLatest = (left: MatrixEvent, right: MatrixEvent): number => right.getTs() - left.getTs();
const filterPolls =
(filter: PollHistoryFilter) =>
(poll: Poll): boolean =>
(filter === "ACTIVE") !== poll.isEnded;
const filterAndSortPolls = (polls: Map<string, Poll>, filter: PollHistoryFilter): MatrixEvent[] => {
return [...polls.values()]
.filter(filterPolls(filter))
.map((poll) => poll.rootEvent)
.sort(sortEventsByLatest);
};
export const PollHistoryDialog: React.FC<PollHistoryDialogProps> = ({ roomId, matrixClient, onFinished }) => {
const pollStartEvents = getPolls(roomId, matrixClient);
const { polls } = usePolls(roomId, matrixClient);
const [filter, setFilter] = useState<PollHistoryFilter>("ACTIVE");
const [pollStartEvents, setPollStartEvents] = useState(filterAndSortPolls(polls, filter));
useEffect(() => {
setPollStartEvents(filterAndSortPolls(polls, filter));
}, [filter, polls]);
return (
<BaseDialog title={_t("Polls history")} onFinished={onFinished}>
<div className="mx_PollHistoryDialog_content">
<PollHistoryList pollStartEvents={pollStartEvents} />
<PollHistoryList pollStartEvents={pollStartEvents} filter={filter} onFilterChange={setFilter} />
</div>
</BaseDialog>
);

View file

@ -19,13 +19,26 @@ import { MatrixEvent } from "matrix-js-sdk/src/matrix";
import PollListItem from "./PollListItem";
import { _t } from "../../../../languageHandler";
import { FilterTabGroup } from "../../elements/FilterTabGroup";
import { PollHistoryFilter } from "./types";
type PollHistoryListProps = {
pollStartEvents: MatrixEvent[];
filter: PollHistoryFilter;
onFilterChange: (filter: PollHistoryFilter) => void;
};
export const PollHistoryList: React.FC<PollHistoryListProps> = ({ pollStartEvents }) => {
export const PollHistoryList: React.FC<PollHistoryListProps> = ({ pollStartEvents, filter, onFilterChange }) => {
return (
<div className="mx_PollHistoryList">
<FilterTabGroup<PollHistoryFilter>
name="PollHistoryDialog_filter"
value={filter}
onFilterChange={onFilterChange}
tabs={[
{ id: "ACTIVE", label: "Active polls" },
{ id: "ENDED", label: "Past polls" },
]}
/>
{!!pollStartEvents.length ? (
<ol className="mx_PollHistoryList_list">
{pollStartEvents.map((pollStartEvent) => (
@ -33,7 +46,11 @@ export const PollHistoryList: React.FC<PollHistoryListProps> = ({ pollStartEvent
))}
</ol>
) : (
<span className="mx_PollHistoryList_noResults">{_t("There are no polls in this room")}</span>
<span className="mx_PollHistoryList_noResults">
{filter === "ACTIVE"
? _t("There are no active polls in this room")
: _t("There are no past polls in this room")}
</span>
)}
</div>
);

View file

@ -0,0 +1,22 @@
/*
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.
*/
/**
* Possible values for the "filter" setting in the poll history dialog
*
* Ended polls have a valid M_POLL_END event
*/
export type PollHistoryFilter = "ACTIVE" | "ENDED";

View file

@ -14,27 +14,32 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { M_POLL_START } from "matrix-js-sdk/src/@types/polls";
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
import { Poll, PollEvent } from "matrix-js-sdk/src/matrix";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { useEventEmitterState } from "../../../../hooks/useEventEmitter";
/**
* Get poll start events in a rooms live timeline
* Get poll instances from a room
* @param roomId - id of room to retrieve polls for
* @param matrixClient - client
* @returns {MatrixEvent[]} - array fo poll start events
* @returns {Map<string, Poll>} - Map of Poll instances
*/
export const getPolls = (roomId: string, matrixClient: MatrixClient): MatrixEvent[] => {
export const usePolls = (
roomId: string,
matrixClient: MatrixClient,
): {
polls: Map<string, Poll>;
} => {
const room = matrixClient.getRoom(roomId);
if (!room) {
throw new Error("Cannot find room");
}
// @TODO(kerrya) poll history will be actively fetched in PSG-1043
// for now, just display polls that are in the current timeline
const timelineEvents = room.getLiveTimeline().getEvents();
const pollStartEvents = timelineEvents.filter((event) => M_POLL_START.matches(event.getType()));
const polls = useEventEmitterState(room, PollEvent.New, () => room.polls);
return pollStartEvents;
// @TODO(kerrya) watch polls for end events, trigger refiltering
return { polls };
};

View file

@ -0,0 +1,57 @@
/*
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, { FieldsetHTMLAttributes, ReactNode } from "react";
export type FilterTab<T> = {
label: string | ReactNode;
id: T;
};
type FilterTabGroupProps<T extends string = string> = FieldsetHTMLAttributes<any> & {
// group name used for radio buttons
name: string;
onFilterChange: (id: T) => void;
// active tab's id
value: T;
// tabs to display
tabs: FilterTab<T>[];
};
/**
* React component which styles a set of content filters as tabs
*
* This is used in displays which show a list of content items, and the user can select between one of several
* filters for those items. For example, in the Poll History dialog, the user can select between "Active" and "Ended"
* polls.
*
* Type `T` is used for the `value` attribute for the buttons in the radio group.
*/
export const FilterTabGroup = <T extends string = string>({
name,
value,
tabs,
onFilterChange,
...rest
}: FilterTabGroupProps<T>): JSX.Element => (
<fieldset {...rest} className="mx_FilterTabGroup">
{tabs.map(({ label, id }) => (
<label data-testid={`filter-tab-${name}-${id}`} key={id}>
<input type="radio" name={name} value={id} onChange={() => onFilterChange(id)} checked={value === id} />
<span>{label}</span>
</label>
))}
</fieldset>
);