From e589f0a5775c0bba4e1672bf41364fc996f8b3ff Mon Sep 17 00:00:00 2001 From: Dmytro Shcherbonos Date: Mon, 4 Nov 2024 22:32:03 +0200 Subject: [PATCH] Long-term closed session modal --- public/locales/en-US/translations.json | 7 +- .../LongTermClosedSessionModal.js | 48 ++++++++++++ src/modals/ModalsWrapper/ModalsWrapper.js | 2 + src/redux/actions/ws.js | 5 ++ src/redux/constants/modals.js | 2 + src/redux/constants/ws.js | 1 + src/redux/middleware/ws/on_message.js | 10 +-- src/redux/reducers/ws/api_client.js | 2 +- src/redux/sagas/ws/index.js | 2 + src/redux/sagas/ws/on_client_status_update.js | 73 +++++++++++++++++++ 10 files changed, 141 insertions(+), 11 deletions(-) create mode 100644 src/modals/LongTermClosedSessionModal/LongTermClosedSessionModal.js create mode 100644 src/redux/sagas/ws/on_client_status_update.js diff --git a/public/locales/en-US/translations.json b/public/locales/en-US/translations.json index ef56b99db..01db1f413 100644 --- a/public/locales/en-US/translations.json +++ b/public/locales/en-US/translations.json @@ -447,6 +447,10 @@ "reboot": "Reboot", "restart": "Restart" }, + "longTermClosedSessionModal": { + "title": "Warning - session closed for too long", + "description": "The connection to Bitfinex has been down for over 30 minutes, this might have brought instabilities to your trading operations. Please review carefully all your current trading activity on LIVE mode." + }, "noConnectionActionModal": { "title": "Connection issue", "description": "This action can't be run because the app is encountering connection issues.\nWe are trying to reconnect automatically.\nTry again once you see 'WS connected' present on the status bar at the bottom right-corner." @@ -920,7 +924,8 @@ "launchNoSave": "Launch without saving", "updateAndRestart": "Update and Restart", "saveAndContinue": "Save and Continue", - "starred": "Starred" + "starred": "Starred", + "continueToApp": "Continue to the App" }, "crashHandler": { "text1": "An error occurred that caused the Bitfinex Honey UI to halt. Please, restart the application to proceed working with it", diff --git a/src/modals/LongTermClosedSessionModal/LongTermClosedSessionModal.js b/src/modals/LongTermClosedSessionModal/LongTermClosedSessionModal.js new file mode 100644 index 000000000..fa9499876 --- /dev/null +++ b/src/modals/LongTermClosedSessionModal/LongTermClosedSessionModal.js @@ -0,0 +1,48 @@ +import React from 'react' +import { useDispatch, useSelector } from 'react-redux' + +import { Trans, useTranslation } from 'react-i18next' +import { getUIModalStateForKey } from '../../redux/selectors/ui' +import { UI_MODAL_KEYS } from '../../redux/constants/modals' +import UIActions from '../../redux/actions/ui' +import Modal from '../../ui/Modal' + +const LongTermClosedSessionModal = () => { + const isVisible = useSelector(state => getUIModalStateForKey( + state, + UI_MODAL_KEYS.LONG_TERM_CLOSED_SESSION_MODAL, + )) + + const dispatch = useDispatch() + const { t } = useTranslation() + + const onClose = () => dispatch(UIActions.changeUIModalState( + UI_MODAL_KEYS.LONG_TERM_CLOSED_SESSION_MODAL, + false, + )) + + return ( + + , + }} + /> + + + {t('ui.continueToApp')} + + + + ) +} + +export default LongTermClosedSessionModal diff --git a/src/modals/ModalsWrapper/ModalsWrapper.js b/src/modals/ModalsWrapper/ModalsWrapper.js index 0c36e9003..51c58c9db 100644 --- a/src/modals/ModalsWrapper/ModalsWrapper.js +++ b/src/modals/ModalsWrapper/ModalsWrapper.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types' import AppSettingsModal from '../AppSettingsModal' import CloseSessionModal from '../CloseSessionModal' import DMSRemovalDisclaimerModal from '../DMSRemovalDisclaimerModal' +import LongTermClosedSessionModal from '../LongTermClosedSessionModal/LongTermClosedSessionModal' const BadConnectionModal = lazy(() => import('../BadConnectionModal')) const NoConnectionActionModal = lazy(() => import('../NoConnectionActionModal')) @@ -29,6 +30,7 @@ const ModalsWrapper = ({ isElectronApp }) => { )} + diff --git a/src/redux/actions/ws.js b/src/redux/actions/ws.js index e7d98e7e2..b17abf82c 100644 --- a/src/redux/actions/ws.js +++ b/src/redux/actions/ws.js @@ -148,6 +148,11 @@ export default { payload: { balance }, }), + setAPIClientStatus: ({ status, mode }) => ({ + type: t.DATA_SET_API_CLIENT_STATUS, + payload: { status, mode }, + }), + recvOrders: ({ orders }) => ({ type: t.DATA_ORDERS, payload: { orders }, diff --git a/src/redux/constants/modals.js b/src/redux/constants/modals.js index e83990df3..6a2a95927 100644 --- a/src/redux/constants/modals.js +++ b/src/redux/constants/modals.js @@ -14,4 +14,6 @@ export const UI_MODAL_KEYS = { RESET_PAPER_API_KEY_MODAL: 'ResetPaperApiKeyModal', HELP_US_IMPROVE_HONEY_MODAL: 'HelpUsImproveHoneyModal', DMS_REMOVAL_DISCLAIMER: 'DMSRemovalDisclaimerModal', + LONG_TERM_CLOSED_SESSION_MODAL: 'longTermClosedSessionModal', + LONG_TERM_CLOSED_SESSION_MODAL_ALREADY_SHOWN: 'longTermClosedSessionModalAlreadyShown', } diff --git a/src/redux/constants/ws.js b/src/redux/constants/ws.js index f68bafd1d..727a1ea0a 100644 --- a/src/redux/constants/ws.js +++ b/src/redux/constants/ws.js @@ -32,6 +32,7 @@ export default { DATA_API_CREDENTIALS_CONFIGURED: 'WS_DATA_API_CREDENTIALS_CONFIGURED', UPDATE_API_CREDENTIALS_CONFIGURED: 'WS_UPDATE_API_CREDENTIALS_CONFIGURED', DATA_CLIENT_STATUS_UPDATE: 'WS_DATA_CLIENT_STATUS_UPDATE', + DATA_SET_API_CLIENT_STATUS: 'WS_DATA_SET_API_CLIENT_STATUS', DATA_POSITIONS: 'WS_DATA_POSITIONS', DATA_POSITION: 'WS_DATA_POSITION', DATA_POSITION_CLOSE: 'WS_DATA_POSITION_CLOSE', diff --git a/src/redux/middleware/ws/on_message.js b/src/redux/middleware/ws/on_message.js index 9b214553a..0a6bd0c68 100644 --- a/src/redux/middleware/ws/on_message.js +++ b/src/redux/middleware/ws/on_message.js @@ -17,7 +17,7 @@ import tokenStore from '../../../util/token_store' import { isElectronApp, HONEY_AUTH_URL } from '../../config' import { UI_MODAL_KEYS } from '../../constants/modals' import { UI_KEYS } from '../../constants/ui_keys' -import { WS_CONNECTION } from '../../constants/ws' + import { SETTINGS_KEYS, getCurrentStrategy } from '../../selectors/ui' import { LOG_LEVELS } from '../../../constants/logging' @@ -323,14 +323,6 @@ export default (alias, store) => (e = {}) => { const [, , mode, status] = payload store.dispatch(WSActions.recvClientStatusUpdate({ status, mode })) - if (status === WS_CONNECTION.CLOSED) { - store.dispatch(UIActions.setUIValue(UI_KEYS.isBadInternetConnection, true)) - } - - if (status === WS_CONNECTION.OPENED) { - store.dispatch(UIActions.setUIValue(UI_KEYS.isBadInternetConnection, false)) - } - break } diff --git a/src/redux/reducers/ws/api_client.js b/src/redux/reducers/ws/api_client.js index e1f9da12a..25bbb8154 100644 --- a/src/redux/reducers/ws/api_client.js +++ b/src/redux/reducers/ws/api_client.js @@ -12,7 +12,7 @@ export default function (state = getInitialState(), action = {}) { const { type, payload = {} } = action switch (type) { - case t.DATA_CLIENT_STATUS_UPDATE: { + case t.DATA_SET_API_CLIENT_STATUS: { const { status, mode } = payload return { diff --git a/src/redux/sagas/ws/index.js b/src/redux/sagas/ws/index.js index bcaad39b4..818c36f1b 100644 --- a/src/redux/sagas/ws/index.js +++ b/src/redux/sagas/ws/index.js @@ -15,6 +15,7 @@ import onResetData from './on_reset_data' import onExportStrategiesBeforeReset from './on_export_strategies_before_reset_data.js' import onAlgoOrderStopped from './on_ao_stopped' import cancelAlgoOrder from './cancel_algo_order' +import onClientStatusUpdate from './on_client_status_update' export default function* () { yield takeEvery(t.BUFF_SEND, messageQueueWorker) @@ -29,6 +30,7 @@ export default function* () { yield takeEvery(t.DATA_ALGO_ORDER_STOPPED, onAlgoOrderStopped) yield takeLatest(t.EXPORT_STRATEGIES_ON_RESET, onExportStrategiesBeforeReset) yield takeEvery(t.CANCEL_ALGO_ORDER, cancelAlgoOrder) + yield takeEvery(t.DATA_CLIENT_STATUS_UPDATE, onClientStatusUpdate) yield fork(connectionWorker) yield fork(pingRebootAppWorker) diff --git a/src/redux/sagas/ws/on_client_status_update.js b/src/redux/sagas/ws/on_client_status_update.js new file mode 100644 index 000000000..a27124aa0 --- /dev/null +++ b/src/redux/sagas/ws/on_client_status_update.js @@ -0,0 +1,73 @@ +import _isEmpty from 'lodash/isEmpty' +import { put, select } from 'redux-saga/effects' + +import { WS_CONNECTION } from '../../constants/ws' +import { UI_KEYS } from '../../constants/ui_keys' +import UIActions from '../../actions/ui' +import WSActions from '../../actions/ws' +import { UI_MODAL_KEYS } from '../../constants/modals' +import { + getActiveStrategies, + getFilteredLocalAlgoOrders, + getSocket, +} from '../../selectors/ws' +import { getUIModalStateForKey } from '../../selectors/ui' +import { getAPIClientState } from '../../selectors/ws/api_client_state' + +const LONG_TERM_CLOSED_SESSION_MODAL_DELAY = 30 * 60 * 1000 // 30m + +function* longTermModalStateSwitch(state) { + yield put( + UIActions.changeUIModalState( + UI_MODAL_KEYS.LONG_TERM_CLOSED_SESSION_MODAL, + state, + ), + ) + yield put( + UIActions.changeUIModalState( + UI_MODAL_KEYS.LONG_TERM_CLOSED_SESSION_MODAL_ALREADY_SHOWN, + state, + ), + ) +} + +export default function* onClientStatusUpdate({ payload }) { + const { status } = payload + + const lastStatus = yield select(getAPIClientState) + const activeStrategies = yield select(getActiveStrategies) + const algoOrders = yield select(getFilteredLocalAlgoOrders) + + const isStatusChanged = status !== lastStatus + + const isLongTermClosedSessionModalAlreadyShown = yield select((state) => getUIModalStateForKey( + state, + UI_MODAL_KEYS.LONG_TERM_CLOSED_SESSION_MODAL_ALREADY_SHOWN, + ), + ) + + if (status === WS_CONNECTION.CLOSED) { + if (isStatusChanged) { + yield put(UIActions.setUIValue(UI_KEYS.isBadInternetConnection, true)) + } + + const socket = yield select(getSocket) + if ( + (!_isEmpty(activeStrategies) || !_isEmpty(algoOrders)) + && !isLongTermClosedSessionModalAlreadyShown + && socket?.lastActivity + && Date.now() - socket.lastActivity >= LONG_TERM_CLOSED_SESSION_MODAL_DELAY + ) { + yield longTermModalStateSwitch(true) + } + } + + if (status === WS_CONNECTION.OPENED) { + if (isStatusChanged) { + yield put(UIActions.setUIValue(UI_KEYS.isBadInternetConnection, false)) + } + yield longTermModalStateSwitch(false) + } + + yield put(WSActions.setAPIClientStatus(payload)) +}