From 4c61baa765fdbb7e6afbed6fa5dd1869ab228dc2 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Wed, 4 Sep 2024 10:09:27 +0530 Subject: [PATCH] Adds support for "Still watching" prompt to prevent users from idling when watching stream (#8440) --- .example.env | 6 ++ care.config.ts | 9 ++ .../CentralLiveMonitoring/index.tsx | 29 ++++--- src/Components/CameraFeed/StillWatching.tsx | 82 +++++++++++++++++++ .../ConsultationFeedTab.tsx | 5 +- src/Locale/en/Asset.json | 9 +- src/Utils/useTimer.tsx | 7 +- src/vite-env.d.ts | 2 + 8 files changed, 129 insertions(+), 20 deletions(-) create mode 100644 src/Components/CameraFeed/StillWatching.tsx diff --git a/.example.env b/.example.env index 79332e6b9af..dd4a8865878 100644 --- a/.example.env +++ b/.example.env @@ -60,6 +60,12 @@ REACT_KASP_FULL_STRING=Karunya Arogya Suraksha Padhathi REACT_SAMPLE_FORMAT_ASSET_IMPORT=/asset-import-template.xlsx REACT_SAMPLE_FORMAT_EXTERNAL_RESULT_IMPORT=/External-Results-Template.csv +# Camera feed, still watching idle timeout (in seconds; default: 180) +REACT_STILL_WATCHING_IDLE_TIMEOUT= + +# Camera feed, still watching prompt duration (in seconds; default: 30) +REACT_STILL_WATCHING_PROMPT_DURATION= + # Feature flags REACT_ENABLE_HCX=true REACT_ENABLE_ABDM=true diff --git a/care.config.ts b/care.config.ts index 87604b50030..4341a03dd48 100644 --- a/care.config.ts +++ b/care.config.ts @@ -65,6 +65,15 @@ const careConfig = { wartimeShifting: env.REACT_WARTIME_SHIFTING === "true", + stillWatching: { + idleTimeout: env.REACT_STILL_WATCHING_IDLE_TIMEOUT + ? parseInt(env.REACT_STILL_WATCHING_IDLE_TIMEOUT) + : 3 * 60, + promptDuration: env.REACT_STILL_WATCHING_PROMPT_DURATION + ? parseInt(env.REACT_STILL_WATCHING_PROMPT_DURATION) + : 30, + }, + auth: { tokenRefreshInterval: env.REACT_JWT_TOKEN_REFRESH_INTERVAL ? parseInt(env.REACT_JWT_TOKEN_REFRESH_INTERVAL) diff --git a/src/Components/CameraFeed/CentralLiveMonitoring/index.tsx b/src/Components/CameraFeed/CentralLiveMonitoring/index.tsx index d2e7fd89494..3cca4c00ec4 100644 --- a/src/Components/CameraFeed/CentralLiveMonitoring/index.tsx +++ b/src/Components/CameraFeed/CentralLiveMonitoring/index.tsx @@ -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); @@ -59,19 +60,21 @@ export default function CentralLiveMonitoring(props: { facilityId: string }) { No Camera present in this location or facility. ) : ( - setFullscreen(false)} - > -
- {data.results.map((asset) => ( -
- -
- ))} -
-
+ + setFullscreen(false)} + > +
+ {data.results.map((asset) => ( +
+ +
+ ))} +
+
+
)} ); diff --git a/src/Components/CameraFeed/StillWatching.tsx b/src/Components/CameraFeed/StillWatching.tsx new file mode 100644 index 00000000000..0b5a8056b43 --- /dev/null +++ b/src/Components/CameraFeed/StillWatching.tsx @@ -0,0 +1,82 @@ +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 { useTimer } from "../../Utils/useTimer"; +import careConfig from "@careConfig"; + +type State = "watching" | "prompted" | "timed-out"; + +const useStillWatching = () => { + const { idleTimeout, promptDuration } = careConfig.stillWatching; + const [state, setState] = useState("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 { state, remainingTime, reset } = useStillWatching(); + + return ( +
reset()}> + + + {t("continue_watching")} ({remainingTime}s.) + + } + onConfirm={() => reset(true)} + onClose={() => reset(true)} + /> + {state === "timed-out" ? ( +
+ + {t("stream_stopped_due_to_inativity")} + + reset(true)}> + + {t("resume")} + +
+ ) : ( + props.children + )} +
+ ); +} diff --git a/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx index 08d2b314b82..2e999d1956e 100644 --- a/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx +++ b/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx @@ -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(); @@ -148,7 +149,7 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => { const cannotSaveToPreset = !hasMoved || !preset?.id; return ( - <> + { - + ); }; diff --git a/src/Locale/en/Asset.json b/src/Locale/en/Asset.json index f24549ee0b6..182bb25a4fe 100644 --- a/src/Locale/en/Asset.json +++ b/src/Locale/en/Asset.json @@ -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" +} \ No newline at end of file diff --git a/src/Utils/useTimer.tsx b/src/Utils/useTimer.tsx index f7157f65b86..6a8d8320c5e 100644 --- a/src/Utils/useTimer.tsx +++ b/src/Utils/useTimer.tsx @@ -22,8 +22,8 @@ import { useEffect, useState } from "react"; * // To display the timer in your component: *
{time}
*/ -export const useTimer = () => { - const [running, setRunning] = useState(false); +export const useTimer = (autoStart = false) => { + const [running, setRunning] = useState(autoStart); const [time, setTime] = useState(0); useEffect(() => { @@ -39,7 +39,7 @@ export const useTimer = () => { }, [running]); return { - seconds: time, + seconds: time / 100, time: ( {("0" + Math.floor((time / 6000) % 60)).slice(-2)}: @@ -48,5 +48,6 @@ export const useTimer = () => { ), start: () => setRunning(true), stop: () => setRunning(false), + reset: () => setTime(0), }; }; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index b290add72f9..7a50ebe49db 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -27,6 +27,8 @@ interface ImportMetaEnv { readonly REACT_SAMPLE_FORMAT_ASSET_IMPORT?: string; readonly REACT_SAMPLE_FORMAT_EXTERNAL_RESULT_IMPORT?: string; readonly REACT_WARTIME_SHIFTING?: string; + readonly REACT_STILL_WATCHING_IDLE_TIMEOUT?: string; + readonly REACT_STILL_WATCHING_PROMPT_DURATION?: string; readonly REACT_JWT_TOKEN_REFRESH_INTERVAL?: string; readonly REACT_MIN_ENCOUNTER_DATE?: string;