mirror of
https://github.com/element-hq/element-web
synced 2024-11-25 10:45:51 +03:00
Poll history: fetch last 30 days of polls (#10157)
* use timeline pagination * fetch last 30 days of poll history * add comments, tidy * more comments * finish comment * wait for responses to resolve before displaying in list * dont use state for list * return unsubscribe * strict fixes * unnecessary event type in filter * add catch
This commit is contained in:
parent
3fafa4b58d
commit
d66248c17c
8 changed files with 432 additions and 21 deletions
|
@ -46,3 +46,14 @@ limitations under the License.
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: $secondary-content;
|
color: $secondary-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_PollHistoryList_loading {
|
||||||
|
color: $secondary-content;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
// center in all free space
|
||||||
|
// when there are no results
|
||||||
|
&.mx_PollHistoryList_noResultsYet {
|
||||||
|
margin: auto auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { MatrixEvent, Poll } from "matrix-js-sdk/src/matrix";
|
import { MatrixEvent, Poll } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
@ -23,7 +23,8 @@ import BaseDialog from "../BaseDialog";
|
||||||
import { IDialogProps } from "../IDialogProps";
|
import { IDialogProps } from "../IDialogProps";
|
||||||
import { PollHistoryList } from "./PollHistoryList";
|
import { PollHistoryList } from "./PollHistoryList";
|
||||||
import { PollHistoryFilter } from "./types";
|
import { PollHistoryFilter } from "./types";
|
||||||
import { usePolls } from "./usePollHistory";
|
import { usePollsWithRelations } from "./usePollHistory";
|
||||||
|
import { useFetchPastPolls } from "./fetchPastPolls";
|
||||||
|
|
||||||
type PollHistoryDialogProps = Pick<IDialogProps, "onFinished"> & {
|
type PollHistoryDialogProps = Pick<IDialogProps, "onFinished"> & {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
|
@ -34,7 +35,10 @@ const sortEventsByLatest = (left: MatrixEvent, right: MatrixEvent): number => ri
|
||||||
const filterPolls =
|
const filterPolls =
|
||||||
(filter: PollHistoryFilter) =>
|
(filter: PollHistoryFilter) =>
|
||||||
(poll: Poll): boolean =>
|
(poll: Poll): boolean =>
|
||||||
(filter === "ACTIVE") !== poll.isEnded;
|
// exclude polls while they are still loading
|
||||||
|
// to avoid jitter in list
|
||||||
|
!poll.isFetchingResponses && (filter === "ACTIVE") !== poll.isEnded;
|
||||||
|
|
||||||
const filterAndSortPolls = (polls: Map<string, Poll>, filter: PollHistoryFilter): MatrixEvent[] => {
|
const filterAndSortPolls = (polls: Map<string, Poll>, filter: PollHistoryFilter): MatrixEvent[] => {
|
||||||
return [...polls.values()]
|
return [...polls.values()]
|
||||||
.filter(filterPolls(filter))
|
.filter(filterPolls(filter))
|
||||||
|
@ -43,19 +47,20 @@ const filterAndSortPolls = (polls: Map<string, Poll>, filter: PollHistoryFilter)
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PollHistoryDialog: React.FC<PollHistoryDialogProps> = ({ roomId, matrixClient, onFinished }) => {
|
export const PollHistoryDialog: React.FC<PollHistoryDialogProps> = ({ roomId, matrixClient, onFinished }) => {
|
||||||
const { polls } = usePolls(roomId, matrixClient);
|
const room = matrixClient.getRoom(roomId)!;
|
||||||
|
const { isLoading } = useFetchPastPolls(room, matrixClient);
|
||||||
|
const { polls } = usePollsWithRelations(roomId, matrixClient);
|
||||||
const [filter, setFilter] = useState<PollHistoryFilter>("ACTIVE");
|
const [filter, setFilter] = useState<PollHistoryFilter>("ACTIVE");
|
||||||
const [pollStartEvents, setPollStartEvents] = useState(filterAndSortPolls(polls, filter));
|
|
||||||
|
|
||||||
useEffect(() => {
|
const pollStartEvents = filterAndSortPolls(polls, filter);
|
||||||
setPollStartEvents(filterAndSortPolls(polls, filter));
|
const isLoadingPollResponses = [...polls.values()].some((poll) => poll.isFetchingResponses);
|
||||||
}, [filter, polls]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog title={_t("Polls history")} onFinished={onFinished}>
|
<BaseDialog title={_t("Polls history")} onFinished={onFinished}>
|
||||||
<div className="mx_PollHistoryDialog_content">
|
<div className="mx_PollHistoryDialog_content">
|
||||||
<PollHistoryList
|
<PollHistoryList
|
||||||
pollStartEvents={pollStartEvents}
|
pollStartEvents={pollStartEvents}
|
||||||
|
isLoading={isLoading || isLoadingPollResponses}
|
||||||
polls={polls}
|
polls={polls}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
onFilterChange={setFilter}
|
onFilterChange={setFilter}
|
||||||
|
|
|
@ -19,18 +19,37 @@ import classNames from "classnames";
|
||||||
import { MatrixEvent, Poll } from "matrix-js-sdk/src/matrix";
|
import { MatrixEvent, Poll } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { _t } from "../../../../languageHandler";
|
import { _t } from "../../../../languageHandler";
|
||||||
|
import { FilterTabGroup } from "../../elements/FilterTabGroup";
|
||||||
|
import InlineSpinner from "../../elements/InlineSpinner";
|
||||||
import { PollHistoryFilter } from "./types";
|
import { PollHistoryFilter } from "./types";
|
||||||
import { PollListItem } from "./PollListItem";
|
import { PollListItem } from "./PollListItem";
|
||||||
import { PollListItemEnded } from "./PollListItemEnded";
|
import { PollListItemEnded } from "./PollListItemEnded";
|
||||||
import { FilterTabGroup } from "../../elements/FilterTabGroup";
|
|
||||||
|
const LoadingPolls: React.FC<{ noResultsYet?: boolean }> = ({ noResultsYet }) => (
|
||||||
|
<div
|
||||||
|
className={classNames("mx_PollHistoryList_loading", {
|
||||||
|
mx_PollHistoryList_noResultsYet: noResultsYet,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<InlineSpinner />
|
||||||
|
{_t("Loading polls")}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
type PollHistoryListProps = {
|
type PollHistoryListProps = {
|
||||||
pollStartEvents: MatrixEvent[];
|
pollStartEvents: MatrixEvent[];
|
||||||
polls: Map<string, Poll>;
|
polls: Map<string, Poll>;
|
||||||
filter: PollHistoryFilter;
|
filter: PollHistoryFilter;
|
||||||
onFilterChange: (filter: PollHistoryFilter) => void;
|
onFilterChange: (filter: PollHistoryFilter) => void;
|
||||||
|
isLoading?: boolean;
|
||||||
};
|
};
|
||||||
export const PollHistoryList: React.FC<PollHistoryListProps> = ({ pollStartEvents, polls, filter, onFilterChange }) => {
|
export const PollHistoryList: React.FC<PollHistoryListProps> = ({
|
||||||
|
pollStartEvents,
|
||||||
|
polls,
|
||||||
|
filter,
|
||||||
|
isLoading,
|
||||||
|
onFilterChange,
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="mx_PollHistoryList">
|
<div className="mx_PollHistoryList">
|
||||||
<FilterTabGroup<PollHistoryFilter>
|
<FilterTabGroup<PollHistoryFilter>
|
||||||
|
@ -42,7 +61,7 @@ export const PollHistoryList: React.FC<PollHistoryListProps> = ({ pollStartEvent
|
||||||
{ id: "ENDED", label: "Past polls" },
|
{ id: "ENDED", label: "Past polls" },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{!!pollStartEvents.length ? (
|
{!!pollStartEvents.length && (
|
||||||
<ol className={classNames("mx_PollHistoryList_list", `mx_PollHistoryList_list_${filter}`)}>
|
<ol className={classNames("mx_PollHistoryList_list", `mx_PollHistoryList_list_${filter}`)}>
|
||||||
{pollStartEvents.map((pollStartEvent) =>
|
{pollStartEvents.map((pollStartEvent) =>
|
||||||
filter === "ACTIVE" ? (
|
filter === "ACTIVE" ? (
|
||||||
|
@ -55,14 +74,17 @@ export const PollHistoryList: React.FC<PollHistoryListProps> = ({ pollStartEvent
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
|
{isLoading && <LoadingPolls />}
|
||||||
</ol>
|
</ol>
|
||||||
) : (
|
)}
|
||||||
|
{!pollStartEvents.length && !isLoading && (
|
||||||
<span className="mx_PollHistoryList_noResults">
|
<span className="mx_PollHistoryList_noResults">
|
||||||
{filter === "ACTIVE"
|
{filter === "ACTIVE"
|
||||||
? _t("There are no active polls in this room")
|
? _t("There are no active polls in this room")
|
||||||
: _t("There are no past polls in this room")}
|
: _t("There are no past polls in this room")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{!pollStartEvents.length && isLoading && <LoadingPolls noResultsYet />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
129
src/components/views/dialogs/polls/fetchPastPolls.ts
Normal file
129
src/components/views/dialogs/polls/fetchPastPolls.ts
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
/*
|
||||||
|
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 { useEffect, useState } from "react";
|
||||||
|
import { M_POLL_START } from "matrix-js-sdk/src/@types/polls";
|
||||||
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
import { EventTimeline, EventTimelineSet, Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { Filter, IFilterDefinition } from "matrix-js-sdk/src/filter";
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page timeline backwards until either:
|
||||||
|
* - event older than endOfHistoryPeriodTimestamp is encountered
|
||||||
|
* - end of timeline is reached
|
||||||
|
* @param timelineSet - timelineset to page
|
||||||
|
* @param matrixClient - client
|
||||||
|
* @param endOfHistoryPeriodTimestamp - epoch timestamp to fetch until
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
const pagePolls = async (
|
||||||
|
timelineSet: EventTimelineSet,
|
||||||
|
matrixClient: MatrixClient,
|
||||||
|
endOfHistoryPeriodTimestamp: number,
|
||||||
|
): Promise<void> => {
|
||||||
|
const liveTimeline = timelineSet.getLiveTimeline();
|
||||||
|
const events = liveTimeline.getEvents();
|
||||||
|
const oldestEventTimestamp = events[0]?.getTs() || Date.now();
|
||||||
|
const hasMorePages = !!liveTimeline.getPaginationToken(EventTimeline.BACKWARDS);
|
||||||
|
|
||||||
|
if (!hasMorePages || oldestEventTimestamp <= endOfHistoryPeriodTimestamp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await matrixClient.paginateEventTimeline(liveTimeline, {
|
||||||
|
backwards: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return pagePolls(timelineSet, matrixClient, endOfHistoryPeriodTimestamp);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ONE_DAY_MS = 60000 * 60 * 24;
|
||||||
|
/**
|
||||||
|
* Fetches timeline history for given number of days in past
|
||||||
|
* @param timelineSet - timelineset to page
|
||||||
|
* @param matrixClient - client
|
||||||
|
* @param historyPeriodDays - number of days of history to fetch, from current day
|
||||||
|
* @returns isLoading - true while fetching history
|
||||||
|
*/
|
||||||
|
const useTimelineHistory = (
|
||||||
|
timelineSet: EventTimelineSet | null,
|
||||||
|
matrixClient: MatrixClient,
|
||||||
|
historyPeriodDays: number,
|
||||||
|
): { isLoading: boolean } => {
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!timelineSet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const endOfHistoryPeriodTimestamp = Date.now() - ONE_DAY_MS * historyPeriodDays;
|
||||||
|
|
||||||
|
const doFetchHistory = async (): Promise<void> => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await pagePolls(timelineSet, matrixClient, endOfHistoryPeriodTimestamp);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to fetch room polls history", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
doFetchHistory();
|
||||||
|
}, [timelineSet, historyPeriodDays, matrixClient]);
|
||||||
|
|
||||||
|
return { isLoading };
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterDefinition: IFilterDefinition = {
|
||||||
|
room: {
|
||||||
|
timeline: {
|
||||||
|
types: [M_POLL_START.name, M_POLL_START.altName],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch poll start events in the last N days of room history
|
||||||
|
* @param room - room to fetch history for
|
||||||
|
* @param matrixClient - client
|
||||||
|
* @param historyPeriodDays - number of days of history to fetch, from current day
|
||||||
|
* @returns isLoading - true while fetching history
|
||||||
|
*/
|
||||||
|
export const useFetchPastPolls = (
|
||||||
|
room: Room,
|
||||||
|
matrixClient: MatrixClient,
|
||||||
|
historyPeriodDays = 30,
|
||||||
|
): { isLoading: boolean } => {
|
||||||
|
const [timelineSet, setTimelineSet] = useState<EventTimelineSet | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const filter = new Filter(matrixClient.getSafeUserId());
|
||||||
|
filter.setDefinition(filterDefinition);
|
||||||
|
const getFilteredTimelineSet = async (): Promise<void> => {
|
||||||
|
const filterId = await matrixClient.getOrCreateFilter(`POLL_HISTORY_FILTER_${room.roomId}}`, filter);
|
||||||
|
filter.filterId = filterId;
|
||||||
|
const timelineSet = room.getOrCreateFilteredTimelineSet(filter);
|
||||||
|
setTimelineSet(timelineSet);
|
||||||
|
};
|
||||||
|
|
||||||
|
getFilteredTimelineSet();
|
||||||
|
}, [room, matrixClient]);
|
||||||
|
|
||||||
|
const { isLoading } = useTimelineHistory(timelineSet, matrixClient, historyPeriodDays);
|
||||||
|
|
||||||
|
return { isLoading };
|
||||||
|
};
|
|
@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import { Poll, PollEvent } from "matrix-js-sdk/src/matrix";
|
import { Poll, PollEvent } from "matrix-js-sdk/src/matrix";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
|
||||||
|
@ -21,6 +22,7 @@ import { useEventEmitterState } from "../../../../hooks/useEventEmitter";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get poll instances from a room
|
* Get poll instances from a room
|
||||||
|
* Updates to include new polls
|
||||||
* @param roomId - id of room to retrieve polls for
|
* @param roomId - id of room to retrieve polls for
|
||||||
* @param matrixClient - client
|
* @param matrixClient - client
|
||||||
* @returns {Map<string, Poll>} - Map of Poll instances
|
* @returns {Map<string, Poll>} - Map of Poll instances
|
||||||
|
@ -37,9 +39,58 @@ export const usePolls = (
|
||||||
throw new Error("Cannot find room");
|
throw new Error("Cannot find room");
|
||||||
}
|
}
|
||||||
|
|
||||||
const polls = useEventEmitterState(room, PollEvent.New, () => room.polls);
|
// copy room.polls map so changes can be detected
|
||||||
|
const polls = useEventEmitterState(room, PollEvent.New, () => new Map<string, Poll>(room.polls));
|
||||||
// @TODO(kerrya) watch polls for end events, trigger refiltering
|
|
||||||
|
|
||||||
return { polls };
|
return { polls };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all poll instances from a room
|
||||||
|
* Fetch their responses (using cached poll responses)
|
||||||
|
* Updates on:
|
||||||
|
* - new polls added to room
|
||||||
|
* - new responses added to polls
|
||||||
|
* - changes to poll ended state
|
||||||
|
* @param roomId - id of room to retrieve polls for
|
||||||
|
* @param matrixClient - client
|
||||||
|
* @returns {Map<string, Poll>} - Map of Poll instances
|
||||||
|
*/
|
||||||
|
export const usePollsWithRelations = (
|
||||||
|
roomId: string,
|
||||||
|
matrixClient: MatrixClient,
|
||||||
|
): {
|
||||||
|
polls: Map<string, Poll>;
|
||||||
|
} => {
|
||||||
|
const { polls } = usePolls(roomId, matrixClient);
|
||||||
|
const [pollsWithRelations, setPollsWithRelations] = useState<Map<string, Poll>>(polls);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onPollUpdate = async (): Promise<void> => {
|
||||||
|
// trigger rerender by creating a new poll map
|
||||||
|
setPollsWithRelations(new Map(polls));
|
||||||
|
};
|
||||||
|
if (polls) {
|
||||||
|
for (const poll of polls.values()) {
|
||||||
|
// listen to changes in responses and end state
|
||||||
|
poll.on(PollEvent.End, onPollUpdate);
|
||||||
|
poll.on(PollEvent.Responses, onPollUpdate);
|
||||||
|
// trigger request to get all responses
|
||||||
|
// if they are not already in cache
|
||||||
|
poll.getResponses();
|
||||||
|
}
|
||||||
|
setPollsWithRelations(polls);
|
||||||
|
}
|
||||||
|
// unsubscribe
|
||||||
|
return () => {
|
||||||
|
if (polls) {
|
||||||
|
for (const poll of polls.values()) {
|
||||||
|
poll.off(PollEvent.End, onPollUpdate);
|
||||||
|
poll.off(PollEvent.Responses, onPollUpdate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [polls, setPollsWithRelations]);
|
||||||
|
|
||||||
|
return { polls: pollsWithRelations };
|
||||||
|
};
|
||||||
|
|
|
@ -3131,6 +3131,7 @@
|
||||||
"Not a valid Security Key": "Not a valid Security Key",
|
"Not a valid Security Key": "Not a valid Security Key",
|
||||||
"Access your secure message history and set up secure messaging by entering your Security Key.": "Access your secure message history and set up secure messaging by entering your Security Key.",
|
"Access your secure message history and set up secure messaging by entering your Security Key.": "Access your secure message history and set up secure messaging by entering your Security Key.",
|
||||||
"If you've forgotten your Security Key you can <button>set up new recovery options</button>": "If you've forgotten your Security Key you can <button>set up new recovery options</button>",
|
"If you've forgotten your Security Key you can <button>set up new recovery options</button>": "If you've forgotten your Security Key you can <button>set up new recovery options</button>",
|
||||||
|
"Loading polls": "Loading polls",
|
||||||
"There are no active polls in this room": "There are no active polls in this room",
|
"There are no active polls in this room": "There are no active polls in this room",
|
||||||
"There are no past polls in this room": "There are no past polls in this room",
|
"There are no past polls in this room": "There are no past polls in this room",
|
||||||
"Send custom account data event": "Send custom account data event",
|
"Send custom account data event": "Send custom account data event",
|
||||||
|
|
|
@ -16,10 +16,13 @@ limitations under the License.
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { fireEvent, render } from "@testing-library/react";
|
import { fireEvent, render } from "@testing-library/react";
|
||||||
import { Room } from "matrix-js-sdk/src/matrix";
|
import { Filter } from "matrix-js-sdk/src/filter";
|
||||||
|
import { EventTimeline, Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { M_POLL_START } from "matrix-js-sdk/src/@types/polls";
|
||||||
|
|
||||||
import { PollHistoryDialog } from "../../../../../src/components/views/dialogs/polls/PollHistoryDialog";
|
import { PollHistoryDialog } from "../../../../../src/components/views/dialogs/polls/PollHistoryDialog";
|
||||||
import {
|
import {
|
||||||
|
flushPromises,
|
||||||
getMockClientWithEventEmitter,
|
getMockClientWithEventEmitter,
|
||||||
makePollEndEvent,
|
makePollEndEvent,
|
||||||
makePollStartEvent,
|
makePollStartEvent,
|
||||||
|
@ -30,6 +33,8 @@ import {
|
||||||
} from "../../../../test-utils";
|
} from "../../../../test-utils";
|
||||||
|
|
||||||
describe("<PollHistoryDialog />", () => {
|
describe("<PollHistoryDialog />", () => {
|
||||||
|
// 14.03.2022 16:15
|
||||||
|
const now = 1647270879403;
|
||||||
const userId = "@alice:domain.org";
|
const userId = "@alice:domain.org";
|
||||||
const roomId = "!room:domain.org";
|
const roomId = "!room:domain.org";
|
||||||
const mockClient = getMockClientWithEventEmitter({
|
const mockClient = getMockClientWithEventEmitter({
|
||||||
|
@ -37,8 +42,19 @@ describe("<PollHistoryDialog />", () => {
|
||||||
getRoom: jest.fn(),
|
getRoom: jest.fn(),
|
||||||
relations: jest.fn(),
|
relations: jest.fn(),
|
||||||
decryptEventIfNeeded: jest.fn(),
|
decryptEventIfNeeded: jest.fn(),
|
||||||
|
getOrCreateFilter: jest.fn(),
|
||||||
|
paginateEventTimeline: jest.fn(),
|
||||||
|
});
|
||||||
|
let room = new Room(roomId, mockClient, userId);
|
||||||
|
|
||||||
|
const expectedFilter = new Filter(userId);
|
||||||
|
expectedFilter.setDefinition({
|
||||||
|
room: {
|
||||||
|
timeline: {
|
||||||
|
types: [M_POLL_START.name, M_POLL_START.altName],
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const room = new Room(roomId, mockClient, userId);
|
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
roomId,
|
roomId,
|
||||||
|
@ -52,10 +68,16 @@ describe("<PollHistoryDialog />", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
room = new Room(roomId, mockClient, userId);
|
||||||
mockClient.getRoom.mockReturnValue(room);
|
mockClient.getRoom.mockReturnValue(room);
|
||||||
mockClient.relations.mockResolvedValue({ events: [] });
|
mockClient.relations.mockResolvedValue({ events: [] });
|
||||||
const timeline = room.getLiveTimeline();
|
const timeline = room.getLiveTimeline();
|
||||||
jest.spyOn(timeline, "getEvents").mockReturnValue([]);
|
jest.spyOn(timeline, "getEvents").mockReturnValue([]);
|
||||||
|
jest.spyOn(room, "getOrCreateFilteredTimelineSet");
|
||||||
|
mockClient.getOrCreateFilter.mockResolvedValue(expectedFilter.filterId!);
|
||||||
|
mockClient.paginateEventTimeline.mockReset().mockResolvedValue(false);
|
||||||
|
|
||||||
|
jest.spyOn(Date, "now").mockReturnValue(now);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
|
@ -68,21 +90,161 @@ describe("<PollHistoryDialog />", () => {
|
||||||
expect(() => getComponent()).toThrow("Cannot find room");
|
expect(() => getComponent()).toThrow("Cannot find room");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders a no polls message when there are no active polls in the timeline", () => {
|
it("renders a loading message while poll history is fetched", async () => {
|
||||||
|
const timelineSet = room.getOrCreateFilteredTimelineSet(expectedFilter);
|
||||||
|
const liveTimeline = timelineSet.getLiveTimeline();
|
||||||
|
jest.spyOn(liveTimeline, "getPaginationToken").mockReturnValueOnce("test-pagination-token");
|
||||||
|
|
||||||
|
const { queryByText, getByText } = getComponent();
|
||||||
|
|
||||||
|
expect(mockClient.getOrCreateFilter).toHaveBeenCalledWith(
|
||||||
|
`POLL_HISTORY_FILTER_${room.roomId}}`,
|
||||||
|
expectedFilter,
|
||||||
|
);
|
||||||
|
// no results not shown until loading finished
|
||||||
|
expect(queryByText("There are no active polls in this room")).not.toBeInTheDocument();
|
||||||
|
expect(getByText("Loading polls")).toBeInTheDocument();
|
||||||
|
|
||||||
|
// flush filter creation request
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(liveTimeline.getPaginationToken).toHaveBeenCalledWith(EventTimeline.BACKWARDS);
|
||||||
|
expect(mockClient.paginateEventTimeline).toHaveBeenCalledWith(liveTimeline, { backwards: true });
|
||||||
|
// only one page
|
||||||
|
expect(mockClient.paginateEventTimeline).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// finished loading
|
||||||
|
expect(queryByText("Loading polls")).not.toBeInTheDocument();
|
||||||
|
expect(getByText("There are no active polls in this room")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fetches poll history until end of timeline is reached while within time limit", async () => {
|
||||||
|
const timelineSet = room.getOrCreateFilteredTimelineSet(expectedFilter);
|
||||||
|
const liveTimeline = timelineSet.getLiveTimeline();
|
||||||
|
|
||||||
|
// mock three pages of timeline history
|
||||||
|
jest.spyOn(liveTimeline, "getPaginationToken")
|
||||||
|
.mockReturnValueOnce("test-pagination-token-1")
|
||||||
|
.mockReturnValueOnce("test-pagination-token-2")
|
||||||
|
.mockReturnValueOnce("test-pagination-token-3");
|
||||||
|
|
||||||
|
const { queryByText, getByText } = getComponent();
|
||||||
|
|
||||||
|
expect(mockClient.getOrCreateFilter).toHaveBeenCalledWith(
|
||||||
|
`POLL_HISTORY_FILTER_${room.roomId}}`,
|
||||||
|
expectedFilter,
|
||||||
|
);
|
||||||
|
|
||||||
|
// flush filter creation request
|
||||||
|
await flushPromises();
|
||||||
|
// once per page
|
||||||
|
expect(mockClient.paginateEventTimeline).toHaveBeenCalledTimes(3);
|
||||||
|
|
||||||
|
// finished loading
|
||||||
|
expect(queryByText("Loading polls")).not.toBeInTheDocument();
|
||||||
|
expect(getByText("There are no active polls in this room")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fetches poll history until event older than history period is reached", async () => {
|
||||||
|
const timelineSet = room.getOrCreateFilteredTimelineSet(expectedFilter);
|
||||||
|
const liveTimeline = timelineSet.getLiveTimeline();
|
||||||
|
const thirtyOneDaysAgoTs = now - 60000 * 60 * 24 * 31;
|
||||||
|
|
||||||
|
jest.spyOn(liveTimeline, "getEvents")
|
||||||
|
.mockReturnValueOnce([])
|
||||||
|
.mockReturnValueOnce([makePollStartEvent("Question?", userId, undefined, { ts: thirtyOneDaysAgoTs })]);
|
||||||
|
|
||||||
|
// mock three pages of timeline history
|
||||||
|
jest.spyOn(liveTimeline, "getPaginationToken")
|
||||||
|
.mockReturnValueOnce("test-pagination-token-1")
|
||||||
|
.mockReturnValueOnce("test-pagination-token-2")
|
||||||
|
.mockReturnValueOnce("test-pagination-token-3");
|
||||||
|
|
||||||
|
getComponent();
|
||||||
|
|
||||||
|
// flush filter creation request
|
||||||
|
await flushPromises();
|
||||||
|
// after first fetch the time limit is reached
|
||||||
|
// stop paging
|
||||||
|
expect(mockClient.paginateEventTimeline).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays loader and list while paging timeline", async () => {
|
||||||
|
const timelineSet = room.getOrCreateFilteredTimelineSet(expectedFilter);
|
||||||
|
const liveTimeline = timelineSet.getLiveTimeline();
|
||||||
|
const tenDaysAgoTs = now - 60000 * 60 * 24 * 10;
|
||||||
|
|
||||||
|
jest.spyOn(liveTimeline, "getEvents").mockReset().mockReturnValue([]);
|
||||||
|
|
||||||
|
// mock three pages of timeline history
|
||||||
|
jest.spyOn(liveTimeline, "getPaginationToken")
|
||||||
|
.mockReturnValueOnce("test-pagination-token-1")
|
||||||
|
.mockReturnValueOnce("test-pagination-token-2")
|
||||||
|
.mockReturnValueOnce("test-pagination-token-3");
|
||||||
|
|
||||||
|
// reference to pagination resolve, so we can assert between pages
|
||||||
|
let resolvePagination1: (value: boolean) => void | undefined;
|
||||||
|
let resolvePagination2: (value: boolean) => void | undefined;
|
||||||
|
mockClient.paginateEventTimeline
|
||||||
|
.mockImplementationOnce(async (_p) => {
|
||||||
|
const pollStart = makePollStartEvent("Question?", userId, undefined, { ts: now, id: "1" });
|
||||||
|
jest.spyOn(liveTimeline, "getEvents").mockReturnValue([pollStart]);
|
||||||
|
room.processPollEvents([pollStart]);
|
||||||
|
return new Promise((resolve) => (resolvePagination1 = resolve));
|
||||||
|
})
|
||||||
|
.mockImplementationOnce(async (_p) => {
|
||||||
|
const pollStart = makePollStartEvent("Older question?", userId, undefined, {
|
||||||
|
ts: tenDaysAgoTs,
|
||||||
|
id: "2",
|
||||||
|
});
|
||||||
|
jest.spyOn(liveTimeline, "getEvents").mockReturnValue([pollStart]);
|
||||||
|
room.processPollEvents([pollStart]);
|
||||||
|
return new Promise((resolve) => (resolvePagination2 = resolve));
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getByText, queryByText } = getComponent();
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(mockClient.paginateEventTimeline).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
resolvePagination1!(true);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
// first page has results, display immediately
|
||||||
|
expect(getByText("Question?")).toBeInTheDocument();
|
||||||
|
// but we are still fetching history, diaply loader
|
||||||
|
expect(getByText("Loading polls")).toBeInTheDocument();
|
||||||
|
|
||||||
|
resolvePagination2!(true);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
// additional results addeds
|
||||||
|
expect(getByText("Older question?")).toBeInTheDocument();
|
||||||
|
expect(getByText("Question?")).toBeInTheDocument();
|
||||||
|
// finished paging
|
||||||
|
expect(queryByText("Loading polls")).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(mockClient.paginateEventTimeline).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders a no polls message when there are no active polls in the room", async () => {
|
||||||
const { getByText } = getComponent();
|
const { getByText } = getComponent();
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
expect(getByText("There are no active polls in this room")).toBeTruthy();
|
expect(getByText("There are no active polls in this room")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders a no past polls message when there are no past polls in the timeline", () => {
|
it("renders a no past polls message when there are no past polls in the room", async () => {
|
||||||
const { getByText } = getComponent();
|
const { getByText } = getComponent();
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
fireEvent.click(getByText("Past polls"));
|
fireEvent.click(getByText("Past polls"));
|
||||||
|
|
||||||
expect(getByText("There are no past polls in this room")).toBeTruthy();
|
expect(getByText("There are no past polls in this room")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders a list of active polls when there are polls in the timeline", async () => {
|
it("renders a list of active polls when there are polls in the room", async () => {
|
||||||
const timestamp = 1675300825090;
|
const timestamp = 1675300825090;
|
||||||
const pollStart1 = makePollStartEvent("Question?", userId, undefined, { ts: timestamp, id: "$1" });
|
const pollStart1 = makePollStartEvent("Question?", userId, undefined, { ts: timestamp, id: "$1" });
|
||||||
const pollStart2 = makePollStartEvent("Where?", userId, undefined, { ts: timestamp + 10000, id: "$2" });
|
const pollStart2 = makePollStartEvent("Where?", userId, undefined, { ts: timestamp + 10000, id: "$2" });
|
||||||
|
@ -92,6 +254,9 @@ describe("<PollHistoryDialog />", () => {
|
||||||
|
|
||||||
const { container, queryByText, getByTestId } = getComponent();
|
const { container, queryByText, getByTestId } = getComponent();
|
||||||
|
|
||||||
|
// flush relations calls for polls
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
expect(getByTestId("filter-tab-PollHistoryDialog_filter-ACTIVE").firstElementChild).toBeChecked();
|
expect(getByTestId("filter-tab-PollHistoryDialog_filter-ACTIVE").firstElementChild).toBeChecked();
|
||||||
|
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
|
@ -99,6 +264,32 @@ describe("<PollHistoryDialog />", () => {
|
||||||
expect(queryByText("What?")).not.toBeInTheDocument();
|
expect(queryByText("What?")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("updates when new polls are added to the room", async () => {
|
||||||
|
const timestamp = 1675300825090;
|
||||||
|
const pollStart1 = makePollStartEvent("Question?", userId, undefined, { ts: timestamp, id: "$1" });
|
||||||
|
const pollStart2 = makePollStartEvent("Where?", userId, undefined, { ts: timestamp + 10000, id: "$2" });
|
||||||
|
// initially room has only one poll
|
||||||
|
await setupRoomWithPollEvents([pollStart1], [], [], mockClient, room);
|
||||||
|
|
||||||
|
const { getByText } = getComponent();
|
||||||
|
|
||||||
|
// wait for relations
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(getByText("Question?")).toBeInTheDocument();
|
||||||
|
|
||||||
|
// add another poll
|
||||||
|
// paged history requests using cli.paginateEventTimeline
|
||||||
|
// call this with new events
|
||||||
|
await room.processPollEvents([pollStart2]);
|
||||||
|
// await relations for new poll
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(getByText("Question?")).toBeInTheDocument();
|
||||||
|
// list updated to include new poll
|
||||||
|
expect(getByText("Where?")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("filters ended polls", async () => {
|
it("filters ended polls", async () => {
|
||||||
const pollStart1 = makePollStartEvent("Question?", userId, undefined, { ts: 1675300825090, id: "$1" });
|
const pollStart1 = makePollStartEvent("Question?", userId, undefined, { ts: 1675300825090, id: "$1" });
|
||||||
const pollStart2 = makePollStartEvent("Where?", userId, undefined, { ts: 1675300725090, id: "$2" });
|
const pollStart2 = makePollStartEvent("Where?", userId, undefined, { ts: 1675300725090, id: "$2" });
|
||||||
|
@ -107,6 +298,7 @@ describe("<PollHistoryDialog />", () => {
|
||||||
await setupRoomWithPollEvents([pollStart1, pollStart2, pollStart3], [], [pollEnd3], mockClient, room);
|
await setupRoomWithPollEvents([pollStart1, pollStart2, pollStart3], [], [pollEnd3], mockClient, room);
|
||||||
|
|
||||||
const { getByText, queryByText, getByTestId } = getComponent();
|
const { getByText, queryByText, getByTestId } = getComponent();
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
expect(getByText("Question?")).toBeInTheDocument();
|
expect(getByText("Question?")).toBeInTheDocument();
|
||||||
expect(getByText("Where?")).toBeInTheDocument();
|
expect(getByText("Where?")).toBeInTheDocument();
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`<PollHistoryDialog /> renders a list of active polls when there are polls in the timeline 1`] = `
|
exports[`<PollHistoryDialog /> renders a list of active polls when there are polls in the room 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
data-focus-guard="true"
|
data-focus-guard="true"
|
||||||
|
|
Loading…
Reference in a new issue