From 0d9de1959d5516eafa1e8401faa7c1a32a6bf793 Mon Sep 17 00:00:00 2001 From: D063222 Date: Mon, 13 Mar 2023 16:10:43 +0100 Subject: [PATCH 1/3] [greenhouse] track user inactivity --- apps/greenhouse/src/Shell.js | 23 +++--- .../src/components/IdleTimerProvider.js | 47 +++++++++++ apps/greenhouse/src/hooks/useIdleTimer.js | 78 +++++++++++++++++++ apps/greenhouse/src/hooks/useStore.js | 10 +++ 4 files changed, 148 insertions(+), 10 deletions(-) create mode 100644 apps/greenhouse/src/components/IdleTimerProvider.js create mode 100644 apps/greenhouse/src/hooks/useIdleTimer.js diff --git a/apps/greenhouse/src/Shell.js b/apps/greenhouse/src/Shell.js index 6465173da..2816ade1c 100644 --- a/apps/greenhouse/src/Shell.js +++ b/apps/greenhouse/src/Shell.js @@ -9,6 +9,8 @@ import styles from "./styles.scss" import StyleProvider from "juno-ui-components" import useCommunication from "./hooks/useCommunication" import { registerConsumer, currentState } from "url-state-provider" +import useIdleTimer from "./hooks/useIdleTimer" +import IdleTimerProvider from "./components/IdleTimerProvider" /* IMPORTANT: Replace this with your app's name */ const URL_STATE_KEY = "greenhouse" @@ -75,7 +77,6 @@ const Shell = (props = {}) => { // Create query client which it can be used from overall in the app const queryClient = new QueryClient() - useCommunication() // INIT @@ -129,15 +130,17 @@ const Shell = (props = {}) => { return ( - - {Object.keys(appsConfig).map((appName, i) => ( - = 0} - /> - ))} - + + + {Object.keys(appsConfig).map((appName, i) => ( + = 0} + /> + ))} + + ) diff --git a/apps/greenhouse/src/components/IdleTimerProvider.js b/apps/greenhouse/src/components/IdleTimerProvider.js new file mode 100644 index 000000000..7ddf08965 --- /dev/null +++ b/apps/greenhouse/src/components/IdleTimerProvider.js @@ -0,0 +1,47 @@ +import React, { useState, useEffect } from "react" +import useIdleTimer from "../hooks/useIdleTimer" +import { Modal } from "juno-ui-components" +import { broadcast, watch, onGet } from "communicator" + +const IdleTimerProvider = ({ timeout, onTimeout, onActive, children }) => { + const { isActive, inActiveSince } = useIdleTimer({ timeout }) + const [isUserInactive, setIsUserInactive] = useState(false) + + console.log("IdleTimerProvider: ", timeout, isActive, inActiveSince) + + useEffect(() => { + if (!isActive) { + setIsUserInactive(true) + } + }, [isActive]) + + useEffect(() => { + if (isUserInactive) { + broadcast("USER_INACTIVE", "greenhouse", { debug: true }) + } else { + broadcast("USER_ACTIVE", "greenhouse", { debug: true }) + } + }, [isUserInactive]) + + return ( + <> + {isUserInactive && ( + { + setIsUserInactive(false) + }} + cancelButtonLabel="Continue" + > +

+ It seems you aren't anymore active. The workers are paused until you + hit continue. +

+
+ )} + {children} + + ) +} + +export default IdleTimerProvider diff --git a/apps/greenhouse/src/hooks/useIdleTimer.js b/apps/greenhouse/src/hooks/useIdleTimer.js new file mode 100644 index 000000000..bcb67df70 --- /dev/null +++ b/apps/greenhouse/src/hooks/useIdleTimer.js @@ -0,0 +1,78 @@ +import { useEffect, useState, useMemo } from "react" + +const DEFAULT_TIMEOUT = 1800 // 30 min + +const useIdleTimer = ({ timeout, onTimeout, onActive }) => { + const [intervalChecker, setIntervalChecker] = useState(null) + const [counter, setCounter] = useState(0) + const [isActive, setIsActive] = useState(true) // default to true so it is not starting inactive + + // set a default timeout + timeout = useMemo(() => { + if (!timeout) return DEFAULT_TIMEOUT + return timeout + }, [timeout]) + + // on load bind events and reset on timeout changes + useEffect(() => { + trackActivity() + startInterval() + return () => cleanUp() + }, [timeout]) + + useEffect(() => { + if (isActive) { + if (onActive) onActive() + console.log("Active") + } else { + if (onTimeout) onTimeout + console.log("NOT active") + } + }, [isActive]) + + // track user activity by adding event listeners + const trackActivity = () => { + window.addEventListener("mousemove", activity) + window.addEventListener("scroll", activity) + window.addEventListener("keydown", activity) + window.addEventListener("keydown", focus) + } + + // cleanup all events + const cleanUp = () => { + clearInterval(intervalChecker) + window.removeEventListener("mousemove", activity) + window.removeEventListener("scroll", activity) + window.removeEventListener("keydown", activity) + window.removeEventListener("focus", activity) + } + + // set the expire time by reducing noise + const activity = () => { + setIsActive(true) + setCounter(0) + } + + // check in regular periods if we still active + const startInterval = () => { + setIntervalChecker( + setInterval(() => { + // use functional updates since interval will be created once + // but we need to read the updated counter + setCounter((prevCounter) => { + const newCount = prevCounter + 1 + if (newCount > timeout) { + setIsActive(false) + } + return newCount + }) + }, 1000) + ) + } + + console.log("test: ", timeout, isActive, counter) + + return { isActive: isActive, inActiveSince: counter } +} + +export default useIdleTimer diff --git a/apps/greenhouse/src/hooks/useStore.js b/apps/greenhouse/src/hooks/useStore.js index dc669edfa..0987c168a 100644 --- a/apps/greenhouse/src/hooks/useStore.js +++ b/apps/greenhouse/src/hooks/useStore.js @@ -5,6 +5,15 @@ const ACTIONS = { SIGN_OUT: "signOut", } +const createActivityDataSlice = (set, get) => ({ + activity: { + timeout: null, + setTimeout: (timeout) => { + set((state) => ({ activity: { ...state.activity, timeout } })) + }, + }, +}) + const createAuthDataSlice = (set, get) => ({ auth: { data: null, @@ -70,6 +79,7 @@ const createAppsDataSlice = (set, get) => ({ const useStore = create((set, get) => ({ ...createAuthDataSlice(set, get), ...createAppsDataSlice(set, get), + ...createActivityDataSlice(set, get), endpoint: "", urlStateKey: "", assetsHost: "", From 08ce3d9e1898ad000d7d4ddc6089e65036d1dee2 Mon Sep 17 00:00:00 2001 From: D063222 Date: Tue, 14 Mar 2023 11:54:27 +0100 Subject: [PATCH 2/3] [grennhouse] idle timer through the store --- apps/greenhouse/src/Shell.js | 3 +- .../src/components/IdleTimerProvider.js | 30 ++++++++----------- apps/greenhouse/src/hooks/useCommunication.js | 12 ++++++++ apps/greenhouse/src/hooks/useIdleTimer.js | 15 ++++++---- apps/greenhouse/src/hooks/useStore.js | 22 ++++++++++---- 5 files changed, 52 insertions(+), 30 deletions(-) diff --git a/apps/greenhouse/src/Shell.js b/apps/greenhouse/src/Shell.js index 2816ade1c..dee85a50c 100644 --- a/apps/greenhouse/src/Shell.js +++ b/apps/greenhouse/src/Shell.js @@ -78,6 +78,7 @@ const Shell = (props = {}) => { // Create query client which it can be used from overall in the app const queryClient = new QueryClient() useCommunication() + useIdleTimer({ timeout: 5 }) // INIT // on app initial load save Endpoint and URL_STATE_KEY so it can be @@ -130,7 +131,7 @@ const Shell = (props = {}) => { return ( - + {Object.keys(appsConfig).map((appName, i) => ( { - const { isActive, inActiveSince } = useIdleTimer({ timeout }) - const [isUserInactive, setIsUserInactive] = useState(false) - - console.log("IdleTimerProvider: ", timeout, isActive, inActiveSince) +const IdleTimerProvider = ({ children }) => { + const userActivity = useStore((state) => state.userActivity) + const [showModal, setShowModal] = useState(false) useEffect(() => { - if (!isActive) { - setIsUserInactive(true) + if (!userActivity?.isActive) { + setShowModal(true) } - }, [isActive]) + }, [userActivity?.isActive]) useEffect(() => { - if (isUserInactive) { - broadcast("USER_INACTIVE", "greenhouse", { debug: true }) - } else { - broadcast("USER_ACTIVE", "greenhouse", { debug: true }) - } - }, [isUserInactive]) + console.log("SHOW MODAL: ", showModal) + userActivity?.setInactiveModal(showModal) + }, [showModal]) return ( <> - {isUserInactive && ( + {showModal && ( { - setIsUserInactive(false) + setShowModal(false) }} cancelButtonLabel="Continue" > diff --git a/apps/greenhouse/src/hooks/useCommunication.js b/apps/greenhouse/src/hooks/useCommunication.js index a44d7fe13..ff8f687ec 100644 --- a/apps/greenhouse/src/hooks/useCommunication.js +++ b/apps/greenhouse/src/hooks/useCommunication.js @@ -4,6 +4,7 @@ import useStore from "./useStore" const useCommunication = () => { const auth = useStore((state) => state.auth) + const userActivity = useStore((state) => state.userActivity) useEffect(() => { if (!auth.appLoaded || auth?.isProcessing || auth?.error) return @@ -28,6 +29,17 @@ const useCommunication = () => { if (unwatchUpdate) unwatchUpdate() } }, [auth?.setData, auth?.setAppLoaded]) + + useEffect(() => { + // check against undefined since this is a boolean + if (userActivity?.inactiveModal === undefined) return + + broadcast( + "GREENHOUSE_USER_ACTIVITY", + { isUserInactive: userActivity?.inactiveModal }, + { debug: true } + ) + }, [userActivity?.inactiveModal]) } export default useCommunication diff --git a/apps/greenhouse/src/hooks/useIdleTimer.js b/apps/greenhouse/src/hooks/useIdleTimer.js index bcb67df70..fca6963eb 100644 --- a/apps/greenhouse/src/hooks/useIdleTimer.js +++ b/apps/greenhouse/src/hooks/useIdleTimer.js @@ -1,8 +1,11 @@ import { useEffect, useState, useMemo } from "react" +import useStore from "./useStore" const DEFAULT_TIMEOUT = 1800 // 30 min +// TODO: should we return the counter? const useIdleTimer = ({ timeout, onTimeout, onActive }) => { + const userActivity = useStore((state) => state.userActivity) const [intervalChecker, setIntervalChecker] = useState(null) const [counter, setCounter] = useState(0) const [isActive, setIsActive] = useState(true) // default to true so it is not starting inactive @@ -20,16 +23,20 @@ const useIdleTimer = ({ timeout, onTimeout, onActive }) => { return () => cleanUp() }, [timeout]) + // dispatch callbacks and save state into the store useEffect(() => { + // save into the store + userActivity?.setIsActive(isActive) + // send callbacks if (isActive) { if (onActive) onActive() - console.log("Active") } else { - if (onTimeout) onTimeout - console.log("NOT active") + if (onTimeout) onTimeout() } }, [isActive]) + console.log("IDLETIMMER HOOK: ", isActive, counter) + // track user activity by adding event listeners const trackActivity = () => { window.addEventListener("mousemove", activity) @@ -70,8 +77,6 @@ const useIdleTimer = ({ timeout, onTimeout, onActive }) => { ) } - console.log("test: ", timeout, isActive, counter) - return { isActive: isActive, inActiveSince: counter } } diff --git a/apps/greenhouse/src/hooks/useStore.js b/apps/greenhouse/src/hooks/useStore.js index 0987c168a..b9381e45a 100644 --- a/apps/greenhouse/src/hooks/useStore.js +++ b/apps/greenhouse/src/hooks/useStore.js @@ -5,11 +5,21 @@ const ACTIONS = { SIGN_OUT: "signOut", } -const createActivityDataSlice = (set, get) => ({ - activity: { - timeout: null, - setTimeout: (timeout) => { - set((state) => ({ activity: { ...state.activity, timeout } })) +const createUserActivitySlice = (set, get) => ({ + userActivity: { + // this state tracks the user activity + isActive: true, + setIsActive: (activity) => { + set((state) => ({ + userActivity: { ...state.userActivity, isActive: activity }, + })) + }, + //this state tracks + inactiveModal: false, + setInactiveModal: (activity) => { + set((state) => ({ + userActivity: { ...state.userActivity, inactiveModal: activity }, + })) }, }, }) @@ -79,7 +89,7 @@ const createAppsDataSlice = (set, get) => ({ const useStore = create((set, get) => ({ ...createAuthDataSlice(set, get), ...createAppsDataSlice(set, get), - ...createActivityDataSlice(set, get), + ...createUserActivitySlice(set, get), endpoint: "", urlStateKey: "", assetsHost: "", From 86872b73ea9c1e1a166f681a88e1d4da52bdbbbd Mon Sep 17 00:00:00 2001 From: D063222 Date: Tue, 14 Mar 2023 12:08:37 +0100 Subject: [PATCH 3/3] [greenhouse] rename store attr add mouse click event --- apps/greenhouse/src/components/IdleTimerProvider.js | 2 +- apps/greenhouse/src/hooks/useCommunication.js | 6 +++--- apps/greenhouse/src/hooks/useIdleTimer.js | 2 ++ apps/greenhouse/src/hooks/useStore.js | 6 +++--- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/greenhouse/src/components/IdleTimerProvider.js b/apps/greenhouse/src/components/IdleTimerProvider.js index b84821a20..73360a9c1 100644 --- a/apps/greenhouse/src/components/IdleTimerProvider.js +++ b/apps/greenhouse/src/components/IdleTimerProvider.js @@ -14,7 +14,7 @@ const IdleTimerProvider = ({ children }) => { useEffect(() => { console.log("SHOW MODAL: ", showModal) - userActivity?.setInactiveModal(showModal) + userActivity?.setShowInactiveModal(showModal) }, [showModal]) return ( diff --git a/apps/greenhouse/src/hooks/useCommunication.js b/apps/greenhouse/src/hooks/useCommunication.js index ff8f687ec..ef5e7218a 100644 --- a/apps/greenhouse/src/hooks/useCommunication.js +++ b/apps/greenhouse/src/hooks/useCommunication.js @@ -32,14 +32,14 @@ const useCommunication = () => { useEffect(() => { // check against undefined since this is a boolean - if (userActivity?.inactiveModal === undefined) return + if (userActivity?.showInactiveModal === undefined) return broadcast( "GREENHOUSE_USER_ACTIVITY", - { isUserInactive: userActivity?.inactiveModal }, + { isActive: !userActivity?.showInactiveModal }, { debug: true } ) - }, [userActivity?.inactiveModal]) + }, [userActivity?.showInactiveModal]) } export default useCommunication diff --git a/apps/greenhouse/src/hooks/useIdleTimer.js b/apps/greenhouse/src/hooks/useIdleTimer.js index fca6963eb..929efc5f2 100644 --- a/apps/greenhouse/src/hooks/useIdleTimer.js +++ b/apps/greenhouse/src/hooks/useIdleTimer.js @@ -40,6 +40,7 @@ const useIdleTimer = ({ timeout, onTimeout, onActive }) => { // track user activity by adding event listeners const trackActivity = () => { window.addEventListener("mousemove", activity) + window.addEventListener("click", activity) window.addEventListener("scroll", activity) window.addEventListener("keydown", activity) window.addEventListener("keydown", focus) @@ -49,6 +50,7 @@ const useIdleTimer = ({ timeout, onTimeout, onActive }) => { const cleanUp = () => { clearInterval(intervalChecker) window.removeEventListener("mousemove", activity) + window.removeEventListener("click", activity) window.removeEventListener("scroll", activity) window.removeEventListener("keydown", activity) window.removeEventListener("focus", activity) diff --git a/apps/greenhouse/src/hooks/useStore.js b/apps/greenhouse/src/hooks/useStore.js index b9381e45a..a6534fd8c 100644 --- a/apps/greenhouse/src/hooks/useStore.js +++ b/apps/greenhouse/src/hooks/useStore.js @@ -15,10 +15,10 @@ const createUserActivitySlice = (set, get) => ({ })) }, //this state tracks - inactiveModal: false, - setInactiveModal: (activity) => { + showInactiveModal: false, + setShowInactiveModal: (activity) => { set((state) => ({ - userActivity: { ...state.userActivity, inactiveModal: activity }, + userActivity: { ...state.userActivity, showInactiveModal: activity }, })) }, },