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:
parent
f0f50485d7
commit
18ab325eaf
15 changed files with 388 additions and 61 deletions
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
22
src/components/views/dialogs/polls/types.ts
Normal file
22
src/components/views/dialogs/polls/types.ts
Normal 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";
|
|
@ -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 };
|
||||
};
|
||||
|
|
57
src/components/views/elements/FilterTabGroup.tsx
Normal file
57
src/components/views/elements/FilterTabGroup.tsx
Normal 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>
|
||||
);
|
Loading…
Add table
Add a link
Reference in a new issue