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

Commit d66248c

Browse files
author
Kerry
authored
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
1 parent 3fafa4b commit d66248c

File tree

8 files changed

+432
-21
lines changed

8 files changed

+432
-21
lines changed

res/css/views/dialogs/polls/_PollHistoryList.pcss

+11
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,14 @@ limitations under the License.
4646
justify-content: center;
4747
color: $secondary-content;
4848
}
49+
50+
.mx_PollHistoryList_loading {
51+
color: $secondary-content;
52+
text-align: center;
53+
54+
// center in all free space
55+
// when there are no results
56+
&.mx_PollHistoryList_noResultsYet {
57+
margin: auto auto;
58+
}
59+
}

src/components/views/dialogs/polls/PollHistoryDialog.tsx

+13-8
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import React, { useEffect, useState } from "react";
17+
import React, { useState } from "react";
1818
import { MatrixClient } from "matrix-js-sdk/src/client";
1919
import { MatrixEvent, Poll } from "matrix-js-sdk/src/matrix";
2020

@@ -23,7 +23,8 @@ import BaseDialog from "../BaseDialog";
2323
import { IDialogProps } from "../IDialogProps";
2424
import { PollHistoryList } from "./PollHistoryList";
2525
import { PollHistoryFilter } from "./types";
26-
import { usePolls } from "./usePollHistory";
26+
import { usePollsWithRelations } from "./usePollHistory";
27+
import { useFetchPastPolls } from "./fetchPastPolls";
2728

2829
type PollHistoryDialogProps = Pick<IDialogProps, "onFinished"> & {
2930
roomId: string;
@@ -34,7 +35,10 @@ const sortEventsByLatest = (left: MatrixEvent, right: MatrixEvent): number => ri
3435
const filterPolls =
3536
(filter: PollHistoryFilter) =>
3637
(poll: Poll): boolean =>
37-
(filter === "ACTIVE") !== poll.isEnded;
38+
// exclude polls while they are still loading
39+
// to avoid jitter in list
40+
!poll.isFetchingResponses && (filter === "ACTIVE") !== poll.isEnded;
41+
3842
const filterAndSortPolls = (polls: Map<string, Poll>, filter: PollHistoryFilter): MatrixEvent[] => {
3943
return [...polls.values()]
4044
.filter(filterPolls(filter))
@@ -43,19 +47,20 @@ const filterAndSortPolls = (polls: Map<string, Poll>, filter: PollHistoryFilter)
4347
};
4448

4549
export const PollHistoryDialog: React.FC<PollHistoryDialogProps> = ({ roomId, matrixClient, onFinished }) => {
46-
const { polls } = usePolls(roomId, matrixClient);
50+
const room = matrixClient.getRoom(roomId)!;
51+
const { isLoading } = useFetchPastPolls(room, matrixClient);
52+
const { polls } = usePollsWithRelations(roomId, matrixClient);
4753
const [filter, setFilter] = useState<PollHistoryFilter>("ACTIVE");
48-
const [pollStartEvents, setPollStartEvents] = useState(filterAndSortPolls(polls, filter));
4954

50-
useEffect(() => {
51-
setPollStartEvents(filterAndSortPolls(polls, filter));
52-
}, [filter, polls]);
55+
const pollStartEvents = filterAndSortPolls(polls, filter);
56+
const isLoadingPollResponses = [...polls.values()].some((poll) => poll.isFetchingResponses);
5357

5458
return (
5559
<BaseDialog title={_t("Polls history")} onFinished={onFinished}>
5660
<div className="mx_PollHistoryDialog_content">
5761
<PollHistoryList
5862
pollStartEvents={pollStartEvents}
63+
isLoading={isLoading || isLoadingPollResponses}
5964
polls={polls}
6065
filter={filter}
6166
onFilterChange={setFilter}

src/components/views/dialogs/polls/PollHistoryList.tsx

+26-4
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,37 @@ import classNames from "classnames";
1919
import { MatrixEvent, Poll } from "matrix-js-sdk/src/matrix";
2020

2121
import { _t } from "../../../../languageHandler";
22+
import { FilterTabGroup } from "../../elements/FilterTabGroup";
23+
import InlineSpinner from "../../elements/InlineSpinner";
2224
import { PollHistoryFilter } from "./types";
2325
import { PollListItem } from "./PollListItem";
2426
import { PollListItemEnded } from "./PollListItemEnded";
25-
import { FilterTabGroup } from "../../elements/FilterTabGroup";
27+
28+
const LoadingPolls: React.FC<{ noResultsYet?: boolean }> = ({ noResultsYet }) => (
29+
<div
30+
className={classNames("mx_PollHistoryList_loading", {
31+
mx_PollHistoryList_noResultsYet: noResultsYet,
32+
})}
33+
>
34+
<InlineSpinner />
35+
{_t("Loading polls")}
36+
</div>
37+
);
2638

2739
type PollHistoryListProps = {
2840
pollStartEvents: MatrixEvent[];
2941
polls: Map<string, Poll>;
3042
filter: PollHistoryFilter;
3143
onFilterChange: (filter: PollHistoryFilter) => void;
44+
isLoading?: boolean;
3245
};
33-
export const PollHistoryList: React.FC<PollHistoryListProps> = ({ pollStartEvents, polls, filter, onFilterChange }) => {
46+
export const PollHistoryList: React.FC<PollHistoryListProps> = ({
47+
pollStartEvents,
48+
polls,
49+
filter,
50+
isLoading,
51+
onFilterChange,
52+
}) => {
3453
return (
3554
<div className="mx_PollHistoryList">
3655
<FilterTabGroup<PollHistoryFilter>
@@ -42,7 +61,7 @@ export const PollHistoryList: React.FC<PollHistoryListProps> = ({ pollStartEvent
4261
{ id: "ENDED", label: "Past polls" },
4362
]}
4463
/>
45-
{!!pollStartEvents.length ? (
64+
{!!pollStartEvents.length && (
4665
<ol className={classNames("mx_PollHistoryList_list", `mx_PollHistoryList_list_${filter}`)}>
4766
{pollStartEvents.map((pollStartEvent) =>
4867
filter === "ACTIVE" ? (
@@ -55,14 +74,17 @@ export const PollHistoryList: React.FC<PollHistoryListProps> = ({ pollStartEvent
5574
/>
5675
),
5776
)}
77+
{isLoading && <LoadingPolls />}
5878
</ol>
59-
) : (
79+
)}
80+
{!pollStartEvents.length && !isLoading && (
6081
<span className="mx_PollHistoryList_noResults">
6182
{filter === "ACTIVE"
6283
? _t("There are no active polls in this room")
6384
: _t("There are no past polls in this room")}
6485
</span>
6586
)}
87+
{!pollStartEvents.length && isLoading && <LoadingPolls noResultsYet />}
6688
</div>
6789
);
6890
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
Copyright 2023 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { useEffect, useState } from "react";
18+
import { M_POLL_START } from "matrix-js-sdk/src/@types/polls";
19+
import { MatrixClient } from "matrix-js-sdk/src/client";
20+
import { EventTimeline, EventTimelineSet, Room } from "matrix-js-sdk/src/matrix";
21+
import { Filter, IFilterDefinition } from "matrix-js-sdk/src/filter";
22+
import { logger } from "matrix-js-sdk/src/logger";
23+
24+
/**
25+
* Page timeline backwards until either:
26+
* - event older than endOfHistoryPeriodTimestamp is encountered
27+
* - end of timeline is reached
28+
* @param timelineSet - timelineset to page
29+
* @param matrixClient - client
30+
* @param endOfHistoryPeriodTimestamp - epoch timestamp to fetch until
31+
* @returns void
32+
*/
33+
const pagePolls = async (
34+
timelineSet: EventTimelineSet,
35+
matrixClient: MatrixClient,
36+
endOfHistoryPeriodTimestamp: number,
37+
): Promise<void> => {
38+
const liveTimeline = timelineSet.getLiveTimeline();
39+
const events = liveTimeline.getEvents();
40+
const oldestEventTimestamp = events[0]?.getTs() || Date.now();
41+
const hasMorePages = !!liveTimeline.getPaginationToken(EventTimeline.BACKWARDS);
42+
43+
if (!hasMorePages || oldestEventTimestamp <= endOfHistoryPeriodTimestamp) {
44+
return;
45+
}
46+
47+
await matrixClient.paginateEventTimeline(liveTimeline, {
48+
backwards: true,
49+
});
50+
51+
return pagePolls(timelineSet, matrixClient, endOfHistoryPeriodTimestamp);
52+
};
53+
54+
const ONE_DAY_MS = 60000 * 60 * 24;
55+
/**
56+
* Fetches timeline history for given number of days in past
57+
* @param timelineSet - timelineset to page
58+
* @param matrixClient - client
59+
* @param historyPeriodDays - number of days of history to fetch, from current day
60+
* @returns isLoading - true while fetching history
61+
*/
62+
const useTimelineHistory = (
63+
timelineSet: EventTimelineSet | null,
64+
matrixClient: MatrixClient,
65+
historyPeriodDays: number,
66+
): { isLoading: boolean } => {
67+
const [isLoading, setIsLoading] = useState(true);
68+
69+
useEffect(() => {
70+
if (!timelineSet) {
71+
return;
72+
}
73+
const endOfHistoryPeriodTimestamp = Date.now() - ONE_DAY_MS * historyPeriodDays;
74+
75+
const doFetchHistory = async (): Promise<void> => {
76+
setIsLoading(true);
77+
try {
78+
await pagePolls(timelineSet, matrixClient, endOfHistoryPeriodTimestamp);
79+
} catch (error) {
80+
logger.error("Failed to fetch room polls history", error);
81+
} finally {
82+
setIsLoading(false);
83+
}
84+
};
85+
doFetchHistory();
86+
}, [timelineSet, historyPeriodDays, matrixClient]);
87+
88+
return { isLoading };
89+
};
90+
91+
const filterDefinition: IFilterDefinition = {
92+
room: {
93+
timeline: {
94+
types: [M_POLL_START.name, M_POLL_START.altName],
95+
},
96+
},
97+
};
98+
99+
/**
100+
* Fetch poll start events in the last N days of room history
101+
* @param room - room to fetch history for
102+
* @param matrixClient - client
103+
* @param historyPeriodDays - number of days of history to fetch, from current day
104+
* @returns isLoading - true while fetching history
105+
*/
106+
export const useFetchPastPolls = (
107+
room: Room,
108+
matrixClient: MatrixClient,
109+
historyPeriodDays = 30,
110+
): { isLoading: boolean } => {
111+
const [timelineSet, setTimelineSet] = useState<EventTimelineSet | null>(null);
112+
113+
useEffect(() => {
114+
const filter = new Filter(matrixClient.getSafeUserId());
115+
filter.setDefinition(filterDefinition);
116+
const getFilteredTimelineSet = async (): Promise<void> => {
117+
const filterId = await matrixClient.getOrCreateFilter(`POLL_HISTORY_FILTER_${room.roomId}}`, filter);
118+
filter.filterId = filterId;
119+
const timelineSet = room.getOrCreateFilteredTimelineSet(filter);
120+
setTimelineSet(timelineSet);
121+
};
122+
123+
getFilteredTimelineSet();
124+
}, [room, matrixClient]);
125+
126+
const { isLoading } = useTimelineHistory(timelineSet, matrixClient, historyPeriodDays);
127+
128+
return { isLoading };
129+
};

src/components/views/dialogs/polls/usePollHistory.ts

+54-3
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17+
import { useEffect, useState } from "react";
1718
import { Poll, PollEvent } from "matrix-js-sdk/src/matrix";
1819
import { MatrixClient } from "matrix-js-sdk/src/client";
1920

2021
import { useEventEmitterState } from "../../../../hooks/useEventEmitter";
2122

2223
/**
2324
* Get poll instances from a room
25+
* Updates to include new polls
2426
* @param roomId - id of room to retrieve polls for
2527
* @param matrixClient - client
2628
* @returns {Map<string, Poll>} - Map of Poll instances
@@ -37,9 +39,58 @@ export const usePolls = (
3739
throw new Error("Cannot find room");
3840
}
3941

40-
const polls = useEventEmitterState(room, PollEvent.New, () => room.polls);
41-
42-
// @TODO(kerrya) watch polls for end events, trigger refiltering
42+
// copy room.polls map so changes can be detected
43+
const polls = useEventEmitterState(room, PollEvent.New, () => new Map<string, Poll>(room.polls));
4344

4445
return { polls };
4546
};
47+
48+
/**
49+
* Get all poll instances from a room
50+
* Fetch their responses (using cached poll responses)
51+
* Updates on:
52+
* - new polls added to room
53+
* - new responses added to polls
54+
* - changes to poll ended state
55+
* @param roomId - id of room to retrieve polls for
56+
* @param matrixClient - client
57+
* @returns {Map<string, Poll>} - Map of Poll instances
58+
*/
59+
export const usePollsWithRelations = (
60+
roomId: string,
61+
matrixClient: MatrixClient,
62+
): {
63+
polls: Map<string, Poll>;
64+
} => {
65+
const { polls } = usePolls(roomId, matrixClient);
66+
const [pollsWithRelations, setPollsWithRelations] = useState<Map<string, Poll>>(polls);
67+
68+
useEffect(() => {
69+
const onPollUpdate = async (): Promise<void> => {
70+
// trigger rerender by creating a new poll map
71+
setPollsWithRelations(new Map(polls));
72+
};
73+
if (polls) {
74+
for (const poll of polls.values()) {
75+
// listen to changes in responses and end state
76+
poll.on(PollEvent.End, onPollUpdate);
77+
poll.on(PollEvent.Responses, onPollUpdate);
78+
// trigger request to get all responses
79+
// if they are not already in cache
80+
poll.getResponses();
81+
}
82+
setPollsWithRelations(polls);
83+
}
84+
// unsubscribe
85+
return () => {
86+
if (polls) {
87+
for (const poll of polls.values()) {
88+
poll.off(PollEvent.End, onPollUpdate);
89+
poll.off(PollEvent.Responses, onPollUpdate);
90+
}
91+
}
92+
};
93+
}, [polls, setPollsWithRelations]);
94+
95+
return { polls: pollsWithRelations };
96+
};

src/i18n/strings/en_EN.json

+1
Original file line numberDiff line numberDiff line change
@@ -3131,6 +3131,7 @@
31313131
"Not a valid Security Key": "Not a valid Security Key",
31323132
"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.",
31333133
"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>",
3134+
"Loading polls": "Loading polls",
31343135
"There are no active polls in this room": "There are no active polls in this room",
31353136
"There are no past polls in this room": "There are no past polls in this room",
31363137
"Send custom account data event": "Send custom account data event",

0 commit comments

Comments
 (0)