Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic paging intervals for elevator screens #2402

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions assets/src/components/v2/elevator/current_elevator_closed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import makePersistent, {
WrappedComponentProps,
} from "Components/v2/persistent_wrapper";
import PagingIndicators from "Components/v2/elevator/paging_indicators";
import useClientPaging from "Hooks/v2/use_client_paging";
import usePageAdvancer from "Hooks/v2/use_page_advancer";
import useTextResizer from "Hooks/v2/use_text_resizer";
import CurrentLocationMarker from "Images/svgr_bundled/current-location-marker.svg";
import CurrentLocationBackground from "Images/svgr_bundled/current-location-background.svg";
Expand Down Expand Up @@ -40,10 +40,15 @@ const CurrentElevatorClosed = ({
accessible_path_image_url: accessiblePathImageUrl,
accessible_path_image_here_coordinates: accessiblePathImageHereCoordinates,
onFinish,
lastUpdate,
}: Props) => {
const numPages = accessiblePathImageUrl ? 2 : 1;
const pageIndex = useClientPaging({ numPages, onFinish, lastUpdate });
const pageIndex = usePageAdvancer({
numPages,
cycleIntervalMs: 12000, // 12 seconds
advanceOnDataRefresh: false,
onFinish,
});

const { ref, size } = useTextResizer({
sizes: ["small", "medium", "large"],
maxHeight: 746,
Expand Down
11 changes: 8 additions & 3 deletions assets/src/components/v2/elevator/elevator_closures_list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
type StationWithClosures,
type Closure,
} from "Components/v2/elevator/types";
import useClientPaging from "Hooks/v2/use_client_paging";
import usePageAdvancer from "Hooks/v2/use_page_advancer";
import NormalService from "Images/svgr_bundled/normal-service.svg";
import AccessibilityAlert from "Images/svgr_bundled/accessibility-alert.svg";

Expand Down Expand Up @@ -107,7 +107,6 @@ interface OutsideClosureListProps extends WrappedComponentProps {
const OutsideClosureList = ({
stations,
stationId,
lastUpdate,
onFinish,
}: OutsideClosureListProps) => {
const ref = useRef<HTMLDivElement>(null);
Expand All @@ -132,7 +131,13 @@ const OutsideClosureList = ({
const [rowCountsPerPage, setRowCountsPerPage] = useState<number[]>([]);

const numPages = Object.keys(rowCountsPerPage).length;
const pageIndex = useClientPaging({ numPages, onFinish, lastUpdate });

const pageIndex = usePageAdvancer({
numPages,
cycleIntervalMs: 8000, // 8 seconds
advanceOnDataRefresh: false,
onFinish,
});

const numOffsetRows = Object.keys(rowCountsPerPage).reduce((acc, key) => {
if (parseInt(key) === pageIndex) {
Expand Down
5 changes: 3 additions & 2 deletions assets/src/components/v2/persistent_carousel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { ComponentType, ReactNode } from "react";
import makePersistent, { WrappedComponentProps } from "./persistent_wrapper";
import useClientPaging from "Hooks/v2/use_client_paging";
import usePageAdvancer from "Hooks/v2/use_page_advancer";

interface PageRendererProps<T> {
page: T;
Expand All @@ -19,8 +19,9 @@ const Carousel = <T,>({
onFinish,
lastUpdate,
}: Props<T>): ReactNode => {
const pageIndex = useClientPaging({
const pageIndex = usePageAdvancer({
numPages: pages.length,
advanceOnDataRefresh: true,
onFinish,
lastUpdate,
});
Expand Down
4 changes: 2 additions & 2 deletions assets/src/components/v2/persistent_wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { LastFetchContext } from "Components/v2/screen_container";

interface WrappedComponentProps {
onFinish: () => void;
lastUpdate: number | null;
lastUpdate?: number | null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was already defined as | null — it's a little confusing to have multiple ways to not provide a value for a prop. Perhaps we could just do lastUpdate?: number if the changes here make that the easier way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed this should not have both undefined and null. I think null makes this easier, but I had the undefined/optional leftover from my TODO. Will clean up

}

interface Props {
Expand Down Expand Up @@ -37,7 +37,7 @@ const PersistentWrapper: ComponentType<Props> = ({
{...visibleData}
onFinish={handleFinished}
key={renderKey}
lastUpdate={lastFetch}
lastUpdate={lastFetch} // make this optional - either determined by lastFetch Context or by other interval
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was something I originally talked about with Cora, but there isn't really an equivalent for lastUpdate to trigger the hook in wrappedComponent. Not sure if this wrapper makes as much sense anymore to use for the elevator screen components

/>
);
};
Expand Down
33 changes: 0 additions & 33 deletions assets/src/hooks/v2/use_client_paging.tsx

This file was deleted.

81 changes: 81 additions & 0 deletions assets/src/hooks/v2/use_page_advancer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { useState, useEffect, useRef, useCallback } from "react";

interface UsePageAdvancerProps {
numPages: number; // Total number of pages to cycle through
advanceOnDataRefresh: boolean; // Whether to advance when data is refreshed
cycleIntervalMs?: number; // In milliseconds, the interval for cycling pages (only used if advanceOnDataRefresh is false)
lastUpdate?: number | null;
onFinish: () => void; // Callback when cycling completes
}

/**
* This hook acts as a way for components with multiple pages to advance
* through their pages in one of two ways:
* 1. Interval-based advancement (advances page every __ ms)
* 2. Refresh-based advancement (advances page with every data refresh)
*/
function usePageAdvancer({
numPages,
cycleIntervalMs,
advanceOnDataRefresh = false,
lastUpdate,
onFinish,
}: UsePageAdvancerProps) {
const [pageIndex, setPageIndex] = useState(0);
const intervalRef = useRef<NodeJS.Timeout | null>(null);

// Use refs to keep stable references to numPages and onFinish
const numPagesRef = useRef(numPages);
const onFinishRef = useRef(onFinish);

useEffect(() => {
numPagesRef.current = numPages;
}, [numPages]);

useEffect(() => {
onFinishRef.current = onFinish;
}, [onFinish]);

// Callback that handles changing the state of pageIndex
const advancePage = useCallback(() => {
setPageIndex((prevIndex) => {
const nextIndex = (prevIndex + 1) % numPagesRef.current;
if (nextIndex === 0) onFinishRef.current(); // Call onFinish when cycling completes
return nextIndex;
});
}, []);

// Start the interval for time-based advancement
const startTimer = useCallback(() => {
if (intervalRef.current) clearInterval(intervalRef.current); // Clear any existing timer
if (cycleIntervalMs) {
intervalRef.current = setInterval(() => {
advancePage();
}, cycleIntervalMs);
}
}, [cycleIntervalMs, advancePage]);

// Cleanup the timer interval when the component unmounts or dependencies change
useEffect(() => {
if (!advanceOnDataRefresh && cycleIntervalMs) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hook has a lot of conditionals on whether we're using data-refresh paging or interval paging, and a few internal concepts that only matter when we're in one of the two "branches" — considering the code for each case on its own is pretty concise (the old useClientPaging is ~25 lines of code), might it be worth splitting into two separate hooks intead? Something like useRefreshPaging and useIntervalPaging. (I realize this is somewhat of a different approach from what I suggested at a high level previously, which was to combine these behaviors into a single hook with a flag for which one to use — sometimes it's not obvious how things will shake out until you build them 😅)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hook did end up being more confusing than I was expecting to get working for both cases within the same hook. It would likely be quite a bit simpler split out into two hooks, which I think makes up for the ease of use of having one hook for all paging.

I think going back to this approach makes sense!

startTimer();
return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
}
// Hooks must return void or a cleanup function,
// so we explicitly return undefined when no cleanup is necessary.
return undefined;
}, [advanceOnDataRefresh, cycleIntervalMs, startTimer]);

// Handle refresh-based advancement
useEffect(() => {
if (advanceOnDataRefresh && lastUpdate !== null) {
advancePage();
}
}, [lastUpdate]);

return pageIndex;
}

export default usePageAdvancer;
2 changes: 1 addition & 1 deletion lib/screens/v2/screen_data/parameters.ex
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ defmodule Screens.V2.ScreenData.Parameters do
},
elevator_v2: %Static{
candidate_generator: CandidateGenerator.Elevator,
refresh_rate: 8
refresh_rate: 30
},
gl_eink_v2: %Static{
audio_active_time: @all_times,
Expand Down
Loading