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

Adds support for "Still watching" prompt to prevent users from idling when watching stream #8440

Merged
merged 8 commits into from
Sep 4, 2024
10 changes: 10 additions & 0 deletions src/Common/hooks/useConfig.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createContext, useContext } from "react";
import { StillWatchingConfig } from "../../Components/CameraFeed/StillWatching";

export const AppConfigContext = createContext<IConfig | null>(null);

Expand Down Expand Up @@ -83,8 +84,17 @@ export interface IConfig {
* Env to toggle peacetime and wartime shifting
*/
wartime_shifting: boolean;

/**
* The interval at which the JWT access token is refreshed in milliseconds.
*/
jwt_token_refresh_interval?: number;

/**
* Configurations related to the `StillWatching` component.
*/
still_watching?: StillWatchingConfig;

/*
* Minimum date for a possible consultation encounter.
*/
Expand Down
29 changes: 16 additions & 13 deletions src/Components/CameraFeed/CentralLiveMonitoring/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Fullscreen from "../../../CAREUI/misc/Fullscreen";
import useBreakpoints from "../../../Common/hooks/useBreakpoints";
import { useQueryParams } from "raviger";
import LiveMonitoringFilters from "./LiveMonitoringFilters";
import StillWatching from "../StillWatching";

export default function CentralLiveMonitoring(props: { facilityId: string }) {
const [isFullscreen, setFullscreen] = useState(false);
Expand Down Expand Up @@ -59,19 +60,21 @@ export default function CentralLiveMonitoring(props: { facilityId: string }) {
No Camera present in this location or facility.
</div>
) : (
<Fullscreen
fullscreenClassName="h-screen overflow-auto"
fullscreen={isFullscreen}
onExit={() => setFullscreen(false)}
>
<div className="mt-1 grid grid-cols-1 place-content-center gap-1 lg:grid-cols-2 3xl:grid-cols-3">
{data.results.map((asset) => (
<div className="text-clip" key={asset.id}>
<LocationFeedTile asset={asset} />
</div>
))}
</div>
</Fullscreen>
<StillWatching>
<Fullscreen
fullscreenClassName="h-screen overflow-auto"
fullscreen={isFullscreen}
onExit={() => setFullscreen(false)}
>
<div className="mt-1 grid grid-cols-1 place-content-center gap-1 lg:grid-cols-2 3xl:grid-cols-3">
{data.results.map((asset) => (
<div className="text-clip" key={asset.id}>
<LocationFeedTile asset={asset} />
</div>
))}
</div>
</Fullscreen>
</StillWatching>
)}
</Page>
);
Expand Down
94 changes: 94 additions & 0 deletions src/Components/CameraFeed/StillWatching.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { useEffect, useState } from "react";
import ConfirmDialog from "../Common/ConfirmDialog";
import ButtonV2 from "../Common/components/ButtonV2";
import CareIcon from "../../CAREUI/icons/CareIcon";
import { useTranslation } from "react-i18next";
import useConfig from "../../Common/hooks/useConfig";
import { useTimer } from "../../Utils/useTimer";

export type StillWatchingConfig = {
idleTimeout?: number;
promptDuration?: number;
};

const DEFAULT_CONFIG = {
idleTimeout: 3 * 60,
promptDuration: 30,
} satisfies StillWatchingConfig;

type State = "watching" | "prompted" | "timed-out";

const useStillWatching = (config: StillWatchingConfig) => {
const { idleTimeout, promptDuration } = { ...DEFAULT_CONFIG, ...config };

const [state, setState] = useState<State>("watching");
const [sequence, setSequence] = useState(1);

const timer = useTimer(true);

const remainingTime = Math.ceil(
(idleTimeout + promptDuration) * Math.min(sequence, 3) - timer.seconds,
);

useEffect(() => {
if (remainingTime < 0) {
setState("timed-out");
timer.stop();
return;
}
if (remainingTime < promptDuration) {
setState("prompted");
return;
}
}, [promptDuration, remainingTime]);

return {
state,
remainingTime,
reset: (hardReset?: boolean) => {
if (hardReset) {
setSequence((seq) => seq + 1);
}
timer.reset();
setState("watching");
timer.start();
},
};
};

export default function StillWatching(props: { children: React.ReactNode }) {
const { t } = useTranslation();
const { still_watching: config = {} } = useConfig();
const { state, remainingTime, reset } = useStillWatching(config);

return (
<div onClick={() => reset()}>
<ConfirmDialog
show={state === "prompted"}
title={t("are_you_still_watching")}
description={t("stream_stop_due_to_inativity")}
action={
<>
<CareIcon icon="l-play-circle" className="text-lg" />
{t("continue_watching")} ({remainingTime}s.)
</>
}
onConfirm={() => reset(true)}
onClose={() => reset(true)}
/>
{state === "timed-out" ? (
<div className="flex h-[50vh] w-full flex-col items-center justify-center gap-4 rounded-lg border-4 border-dashed border-secondary-400">
<span className="text-center text-xl font-bold text-secondary-700">
{t("stream_stopped_due_to_inativity")}
</span>
<ButtonV2 onClick={() => reset(true)}>
<CareIcon icon="l-play-circle" className="text-lg" />
{t("resume")}
</ButtonV2>
</div>
) : (
props.children
)}
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import useBreakpoints from "../../../Common/hooks/useBreakpoints";
import { Warn } from "../../../Utils/Notifications";
import { useTranslation } from "react-i18next";
import { GetStatusResponse } from "../../CameraFeed/routes";
import StillWatching from "../../CameraFeed/StillWatching";

export const ConsultationFeedTab = (props: ConsultationTabProps) => {
const { t } = useTranslation();
Expand Down Expand Up @@ -148,7 +149,7 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => {
const cannotSaveToPreset = !hasMoved || !preset?.id;

return (
<>
<StillWatching>
<ConfirmDialog
title="Update Preset"
description="Are you sure you want to update this preset to the current location?"
Expand Down Expand Up @@ -257,7 +258,7 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => {
</div>
</CameraFeed>
</div>
</>
</StillWatching>
);
};

Expand Down
9 changes: 7 additions & 2 deletions src/Locale/en/Asset.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,10 @@
"update_asset_service_record": "Update Asset Service Record",
"eg_details_on_functionality_service_etc": "Eg. Details on functionality, service, etc.",
"updating": "Updating",
"update": "Update"
}
"update": "Update",
"are_you_still_watching": "Are you still watching?",
"stream_stop_due_to_inativity": "The live feed will stop streaming due to inactivity",
"stream_stopped_due_to_inativity": "The live feed has stopped streaming due to inactivity",
"continue_watching": "Continue watching",
"resume": "Resume"
}
7 changes: 4 additions & 3 deletions src/Utils/useTimer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import { useEffect, useState } from "react";
* // To display the timer in your component:
* <div>{time}</div>
*/
export const useTimer = () => {
const [running, setRunning] = useState(false);
export const useTimer = (autoStart = false) => {
const [running, setRunning] = useState(autoStart);
const [time, setTime] = useState(0);

useEffect(() => {
Expand All @@ -39,7 +39,7 @@ export const useTimer = () => {
}, [running]);

return {
seconds: time,
seconds: time / 100,
time: (
<span>
{("0" + Math.floor((time / 6000) % 60)).slice(-2)}:
Expand All @@ -48,5 +48,6 @@ export const useTimer = () => {
),
start: () => setRunning(true),
stop: () => setRunning(false),
reset: () => setTime(0),
};
};
Loading