From 4da0f67533648713948877efe75ba68404ceffc8 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Sun, 29 Sep 2024 02:02:30 +0530 Subject: [PATCH 1/7] add support to toggle camera feed based on privacy setting --- src/Components/CameraFeed/CameraFeed.tsx | 55 ++++++++--- src/Components/CameraFeed/NoFeedAvailable.tsx | 53 +++++----- src/Components/CameraFeed/PrivacyToggle.tsx | 97 +++++++++++++++++++ .../ConsultationFeedTab.tsx | 33 +++++++ src/Components/Facility/models.tsx | 1 + src/Redux/api.tsx | 8 +- 6 files changed, 207 insertions(+), 40 deletions(-) create mode 100644 src/Components/CameraFeed/PrivacyToggle.tsx diff --git a/src/Components/CameraFeed/CameraFeed.tsx b/src/Components/CameraFeed/CameraFeed.tsx index a52214f5946..c398d387a1e 100644 --- a/src/Components/CameraFeed/CameraFeed.tsx +++ b/src/Components/CameraFeed/CameraFeed.tsx @@ -1,17 +1,18 @@ +import FeedAlert, { FeedAlertState, StreamStatus } from "./FeedAlert"; +import { classNames, isIOS } from "../../Utils/utils"; import { useCallback, useEffect, useRef, useState } from "react"; -import { AssetData } from "../Assets/AssetTypes"; import useOperateCamera, { PTZPayload } from "./useOperateCamera"; -import { getStreamUrl } from "./utils"; -import { classNames, isIOS } from "../../Utils/utils"; -import FeedAlert, { FeedAlertState, StreamStatus } from "./FeedAlert"; -import FeedNetworkSignal from "./FeedNetworkSignal"; -import NoFeedAvailable from "./NoFeedAvailable"; + +import { AssetData } from "../Assets/AssetTypes"; import FeedControls from "./FeedControls"; +import FeedNetworkSignal from "./FeedNetworkSignal"; import FeedWatermark from "./FeedWatermark"; -import useFullscreen from "../../Common/hooks/useFullscreen"; -import useBreakpoints from "../../Common/hooks/useBreakpoints"; import { GetPresetsResponse } from "./routes"; +import NoFeedAvailable from "./NoFeedAvailable"; import VideoPlayer from "./videoPlayer"; +import { getStreamUrl } from "./utils"; +import useBreakpoints from "../../Common/hooks/useBreakpoints"; +import useFullscreen from "../../Common/hooks/useFullscreen"; interface Props { children?: React.ReactNode; @@ -27,6 +28,7 @@ interface Props { shortcutsDisabled?: boolean; onMove?: () => void; operate: ReturnType["operate"]; + feedDisabled?: boolean | string | React.ReactNode; } export default function CameraFeed(props: Props) { @@ -56,7 +58,7 @@ export default function CameraFeed(props: Props) { if (props.preset) { move(props.preset); } - }, [props.preset]); + }, [props.preset]); // eslint-disable-line react-hooks/exhaustive-deps // Get camera presets (only if onCameraPresetsObtained is provided) useEffect(() => { @@ -68,7 +70,7 @@ export default function CameraFeed(props: Props) { } } getPresets(props.onCameraPresetsObtained); - }, [props.operate, props.onCameraPresetsObtained]); + }, [props.operate, props.onCameraPresetsObtained]); // eslint-disable-line react-hooks/exhaustive-deps const initializeStream = useCallback(async () => { if (!playerRef.current) return; @@ -87,12 +89,14 @@ export default function CameraFeed(props: Props) { setState("host_unreachable"); return props.onStreamError?.(); }); - }, []); + }, []); // eslint-disable-line react-hooks/exhaustive-deps // Start stream on mount useEffect(() => { - initializeStream(); - }, []); + if (!props.feedDisabled) { + initializeStream(); + } + }, [props.feedDisabled, initializeStream]); const resetStream = () => { setState("loading"); @@ -176,12 +180,12 @@ export default function CameraFeed(props: Props) { playerStatus !== "playing" ? "pointer-events-none opacity-10" : "opacity-100", - "transition-all duration-200 ease-in-out", + "transition-all duration-200 ease-in-out flex-1", )} > {props.children} -
+
{props.asset.name} @@ -246,11 +250,30 @@ export default function CameraFeed(props: Props) { } })()} + {props.feedDisabled && + (["string", "boolean"].includes(typeof props.feedDisabled) ? ( + + ) : ( + props.feedDisabled + ))} + {/* Video Player */} { setPlayedOn(new Date()); setState("playing"); diff --git a/src/Components/CameraFeed/NoFeedAvailable.tsx b/src/Components/CameraFeed/NoFeedAvailable.tsx index 1c05296fad1..639904eb695 100644 --- a/src/Components/CameraFeed/NoFeedAvailable.tsx +++ b/src/Components/CameraFeed/NoFeedAvailable.tsx @@ -1,15 +1,17 @@ import CareIcon, { IconName } from "../../CAREUI/icons/CareIcon"; -import { classNames } from "../../Utils/utils"; + import { AssetData } from "../Assets/AssetTypes"; import ButtonV2 from "../Common/components/ButtonV2"; +import { classNames } from "../../Utils/utils"; interface Props { className?: string; icon: IconName; message: string; streamUrl: string; - onResetClick: () => void; - asset: AssetData; + onResetClick?: () => void; + asset?: AssetData; + customActions?: React.ReactNode; } export default function NoFeedAvailable(props: Props) { @@ -32,26 +34,31 @@ export default function NoFeedAvailable(props: Props) { {redactedURL}
- - - Retry - - - - Configure - + {props.onResetClick && ( + + + Retry + + )} + {props.asset && ( + + + Configure + + )} + {props.customActions}
); diff --git a/src/Components/CameraFeed/PrivacyToggle.tsx b/src/Components/CameraFeed/PrivacyToggle.tsx new file mode 100644 index 00000000000..b01a6537c7d --- /dev/null +++ b/src/Components/CameraFeed/PrivacyToggle.tsx @@ -0,0 +1,97 @@ +import ButtonV2 from "../Common/components/ButtonV2"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import request from "../../Utils/request/request"; +import routes from "../../Redux/api"; +import useQuery from "../../Utils/request/useQuery"; +import { useState } from "react"; + +interface PrivacyToggleProps { + consultationBedId: string; + initalValue?: boolean; + onChange?: (value: boolean) => void; +} + +export default function PrivacyToggle({ + consultationBedId, + initalValue, + + onChange, +}: PrivacyToggleProps) { + const [isPrivacyEnabled, setIsPrivacyEnabled] = useState( + initalValue ?? false, + ); + + const updatePrivacyChange = (value: boolean) => { + setIsPrivacyEnabled(value); + onChange?.(value); + }; + + useQuery(routes.getConsultationBed, { + pathParams: { externalId: consultationBedId }, + onResponse(res) { + updatePrivacyChange(res.data?.is_privacy_enabled ?? false); + }, + }); + + return ( + + ); +} + +type TogglePrivacyButtonProps = { + value: boolean; + consultationBedId: string; + onChange: (value: boolean) => void; + iconOnly?: boolean; +}; + +export function TogglePrivacyButton({ + value: isPrivacyEnabled, + consultationBedId, + onChange: updatePrivacyChange, + iconOnly = false, +}: TogglePrivacyButtonProps) { + return ( + { + const { res, data } = await request( + routes.toggleConsultationBedPrivacy, + { + pathParams: { externalId: consultationBedId }, + }, + ); + + if (res?.ok && data) { + updatePrivacyChange(data.is_privacy_enabled); + } + }} + > + {!iconOnly && ( + + {isPrivacyEnabled ? "Disable Privacy" : "Enable Privacy"} + + )} + + + ); +} diff --git a/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx index 2e999d1956e..df4f02eb243 100644 --- a/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx +++ b/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx @@ -22,6 +22,10 @@ import { Warn } from "../../../Utils/Notifications"; import { useTranslation } from "react-i18next"; import { GetStatusResponse } from "../../CameraFeed/routes"; import StillWatching from "../../CameraFeed/StillWatching"; +import PrivacyToggle, { + TogglePrivacyButton, +} from "../../CameraFeed/PrivacyToggle"; +import NoFeedAvailable from "../../CameraFeed/NoFeedAvailable"; export const ConsultationFeedTab = (props: ConsultationTabProps) => { const { t } = useTranslation(); @@ -29,6 +33,9 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => { const facility = useSlug("facility"); const bed = props.consultationData.current_bed?.bed_object; const feedStateSessionKey = `encounterFeedState[${props.consultationId}]`; + const [isPrivacyEnabled, setIsPrivacyEnabled] = useState( + props.consultationData.current_bed?.is_privacy_enabled ?? false, + ); const [asset, setAsset] = useState(); const [preset, setPreset] = useState(); @@ -200,8 +207,34 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => { result: "success", }); }} + feedDisabled={ + isPrivacyEnabled && ( + + ) + } + /> + ) + } >
+ {props.consultationData.current_bed && ( + + )} {presets ? ( <> ; } diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index 0bedb12dca5..382b13e80df 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -559,13 +559,19 @@ const routes = { TRes: Type>(), }, getConsultationBed: { - path: "/api/v1/consultationbed/{external_id}/", + path: "/api/v1/consultationbed/{externalId}/", method: "GET", + TRes: Type(), }, updateConsultationBed: { path: "/api/v1/consultationbed/{external_id}/", method: "PUT", }, + toggleConsultationBedPrivacy: { + path: "/api/v1/consultationbed/{externalId}/toggle_privacy/", + method: "PATCH", + TRes: Type(), + }, // Download Api deleteFacility: { From 57ff5b8d329f849744ce3944ce90c1f3cfbe807e Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Sun, 29 Sep 2024 12:04:31 +0530 Subject: [PATCH 2/7] added functionality to lock camera while being used by user --- src/Components/CameraFeed/CameraFeed.tsx | 133 +++++++++++++++++- src/Components/CameraFeed/routes.ts | 19 ++- src/Components/CameraFeed/useOperateCamera.ts | 21 ++- 3 files changed, 167 insertions(+), 6 deletions(-) diff --git a/src/Components/CameraFeed/CameraFeed.tsx b/src/Components/CameraFeed/CameraFeed.tsx index c398d387a1e..fb2cb3b8965 100644 --- a/src/Components/CameraFeed/CameraFeed.tsx +++ b/src/Components/CameraFeed/CameraFeed.tsx @@ -1,18 +1,29 @@ +import * as Notification from "../../Utils/Notifications"; + import FeedAlert, { FeedAlertState, StreamStatus } from "./FeedAlert"; +import { + GetLockCameraResponse, + GetPresetsResponse, + GetRequestAccessResponse, +} from "./routes"; +import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react"; import { classNames, isIOS } from "../../Utils/utils"; import { useCallback, useEffect, useRef, useState } from "react"; import useOperateCamera, { PTZPayload } from "./useOperateCamera"; import { AssetData } from "../Assets/AssetTypes"; +import ButtonV2 from "../Common/components/ButtonV2"; import FeedControls from "./FeedControls"; import FeedNetworkSignal from "./FeedNetworkSignal"; import FeedWatermark from "./FeedWatermark"; -import { GetPresetsResponse } from "./routes"; import NoFeedAvailable from "./NoFeedAvailable"; +import { UserBareMinimum } from "../Users/models"; import VideoPlayer from "./videoPlayer"; import { getStreamUrl } from "./utils"; +import useAuthUser from "../../Common/hooks/useAuthUser"; import useBreakpoints from "../../Common/hooks/useBreakpoints"; import useFullscreen from "../../Common/hooks/useFullscreen"; +import { useMessageListener } from "../../Common/hooks/useMessageListener"; interface Props { children?: React.ReactNode; @@ -41,6 +52,60 @@ export default function CameraFeed(props: Props) { const [state, setState] = useState(); const [playedOn, setPlayedOn] = useState(); const [playerStatus, setPlayerStatus] = useState("stop"); + + const [cameraUser, setCameraUser] = useState(); + const user = useAuthUser(); + + const lockCamera = useCallback(async () => { + const { res, data, error } = await props.operate({ type: "lock_camera" }); + + const successData = data as GetLockCameraResponse; + const errorData = error as GetLockCameraResponse["result"]; + + if (res?.status === 200 && successData?.result) { + Notification.Success({ + msg: successData.result.message, + }); + setCameraUser(successData.result.camera_user); + } else if (res?.status === 409 && errorData) { + Notification.Warn({ + msg: errorData.message, + }); + setCameraUser(errorData.camera_user); + } else { + Notification.Error({ + msg: "An error occurred while locking the camera", + }); + } + }, []); + + const unlockCamera = useCallback(async () => { + await props.operate({ type: "unlock_camera" }); + }, []); + + useEffect(() => { + lockCamera(); + + return () => { + unlockCamera(); + }; + }, [lockCamera, unlockCamera]); + + useMessageListener((data) => { + if (data?.action === "CAMERA_ACCESS_REQUEST") { + Notification.Warn({ + msg: data?.message, + }); + } + + if (data?.action === "CAMERA_AVAILABILITY") { + Notification.Success({ + msg: data?.message, + }); + lockCamera(); + } + }); + // Move camera when selected preset has changed useEffect(() => { async function move(preset: PTZPayload) { @@ -204,6 +269,72 @@ export default function CameraFeed(props: Props) { />
)} + {cameraUser && ( + + + {cameraUser.username[0]} + + + + +
+

+ {[ + cameraUser.first_name, + cameraUser.last_name, + `(${cameraUser.username})`, + ] + .filter(Boolean) + .join(" ")} +

+

+ {cameraUser.user_type} +

+

+ {cameraUser.email} +

+
+
+ + {cameraUser.username !== user.username && ( + +
+

Need access to move camera?

+ { + const { res, data } = await props.operate({ + type: "request_access", + }); + + const successData = + data as GetRequestAccessResponse; + + if (res?.status === 200) { + Notification.Success({ + msg: successData.result.message, + }); + setCameraUser(successData.result.camera_user); + } else { + Notification.Error({ + msg: "An error occurred while requesting access", + }); + } + }} + > + Request Access + +
+
+ )} +
+
+ )}
diff --git a/src/Components/CameraFeed/routes.ts b/src/Components/CameraFeed/routes.ts index aecbdc655fa..2d4246b55f0 100644 --- a/src/Components/CameraFeed/routes.ts +++ b/src/Components/CameraFeed/routes.ts @@ -1,6 +1,8 @@ -import { Type } from "../../Redux/api"; import { OperationAction, PTZPayload } from "./useOperateCamera"; +import { Type } from "../../Redux/api"; +import { UserBareMinimum } from "../Users/models"; + export type GetStatusResponse = { result: { position: PTZPayload; @@ -23,12 +25,25 @@ export type GetPresetsResponse = { result: Record; }; +export type GetLockCameraResponse = { + result: { + message: string; + camera_user: UserBareMinimum; + }; +}; + +export type GetRequestAccessResponse = GetLockCameraResponse; + export const FeedRoutes = { operateAsset: { path: "/api/v1/asset/{id}/operate_assets/", method: "POST", TRes: Type< - GetStreamTokenResponse | GetStatusResponse | GetPresetsResponse + | GetStreamTokenResponse + | GetStatusResponse + | GetPresetsResponse + | GetLockCameraResponse + | GetRequestAccessResponse >(), TBody: Type<{ action: OperationAction }>(), }, diff --git a/src/Components/CameraFeed/useOperateCamera.ts b/src/Components/CameraFeed/useOperateCamera.ts index bfddbf5b887..588a771e0e0 100644 --- a/src/Components/CameraFeed/useOperateCamera.ts +++ b/src/Components/CameraFeed/useOperateCamera.ts @@ -1,6 +1,6 @@ -import { useState } from "react"; -import request from "../../Utils/request/request"; import { FeedRoutes } from "./routes"; +import request from "../../Utils/request/request"; +import { useState } from "react"; export interface PTZPayload { x: number; @@ -41,6 +41,18 @@ interface ResetFeedOperation { type: "reset"; } +interface LockCameraOperation { + type: "lock_camera"; +} + +interface UnlockCameraOperation { + type: "unlock_camera"; +} + +interface RequestAccessOperation { + type: "request_access"; +} + export type OperationAction = | GetStatusOperation | GetPresetsOperation @@ -48,7 +60,10 @@ export type OperationAction = | AbsoluteMoveOperation | RelativeMoveOperation | GetStreamToken - | ResetFeedOperation; + | ResetFeedOperation + | LockCameraOperation + | UnlockCameraOperation + | RequestAccessOperation; /** * This hook is used to control the PTZ of a camera asset and retrieve other related information. From c571c56aa7b8665a4dcebe29beba4459e4ecd7df Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Fri, 4 Oct 2024 09:22:13 +0530 Subject: [PATCH 3/7] don't render camera feed when the privacy is enabled --- src/Components/CameraFeed/CameraFeed.tsx | 38 ++++--------------- src/Components/CameraFeed/PrivacyToggle.tsx | 4 +- .../ConsultationFeedTab.tsx | 35 ++++++++--------- 3 files changed, 25 insertions(+), 52 deletions(-) diff --git a/src/Components/CameraFeed/CameraFeed.tsx b/src/Components/CameraFeed/CameraFeed.tsx index 0c422337b0c..3ca1a8b9cbd 100644 --- a/src/Components/CameraFeed/CameraFeed.tsx +++ b/src/Components/CameraFeed/CameraFeed.tsx @@ -40,7 +40,6 @@ interface Props { shortcutsDisabled?: boolean; onMove?: () => void; operate: ReturnType["operate"]; - feedDisabled?: boolean | string | React.ReactNode; } export default function CameraFeed(props: Props) { @@ -159,10 +158,8 @@ export default function CameraFeed(props: Props) { // Start stream on mount useEffect(() => { - if (!props.feedDisabled) { - initializeStream(); - } - }, [props.feedDisabled, initializeStream]); + initializeStream(); + }, []); // eslint-disable-line react-hooks/exhaustive-deps const resetStream = () => { setState("loading"); @@ -246,7 +243,7 @@ export default function CameraFeed(props: Props) { playerStatus !== "playing" ? "pointer-events-none opacity-10" : "opacity-100", - "transition-all duration-200 ease-in-out flex-1", + "flex-1 transition-all duration-200 ease-in-out", )} > {props.children} @@ -276,17 +273,17 @@ export default function CameraFeed(props: Props) { )} {cameraUser && ( - + {cameraUser.username[0]} -
+

{[ cameraUser.first_name, @@ -307,7 +304,7 @@ export default function CameraFeed(props: Props) { {cameraUser.username !== user.username && ( -

+

Need access to move camera?

- ) : ( - props.feedDisabled - ))} - {/* Video Player */} { setPlayedOn(new Date()); setState("playing"); diff --git a/src/Components/CameraFeed/PrivacyToggle.tsx b/src/Components/CameraFeed/PrivacyToggle.tsx index b01a6537c7d..ab77a0cca7f 100644 --- a/src/Components/CameraFeed/PrivacyToggle.tsx +++ b/src/Components/CameraFeed/PrivacyToggle.tsx @@ -59,7 +59,7 @@ export function TogglePrivacyButton({ return ( )} diff --git a/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx index df4f02eb243..95c6f2de626 100644 --- a/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx +++ b/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx @@ -25,7 +25,6 @@ import StillWatching from "../../CameraFeed/StillWatching"; import PrivacyToggle, { TogglePrivacyButton, } from "../../CameraFeed/PrivacyToggle"; -import NoFeedAvailable from "../../CameraFeed/NoFeedAvailable"; export const ConsultationFeedTab = (props: ConsultationTabProps) => { const { t } = useTranslation(); @@ -155,6 +154,21 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => { const cannotSaveToPreset = !hasMoved || !preset?.id; + if (isPrivacyEnabled && props.consultationData.current_bed) { + return ( +
+ + The camera feed is currently disabled due to privacy settings. + + +
+ ); + } + return ( { result: "success", }); }} - feedDisabled={ - isPrivacyEnabled && ( - - ) - } - /> - ) - } >
{props.consultationData.current_bed && ( From 1d623a4f2d4915ff4d7b506a6cddfd15e64a4536 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Fri, 4 Oct 2024 09:51:20 +0530 Subject: [PATCH 4/7] show error message while using relative move when camera controls are locked. --- src/Components/CameraFeed/CameraFeed.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Components/CameraFeed/CameraFeed.tsx b/src/Components/CameraFeed/CameraFeed.tsx index 3ca1a8b9cbd..107cdf51709 100644 --- a/src/Components/CameraFeed/CameraFeed.tsx +++ b/src/Components/CameraFeed/CameraFeed.tsx @@ -16,6 +16,7 @@ import ButtonV2 from "../Common/components/ButtonV2"; import FeedControls from "./FeedControls"; import FeedNetworkSignal from "./FeedNetworkSignal"; import FeedWatermark from "./FeedWatermark"; +import MonitorAssetPopover from "../Common/MonitorAssetPopover"; import NoFeedAvailable from "./NoFeedAvailable"; import { UserBareMinimum } from "../Users/models"; import VideoPlayer from "./videoPlayer"; @@ -24,7 +25,6 @@ import useAuthUser from "../../Common/hooks/useAuthUser"; import useBreakpoints from "../../Common/hooks/useBreakpoints"; import useFullscreen from "../../Common/hooks/useFullscreen"; import { useMessageListener } from "../../Common/hooks/useMessageListener"; -import MonitorAssetPopover from "../Common/MonitorAssetPopover"; interface Props { children?: React.ReactNode; @@ -199,14 +199,27 @@ export default function CameraFeed(props: Props) { onReset={resetStream} onMove={async (data) => { setState("moving"); - const { res } = await props.operate({ type: "relative_move", data }); + const { res, error } = await props.operate({ + type: "relative_move", + data, + }); props.onMove?.(); setTimeout(() => { setState((state) => (state === "moving" ? undefined : state)); }, 4000); + if (res?.status === 500) { setState("host_unreachable"); } + + if (res?.status === 409 && error) { + const errorData = error as GetLockCameraResponse["result"]; + + Notification.Warn({ + msg: errorData?.message, + }); + setCameraUser(errorData?.camera_user); + } }} /> ); From 24c4c40c1f72979effdb68ae0c8ab3070cf53665 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Wed, 9 Oct 2024 05:54:26 +0530 Subject: [PATCH 5/7] changed toggle_privacy to set_privacy --- src/Components/CameraFeed/PrivacyToggle.tsx | 3 +++ src/Redux/api.tsx | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Components/CameraFeed/PrivacyToggle.tsx b/src/Components/CameraFeed/PrivacyToggle.tsx index ab77a0cca7f..6a95a01bc2e 100644 --- a/src/Components/CameraFeed/PrivacyToggle.tsx +++ b/src/Components/CameraFeed/PrivacyToggle.tsx @@ -75,6 +75,9 @@ export function TogglePrivacyButton({ routes.toggleConsultationBedPrivacy, { pathParams: { externalId: consultationBedId }, + body: { + is_privacy_enabled: !isPrivacyEnabled, + }, }, ); diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index 79bb2c3cd81..c669845f52f 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -568,8 +568,9 @@ const routes = { method: "PUT", }, toggleConsultationBedPrivacy: { - path: "/api/v1/consultationbed/{externalId}/toggle_privacy/", + path: "/api/v1/consultationbed/{externalId}/set_privacy/", method: "PATCH", + TBody: Type<{ is_privacy_enabled: boolean }>(), TRes: Type(), }, From bd11dc6d1b1a05a808b9291cb532545173742d14 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Wed, 9 Oct 2024 12:54:55 +0530 Subject: [PATCH 6/7] fixed camera feed re-rendering twice --- .../Facility/ConsultationDetails/ConsultationFeedTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx index 95c6f2de626..8a3ab57309f 100644 --- a/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx +++ b/src/Components/Facility/ConsultationDetails/ConsultationFeedTab.tsx @@ -61,7 +61,7 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => { const { key, operate } = useOperateCamera(asset?.id ?? "", true); const { data, loading, refetch } = useQuery(routes.listAssetBeds, { - query: { limit: 100, facility, bed: bed?.id, asset: asset?.id }, + query: { limit: 100, facility, bed: bed?.id }, prefetch: !!bed, onResponse: ({ data }) => { if (!data) { From 77605d72070ac7c12596ae86951d0f5dfd3b8d0a Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Thu, 10 Oct 2024 07:21:01 +0530 Subject: [PATCH 7/7] added i18n --- src/Components/CameraFeed/CameraFeed.tsx | 16 +++--- src/Components/CameraFeed/NoFeedAvailable.tsx | 56 +++++++++---------- src/Components/CameraFeed/PrivacyToggle.tsx | 9 ++- .../ConsultationFeedTab.tsx | 20 +++---- src/Locale/en.json | 18 +++++- 5 files changed, 68 insertions(+), 51 deletions(-) diff --git a/src/Components/CameraFeed/CameraFeed.tsx b/src/Components/CameraFeed/CameraFeed.tsx index 107cdf51709..59d0011d393 100644 --- a/src/Components/CameraFeed/CameraFeed.tsx +++ b/src/Components/CameraFeed/CameraFeed.tsx @@ -25,6 +25,7 @@ import useAuthUser from "../../Common/hooks/useAuthUser"; import useBreakpoints from "../../Common/hooks/useBreakpoints"; import useFullscreen from "../../Common/hooks/useFullscreen"; import { useMessageListener } from "../../Common/hooks/useMessageListener"; +import { useTranslation } from "react-i18next"; interface Props { children?: React.ReactNode; @@ -43,6 +44,7 @@ interface Props { } export default function CameraFeed(props: Props) { + const { t } = useTranslation(); const playerRef = useRef(null); const playerWrapperRef = useRef(null); const [streamUrl, setStreamUrl] = useState(""); @@ -74,14 +76,14 @@ export default function CameraFeed(props: Props) { setCameraUser(errorData.camera_user); } else { Notification.Error({ - msg: "An error occurred while locking the camera", + msg: t("camera_locking_error"), }); } - }, []); + }, []); // eslint-disable-line react-hooks/exhaustive-deps const unlockCamera = useCallback(async () => { await props.operate({ type: "unlock_camera" }); - }, []); + }, []); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { lockCamera(); @@ -277,7 +279,7 @@ export default function CameraFeed(props: Props) { )} >
-

Need access to move camera?

+

{t("need_camera_access")}

- Request Access + {t("request_access")}
diff --git a/src/Components/CameraFeed/NoFeedAvailable.tsx b/src/Components/CameraFeed/NoFeedAvailable.tsx index 639904eb695..2625c330cad 100644 --- a/src/Components/CameraFeed/NoFeedAvailable.tsx +++ b/src/Components/CameraFeed/NoFeedAvailable.tsx @@ -1,20 +1,21 @@ +import { useTranslation } from "react-i18next"; import CareIcon, { IconName } from "../../CAREUI/icons/CareIcon"; - +import { classNames } from "../../Utils/utils"; import { AssetData } from "../Assets/AssetTypes"; import ButtonV2 from "../Common/components/ButtonV2"; -import { classNames } from "../../Utils/utils"; interface Props { className?: string; icon: IconName; message: string; streamUrl: string; - onResetClick?: () => void; - asset?: AssetData; - customActions?: React.ReactNode; + onResetClick: () => void; + asset: AssetData; } export default function NoFeedAvailable(props: Props) { + const { t } = useTranslation(); + const redactedURL = props.streamUrl // Replace all uuids in the URL with "ID_REDACTED" .replace(/[a-f\d]{8}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{12}/gi, "***") @@ -34,31 +35,26 @@ export default function NoFeedAvailable(props: Props) { {redactedURL}
- {props.onResetClick && ( - - - Retry - - )} - {props.asset && ( - - - Configure - - )} - {props.customActions} + + + {t("retry")} + + + + {t("configure")} +
); diff --git a/src/Components/CameraFeed/PrivacyToggle.tsx b/src/Components/CameraFeed/PrivacyToggle.tsx index 6a95a01bc2e..20cd5a0c1a7 100644 --- a/src/Components/CameraFeed/PrivacyToggle.tsx +++ b/src/Components/CameraFeed/PrivacyToggle.tsx @@ -4,6 +4,7 @@ import request from "../../Utils/request/request"; import routes from "../../Redux/api"; import useQuery from "../../Utils/request/useQuery"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; interface PrivacyToggleProps { consultationBedId: string; @@ -56,6 +57,8 @@ export function TogglePrivacyButton({ onChange: updatePrivacyChange, iconOnly = false, }: TogglePrivacyButtonProps) { + const { t } = useTranslation(); + return ( {!iconOnly && ( - {isPrivacyEnabled ? "Disable Privacy" : "Enable Privacy"} + {isPrivacyEnabled ? t("disable_privacy") : t("enable_privacy")} )} { useState(false); const [isUpdatingPreset, setIsUpdatingPreset] = useState(false); const [hasMoved, setHasMoved] = useState(false); - const divRef = useRef(); + const divRef = useRef(null); const suggestOptimalExperience = useBreakpoints({ default: true, sm: false }); @@ -56,7 +56,7 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => { ), }); } - }, []); + }, []); // eslint-disable-line react-hooks/exhaustive-deps const { key, operate } = useOperateCamera(asset?.id ?? "", true); @@ -130,7 +130,7 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => { if (divRef.current) { divRef.current.scrollIntoView({ behavior: "smooth" }); } - }, [!!bed, loading, !!asset, divRef.current]); + }, [!!bed, loading, !!asset, divRef.current]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { if (preset?.id) { @@ -149,7 +149,7 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => { } if (!bed || !asset) { - return No bed/asset linked allocated; + return {t("no_bed_or_asset_linked")}; } const cannotSaveToPreset = !hasMoved || !preset?.id; @@ -158,7 +158,7 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => { return (
- The camera feed is currently disabled due to privacy settings. + {t("camera_feed_disabled_due_to_privacy")} { return ( setShowPresetSaveConfirmation(false)} @@ -269,8 +269,8 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => { shadow={!cannotSaveToPreset} tooltip={ !cannotSaveToPreset - ? "Save current position to selected preset" - : "Change camera position to update preset" + ? t("save_current_position_to_preset") + : t("change_camera_position_and_update_preset") } tooltipClassName="translate-x-3 translate-y-8 text-xs" className="ml-1" @@ -281,7 +281,7 @@ export const ConsultationFeedTab = (props: ConsultationTabProps) => { )} ) : ( - loading presets... + {t("loading_preset") + "..."} )}
diff --git a/src/Locale/en.json b/src/Locale/en.json index 2b47a90bd46..49dfda28646 100644 --- a/src/Locale/en.json +++ b/src/Locale/en.json @@ -1022,5 +1022,21 @@ "date_of_result": "Covid confirmation date", "is_vaccinated": "Whether vaccinated", "consultation_not_filed": "You have not filed any consultation for this patient yet.", - "consultation_not_filed_description": "Please file a consultation for this patient to continue." + "consultation_not_filed_description": "Please file a consultation for this patient to continue.", + "camera_locking_error": "An error occurred while locking the camera", + "need_camera_access": "Need access to move camera?", + "request_camera_access_error": "An error occurred while requesting access", + "request_access": "Request Access", + "enable_privacy": "Enable Privacy", + "disable_privacy": "Disable Privacy", + "privacy_enabled_tooltip": "Privacy is enabled. Click to disable privacy", + "privacy_disabled_tooltip": "Privacy is disabled. Click to enable privacy", + "no_bed_or_asset_linked": "No bed/asset linked allocated", + "camera_feed_disabled_due_to_privacy": "The camera feed is currently disabled due to privacy settings.", + "update_preset": "Update Preset", + "update_preset_confirmation": "Are you sure you want to update this preset to the current location?", + "save_current_position_to_preset": "Save current position to selected preset", + "change_camera_position_and_update_preset": "Change camera position to update preset", + "loading_preset": "Loading Preset", + "retry": "Retry" }