diff --git a/apps/greenhouse/src/Shell.js b/apps/greenhouse/src/Shell.js index aaaa9a714..24ba86970 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" @@ -77,8 +79,8 @@ 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 @@ -131,15 +133,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..73360a9c1 --- /dev/null +++ b/apps/greenhouse/src/components/IdleTimerProvider.js @@ -0,0 +1,41 @@ +import React, { useState, useEffect } from "react" +import { Modal } from "juno-ui-components" +import useStore from "../hooks/useStore" + +const IdleTimerProvider = ({ children }) => { + const userActivity = useStore((state) => state.userActivity) + const [showModal, setShowModal] = useState(false) + + useEffect(() => { + if (!userActivity?.isActive) { + setShowModal(true) + } + }, [userActivity?.isActive]) + + useEffect(() => { + console.log("SHOW MODAL: ", showModal) + userActivity?.setShowInactiveModal(showModal) + }, [showModal]) + + return ( + <> + {showModal && ( + { + setShowModal(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/useCommunication.js b/apps/greenhouse/src/hooks/useCommunication.js index a44d7fe13..ef5e7218a 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?.showInactiveModal === undefined) return + + broadcast( + "GREENHOUSE_USER_ACTIVITY", + { isActive: !userActivity?.showInactiveModal }, + { debug: true } + ) + }, [userActivity?.showInactiveModal]) } export default useCommunication diff --git a/apps/greenhouse/src/hooks/useIdleTimer.js b/apps/greenhouse/src/hooks/useIdleTimer.js new file mode 100644 index 000000000..929efc5f2 --- /dev/null +++ b/apps/greenhouse/src/hooks/useIdleTimer.js @@ -0,0 +1,85 @@ +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 + + // 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]) + + // dispatch callbacks and save state into the store + useEffect(() => { + // save into the store + userActivity?.setIsActive(isActive) + // send callbacks + if (isActive) { + if (onActive) onActive() + } else { + if (onTimeout) onTimeout() + } + }, [isActive]) + + console.log("IDLETIMMER HOOK: ", isActive, counter) + + // 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) + } + + // cleanup all events + const cleanUp = () => { + clearInterval(intervalChecker) + window.removeEventListener("mousemove", activity) + window.removeEventListener("click", 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) + ) + } + + 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..a6534fd8c 100644 --- a/apps/greenhouse/src/hooks/useStore.js +++ b/apps/greenhouse/src/hooks/useStore.js @@ -5,6 +5,25 @@ const ACTIONS = { SIGN_OUT: "signOut", } +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 + showInactiveModal: false, + setShowInactiveModal: (activity) => { + set((state) => ({ + userActivity: { ...state.userActivity, showInactiveModal: activity }, + })) + }, + }, +}) + const createAuthDataSlice = (set, get) => ({ auth: { data: null, @@ -70,6 +89,7 @@ const createAppsDataSlice = (set, get) => ({ const useStore = create((set, get) => ({ ...createAuthDataSlice(set, get), ...createAppsDataSlice(set, get), + ...createUserActivitySlice(set, get), endpoint: "", urlStateKey: "", assetsHost: "",
+ It seems you aren't anymore active. The workers are paused until you + hit continue. +