Poll history: fetch more poll history (#10235)
* load more pages of polls * load more and no results messages * style no results message * remove debug * strict fixes * comments * i18n pluralisations * pluralisation the right way
This commit is contained in:
parent
f57495d3cd
commit
7c70dd9d16
7 changed files with 300 additions and 105 deletions
|
@ -57,9 +57,9 @@ export const PollHistoryDialog: React.FC<PollHistoryDialogProps> = ({
|
|||
onFinished,
|
||||
}) => {
|
||||
const { polls } = usePollsWithRelations(room.roomId, matrixClient);
|
||||
const { isLoading, loadMorePolls, oldestEventTimestamp } = useFetchPastPolls(room, matrixClient);
|
||||
const [filter, setFilter] = useState<PollHistoryFilter>("ACTIVE");
|
||||
const [focusedPollId, setFocusedPollId] = useState<string | null>(null);
|
||||
const { isLoading } = useFetchPastPolls(room, matrixClient);
|
||||
|
||||
const pollStartEvents = filterAndSortPolls(polls, filter);
|
||||
const isLoadingPollResponses = [...polls.values()].some((poll) => poll.isFetchingResponses);
|
||||
|
@ -78,12 +78,14 @@ export const PollHistoryDialog: React.FC<PollHistoryDialogProps> = ({
|
|||
<PollDetail poll={focusedPoll} permalinkCreator={permalinkCreator} requestModalClose={onFinished} />
|
||||
) : (
|
||||
<PollHistoryList
|
||||
onItemClick={setFocusedPollId}
|
||||
pollStartEvents={pollStartEvents}
|
||||
isLoading={isLoading || isLoadingPollResponses}
|
||||
oldestFetchedEventTimestamp={oldestEventTimestamp}
|
||||
polls={polls}
|
||||
filter={filter}
|
||||
onFilterChange={setFilter}
|
||||
onItemClick={setFocusedPollId}
|
||||
loadMorePolls={loadMorePolls}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -24,6 +24,7 @@ import InlineSpinner from "../../elements/InlineSpinner";
|
|||
import { PollHistoryFilter } from "./types";
|
||||
import { PollListItem } from "./PollListItem";
|
||||
import { PollListItemEnded } from "./PollListItemEnded";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
|
||||
const LoadingPolls: React.FC<{ noResultsYet?: boolean }> = ({ noResultsYet }) => (
|
||||
<div
|
||||
|
@ -36,20 +37,93 @@ const LoadingPolls: React.FC<{ noResultsYet?: boolean }> = ({ noResultsYet }) =>
|
|||
</div>
|
||||
);
|
||||
|
||||
const LoadMorePolls: React.FC<{ loadMorePolls?: () => void; isLoading?: boolean }> = ({ isLoading, loadMorePolls }) =>
|
||||
loadMorePolls ? (
|
||||
<AccessibleButton
|
||||
className="mx_PollHistoryList_loadMorePolls"
|
||||
kind="link_inline"
|
||||
onClick={() => loadMorePolls()}
|
||||
>
|
||||
{_t("Load more polls")}
|
||||
{isLoading && <InlineSpinner />}
|
||||
</AccessibleButton>
|
||||
) : null;
|
||||
|
||||
const ONE_DAY_MS = 60000 * 60 * 24;
|
||||
const getNoResultsMessage = (
|
||||
filter: PollHistoryFilter,
|
||||
oldestEventTimestamp?: number,
|
||||
loadMorePolls?: () => void,
|
||||
): string => {
|
||||
if (!loadMorePolls) {
|
||||
return filter === "ACTIVE"
|
||||
? _t("There are no active polls in this room")
|
||||
: _t("There are no past polls in this room");
|
||||
}
|
||||
|
||||
// we don't know how much history has been fetched
|
||||
if (!oldestEventTimestamp) {
|
||||
return filter === "ACTIVE"
|
||||
? _t("There are no active polls. Load more polls to view polls for previous months")
|
||||
: _t("There are no past polls. Load more polls to view polls for previous months");
|
||||
}
|
||||
|
||||
const fetchedHistoryDaysCount = Math.ceil((Date.now() - oldestEventTimestamp) / ONE_DAY_MS);
|
||||
return filter === "ACTIVE"
|
||||
? _t(
|
||||
"There are no active polls for the past %(count)s days. Load more polls to view polls for previous months",
|
||||
{ count: fetchedHistoryDaysCount },
|
||||
)
|
||||
: _t("There are no past polls for the past %(count)s days. Load more polls to view polls for previous months", {
|
||||
count: fetchedHistoryDaysCount,
|
||||
});
|
||||
};
|
||||
|
||||
const NoResults: React.FC<{
|
||||
filter: PollHistoryFilter;
|
||||
oldestFetchedEventTimestamp?: number;
|
||||
loadMorePolls?: () => void;
|
||||
isLoading?: boolean;
|
||||
}> = ({ filter, isLoading, oldestFetchedEventTimestamp, loadMorePolls }) => {
|
||||
// we can't page the timeline anymore
|
||||
// just use plain loader
|
||||
if (!loadMorePolls && isLoading) {
|
||||
return <LoadingPolls noResultsYet />;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="mx_PollHistoryList_noResults">
|
||||
{getNoResultsMessage(filter, oldestFetchedEventTimestamp, loadMorePolls)}
|
||||
|
||||
{!!loadMorePolls && <LoadMorePolls loadMorePolls={loadMorePolls} isLoading={isLoading} />}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
type PollHistoryListProps = {
|
||||
pollStartEvents: MatrixEvent[];
|
||||
polls: Map<string, Poll>;
|
||||
filter: PollHistoryFilter;
|
||||
isLoading?: boolean;
|
||||
/**
|
||||
* server ts of the oldest fetched poll
|
||||
* ignoring filter
|
||||
* used to render no results in last x days message
|
||||
* undefined when no polls are found
|
||||
*/
|
||||
oldestFetchedEventTimestamp?: number;
|
||||
onFilterChange: (filter: PollHistoryFilter) => void;
|
||||
onItemClick: (pollId: string) => void;
|
||||
loadMorePolls?: () => void;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
export const PollHistoryList: React.FC<PollHistoryListProps> = ({
|
||||
pollStartEvents,
|
||||
polls,
|
||||
filter,
|
||||
isLoading,
|
||||
oldestFetchedEventTimestamp,
|
||||
onFilterChange,
|
||||
loadMorePolls,
|
||||
onItemClick,
|
||||
}) => {
|
||||
return (
|
||||
|
@ -81,17 +155,18 @@ export const PollHistoryList: React.FC<PollHistoryListProps> = ({
|
|||
/>
|
||||
),
|
||||
)}
|
||||
{isLoading && <LoadingPolls />}
|
||||
{isLoading && !loadMorePolls && <LoadingPolls />}
|
||||
{!!loadMorePolls && <LoadMorePolls loadMorePolls={loadMorePolls} isLoading={isLoading} />}
|
||||
</ol>
|
||||
)}
|
||||
{!pollStartEvents.length && !isLoading && (
|
||||
<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>
|
||||
{!pollStartEvents.length && (
|
||||
<NoResults
|
||||
oldestFetchedEventTimestamp={oldestFetchedEventTimestamp}
|
||||
isLoading={isLoading}
|
||||
filter={filter}
|
||||
loadMorePolls={loadMorePolls}
|
||||
/>
|
||||
)}
|
||||
{!pollStartEvents.length && isLoading && <LoadingPolls noResultsYet />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -14,41 +14,82 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, 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 { Direction, 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) {
|
||||
const getOldestEventTimestamp = (timelineSet?: EventTimelineSet): number | undefined => {
|
||||
if (!timelineSet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const liveTimeline = timelineSet?.getLiveTimeline();
|
||||
const events = liveTimeline.getEvents();
|
||||
return events[0]?.getTs();
|
||||
};
|
||||
|
||||
/**
|
||||
* Page backwards in timeline history
|
||||
* @param timelineSet - timelineset to page
|
||||
* @param matrixClient - client
|
||||
* @param canPageBackward - whether the timeline has more pages
|
||||
* @param oldestEventTimestamp - server ts of the oldest encountered event
|
||||
*/
|
||||
const pagePollHistory = async (
|
||||
timelineSet: EventTimelineSet,
|
||||
matrixClient: MatrixClient,
|
||||
): Promise<{
|
||||
oldestEventTimestamp?: number;
|
||||
canPageBackward: boolean;
|
||||
}> => {
|
||||
if (!timelineSet) {
|
||||
return { canPageBackward: false };
|
||||
}
|
||||
|
||||
const liveTimeline = timelineSet.getLiveTimeline();
|
||||
|
||||
await matrixClient.paginateEventTimeline(liveTimeline, {
|
||||
backwards: true,
|
||||
});
|
||||
|
||||
return pagePolls(timelineSet, matrixClient, endOfHistoryPeriodTimestamp);
|
||||
return {
|
||||
oldestEventTimestamp: getOldestEventTimestamp(timelineSet),
|
||||
canPageBackward: !!liveTimeline.getPaginationToken(EventTimeline.BACKWARDS),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Page timeline backwards until either:
|
||||
* - event older than timestamp is encountered
|
||||
* - end of timeline is reached
|
||||
* @param timelineSet - timeline set to page
|
||||
* @param matrixClient - client
|
||||
* @param timestamp - epoch timestamp to page until
|
||||
* @param canPageBackward - whether the timeline has more pages
|
||||
* @param oldestEventTimestamp - server ts of the oldest encountered event
|
||||
*/
|
||||
const fetchHistoryUntilTimestamp = async (
|
||||
timelineSet: EventTimelineSet | undefined,
|
||||
matrixClient: MatrixClient,
|
||||
timestamp: number,
|
||||
canPageBackward: boolean,
|
||||
oldestEventTimestamp?: number,
|
||||
): Promise<void> => {
|
||||
if (!timelineSet || !canPageBackward || (oldestEventTimestamp && oldestEventTimestamp < timestamp)) {
|
||||
return;
|
||||
}
|
||||
const result = await pagePollHistory(timelineSet, matrixClient);
|
||||
|
||||
return fetchHistoryUntilTimestamp(
|
||||
timelineSet,
|
||||
matrixClient,
|
||||
timestamp,
|
||||
result.canPageBackward,
|
||||
result.oldestEventTimestamp,
|
||||
);
|
||||
};
|
||||
|
||||
const ONE_DAY_MS = 60000 * 60 * 24;
|
||||
|
@ -57,35 +98,73 @@ const ONE_DAY_MS = 60000 * 60 * 24;
|
|||
* @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
|
||||
* @returns isLoading - true while fetching
|
||||
* @returns oldestEventTimestamp - timestamp of oldest encountered poll, undefined when no polls found in timeline so far
|
||||
* @returns loadMorePolls - function to page timeline backwards, undefined when timeline cannot be paged backwards
|
||||
* @returns loadTimelineHistory - loads timeline history for the given history period
|
||||
*/
|
||||
const useTimelineHistory = (
|
||||
timelineSet: EventTimelineSet | null,
|
||||
timelineSet: EventTimelineSet | undefined,
|
||||
matrixClient: MatrixClient,
|
||||
historyPeriodDays: number,
|
||||
): { isLoading: boolean } => {
|
||||
): {
|
||||
isLoading: boolean;
|
||||
oldestEventTimestamp?: number;
|
||||
loadTimelineHistory: () => Promise<void>;
|
||||
loadMorePolls?: () => Promise<void>;
|
||||
} => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [oldestEventTimestamp, setOldestEventTimestamp] = useState<number | undefined>(undefined);
|
||||
const [canPageBackward, setCanPageBackward] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const loadTimelineHistory = useCallback(async () => {
|
||||
const endOfHistoryPeriodTimestamp = Date.now() - ONE_DAY_MS * historyPeriodDays;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const liveTimeline = timelineSet?.getLiveTimeline();
|
||||
const canPageBackward = !!liveTimeline?.getPaginationToken(Direction.Backward);
|
||||
const oldestEventTimestamp = getOldestEventTimestamp(timelineSet);
|
||||
|
||||
await fetchHistoryUntilTimestamp(
|
||||
timelineSet,
|
||||
matrixClient,
|
||||
endOfHistoryPeriodTimestamp,
|
||||
canPageBackward,
|
||||
oldestEventTimestamp,
|
||||
);
|
||||
|
||||
setCanPageBackward(!!timelineSet?.getLiveTimeline()?.getPaginationToken(EventTimeline.BACKWARDS));
|
||||
setOldestEventTimestamp(getOldestEventTimestamp(timelineSet));
|
||||
} catch (error) {
|
||||
logger.error("Failed to fetch room polls history", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [historyPeriodDays, timelineSet, matrixClient]);
|
||||
|
||||
const loadMorePolls = useCallback(async () => {
|
||||
if (!timelineSet) {
|
||||
return;
|
||||
}
|
||||
const endOfHistoryPeriodTimestamp = Date.now() - ONE_DAY_MS * historyPeriodDays;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await pagePollHistory(timelineSet, matrixClient);
|
||||
|
||||
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]);
|
||||
setCanPageBackward(result.canPageBackward);
|
||||
setOldestEventTimestamp(result.oldestEventTimestamp);
|
||||
} catch (error) {
|
||||
logger.error("Failed to fetch room polls history", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [timelineSet, matrixClient]);
|
||||
|
||||
return { isLoading };
|
||||
return {
|
||||
isLoading,
|
||||
oldestEventTimestamp,
|
||||
loadTimelineHistory,
|
||||
loadMorePolls: canPageBackward ? loadMorePolls : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const filterDefinition: IFilterDefinition = {
|
||||
|
@ -97,18 +176,24 @@ const filterDefinition: IFilterDefinition = {
|
|||
};
|
||||
|
||||
/**
|
||||
* Fetch poll start events in the last N days of room history
|
||||
* Fetches 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
|
||||
* @returns oldestEventTimestamp - timestamp of oldest encountered poll, undefined when no polls found in timeline so far
|
||||
* @returns loadMorePolls - function to page timeline backwards, undefined when timeline cannot be paged backwards
|
||||
*/
|
||||
export const useFetchPastPolls = (
|
||||
room: Room,
|
||||
matrixClient: MatrixClient,
|
||||
historyPeriodDays = 30,
|
||||
): { isLoading: boolean } => {
|
||||
const [timelineSet, setTimelineSet] = useState<EventTimelineSet | null>(null);
|
||||
): {
|
||||
isLoading: boolean;
|
||||
oldestEventTimestamp?: number;
|
||||
loadMorePolls?: () => Promise<void>;
|
||||
} => {
|
||||
const [timelineSet, setTimelineSet] = useState<EventTimelineSet | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const filter = new Filter(matrixClient.getSafeUserId());
|
||||
|
@ -123,7 +208,15 @@ export const useFetchPastPolls = (
|
|||
getFilteredTimelineSet();
|
||||
}, [room, matrixClient]);
|
||||
|
||||
const { isLoading } = useTimelineHistory(timelineSet, matrixClient, historyPeriodDays);
|
||||
const { isLoading, oldestEventTimestamp, loadMorePolls, loadTimelineHistory } = useTimelineHistory(
|
||||
timelineSet,
|
||||
matrixClient,
|
||||
historyPeriodDays,
|
||||
);
|
||||
|
||||
return { isLoading };
|
||||
useEffect(() => {
|
||||
loadTimelineHistory();
|
||||
}, [loadTimelineHistory]);
|
||||
|
||||
return { isLoading, oldestEventTimestamp, loadMorePolls };
|
||||
};
|
||||
|
|
|
@ -3143,8 +3143,15 @@
|
|||
"Active polls": "Active polls",
|
||||
"Past polls": "Past polls",
|
||||
"Loading polls": "Loading polls",
|
||||
"Load more polls": "Load more polls",
|
||||
"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 active polls. Load more polls to view polls for previous months": "There are no active polls. Load more polls to view polls for previous months",
|
||||
"There are no past polls. Load more polls to view polls for previous months": "There are no past polls. Load more polls to view polls for previous months",
|
||||
"There are no active polls for the past %(count)s days. Load more polls to view polls for previous months|other": "There are no active polls for the past %(count)s days. Load more polls to view polls for previous months",
|
||||
"There are no active polls for the past %(count)s days. Load more polls to view polls for previous months|one": "There are no active polls for the past day. Load more polls to view polls for previous months",
|
||||
"There are no past polls for the past %(count)s days. Load more polls to view polls for previous months|other": "There are no past polls for the past %(count)s days. Load more polls to view polls for previous months",
|
||||
"There are no past polls for the past %(count)s days. Load more polls to view polls for previous months|one": "There are no past polls for the past day. Load more polls to view polls for previous months",
|
||||
"View poll": "View poll",
|
||||
"Send custom account data event": "Send custom account data event",
|
||||
"Send custom room account data event": "Send custom room account data event",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue