From b75b19dbc6e5194c08619c8c895bd604e81e6650 Mon Sep 17 00:00:00 2001 From: Jonni Madekivi Date: Tue, 15 Oct 2024 15:36:51 +0300 Subject: [PATCH 1/4] Add session keepalive mechanism to citizen message reply editor --- .../messages/MessageEditor.tsx | 1 + .../citizen-frontend/messages/ThreadView.tsx | 13 ++ .../components/messages/SingleThreadView.tsx | 1 + .../messages/ThreadView.tsx | 1 + .../lib-components/atoms/form/TextArea.tsx | 157 +++++++++--------- frontend/src/lib-components/i18n.tsx | 6 + .../messages/MessageReplyEditor.tsx | 18 +- .../molecules/modals/SessionExpiredModal.tsx | 40 +++++ .../src/lib-components/useKeepSessionAlive.ts | 35 ++++ .../utils/TestContextProvider.tsx | 6 + .../defaults/components/i18n/en.tsx | 7 + .../defaults/components/i18n/fi.tsx | 7 + .../defaults/components/i18n/sv.tsx | 7 + 13 files changed, 224 insertions(+), 75 deletions(-) create mode 100644 frontend/src/lib-components/molecules/modals/SessionExpiredModal.tsx create mode 100644 frontend/src/lib-components/useKeepSessionAlive.ts diff --git a/frontend/src/citizen-frontend/messages/MessageEditor.tsx b/frontend/src/citizen-frontend/messages/MessageEditor.tsx index 546e447b0aa..a7240653e82 100644 --- a/frontend/src/citizen-frontend/messages/MessageEditor.tsx +++ b/frontend/src/citizen-frontend/messages/MessageEditor.tsx @@ -386,6 +386,7 @@ export default React.memo(function MessageEditor({ })) } data-qa="input-content" + onKeyUp={() => console.log('MessageEditor keyup')} /> diff --git a/frontend/src/citizen-frontend/messages/ThreadView.tsx b/frontend/src/citizen-frontend/messages/ThreadView.tsx index 6ccd066dffc..2c864326ed8 100644 --- a/frontend/src/citizen-frontend/messages/ThreadView.tsx +++ b/frontend/src/citizen-frontend/messages/ThreadView.tsx @@ -365,6 +365,19 @@ export default React.memo( replyContent={replyContent} sendEnabled={sendEnabled} messageThreadSensitive={sensitive} + sessionKeepAlive={async () => { + const response = await fetch( + '/api/application/citizen/children', + { + method: 'GET', + credentials: 'include' + } + ) + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + return Promise.resolve() + }} /> ) : ( diff --git a/frontend/src/employee-frontend/components/messages/SingleThreadView.tsx b/frontend/src/employee-frontend/components/messages/SingleThreadView.tsx index 3a6d63986a7..b4be01c3900 100644 --- a/frontend/src/employee-frontend/components/messages/SingleThreadView.tsx +++ b/frontend/src/employee-frontend/components/messages/SingleThreadView.tsx @@ -297,6 +297,7 @@ export function SingleThreadView({ onToggleRecipient={onToggleRecipient} replyContent={replyContent} sendEnabled={sendEnabled} + sessionKeepAlive={() => Promise.resolve()} /> ) : ( diff --git a/frontend/src/employee-mobile-frontend/messages/ThreadView.tsx b/frontend/src/employee-mobile-frontend/messages/ThreadView.tsx index 04379707236..fd472467a29 100644 --- a/frontend/src/employee-mobile-frontend/messages/ThreadView.tsx +++ b/frontend/src/employee-mobile-frontend/messages/ThreadView.tsx @@ -180,6 +180,7 @@ const ReceivedThread = React.memo(function ReceivedThread({ onToggleRecipient={onToggleRecipient} replyContent={replyContent} sendEnabled={sendEnabled} + sessionKeepAlive={() => Promise.resolve()} /> ) : ( diff --git a/frontend/src/lib-components/atoms/form/TextArea.tsx b/frontend/src/lib-components/atoms/form/TextArea.tsx index 005e225b48e..6036f58f078 100644 --- a/frontend/src/lib-components/atoms/form/TextArea.tsx +++ b/frontend/src/lib-components/atoms/form/TextArea.tsx @@ -4,7 +4,7 @@ import autosize from 'autosize' import classNames from 'classnames' -import React, { useEffect, useMemo, useRef, useState } from 'react' +import React, { useEffect, useMemo, useRef, useState, forwardRef } from 'react' import styled from 'styled-components' import { BoundFormState } from 'lib-common/form/hooks' @@ -20,6 +20,7 @@ interface TextAreaInputProps extends BaseProps { onFocus?: (e: React.FocusEvent) => void onBlur?: (e: React.FocusEvent) => void onKeyPress?: (e: React.KeyboardEvent) => void + onKeyUp?: (e: React.KeyboardEvent) => void readonly?: boolean rows?: number maxLength?: number @@ -38,79 +39,87 @@ interface TextAreaInputProps extends BaseProps { wrapperClassName?: string } -const TextArea = React.memo(function TextArea({ - value, - onChange, - onFocus, - onBlur, - onKeyPress, - readonly, - rows, - maxLength, - type, - autoFocus, - preventAutoFocusScroll, - placeholder, - info, - id, - 'data-qa': dataQa, - className, - 'aria-describedby': ariaId, - hideErrorsBeforeTouched, - required -}: TextAreaInputProps) { - const [touched, setTouched] = useState(false) - - const hideError = - hideErrorsBeforeTouched && !touched && info?.status === 'warning' - const infoText = hideError ? undefined : info?.text - const infoStatus = hideError ? undefined : info?.status - - const handleChange = useMemo( - () => - onChange - ? (e: React.ChangeEvent) => - onChange(e.target.value) - : undefined, - [onChange] - ) - - return ( - <> - { - setTouched(true) - if (onBlur) onBlur(e) - }} - onKeyPress={onKeyPress} - placeholder={placeholder} - readOnly={readonly} - disabled={readonly} - maxLength={maxLength} - type={type} - autoFocus={autoFocus} - preventAutoFocusScroll={preventAutoFocusScroll} - className={classNames(className, infoStatus)} - data-qa={dataQa} - id={id} - aria-describedby={ariaId} - required={required ?? false} - rows={rows} - /> - {!!infoText && ( - - - {infoText} - - - - )} - - ) -}) +const TextArea = forwardRef( + function TextArea( + { + value, + onChange, + onFocus, + onBlur, + onKeyPress, + readonly, + rows, + maxLength, + type, + autoFocus, + preventAutoFocusScroll, + placeholder, + info, + id, + 'data-qa': dataQa, + className, + 'aria-describedby': ariaId, + hideErrorsBeforeTouched, + required, + onKeyUp + }, + ref + ) { + const [touched, setTouched] = useState(false) + + const hideError = + hideErrorsBeforeTouched && !touched && info?.status === 'warning' + const infoText = hideError ? undefined : info?.text + const infoStatus = hideError ? undefined : info?.status + + const handleChange = useMemo( + () => + onChange + ? (e: React.ChangeEvent) => + onChange(e.target.value) + : undefined, + [onChange] + ) + + return ( + <> + { + setTouched(true) + if (onBlur) onBlur(e) + }} + onKeyPress={onKeyPress} + onKeyUp={onKeyUp} + placeholder={placeholder} + readOnly={readonly} + disabled={readonly} + maxLength={maxLength} + type={type} + autoFocus={autoFocus} + preventAutoFocusScroll={preventAutoFocusScroll} + className={classNames(className, infoStatus)} + data-qa={dataQa} + id={id} + aria-describedby={ariaId} + required={required ?? false} + rows={rows} + /> + {!!infoText && ( + + + {infoText} + + + + )} + + ) + } +) export default TextArea diff --git a/frontend/src/lib-components/i18n.tsx b/frontend/src/lib-components/i18n.tsx index e0f00656a0d..e6ba612106f 100644 --- a/frontend/src/lib-components/i18n.tsx +++ b/frontend/src/lib-components/i18n.tsx @@ -95,6 +95,12 @@ export interface Translations { discard: string messagePlaceholderSensitiveThread: string | undefined } + sessionTimeout: { + sessionExpiredTitle: string + sessionExpiredMessage: string + goToLoginPage: string + cancel: string + } notifications: { close: string } diff --git a/frontend/src/lib-components/messages/MessageReplyEditor.tsx b/frontend/src/lib-components/messages/MessageReplyEditor.tsx index c7a682ab157..4d5af4b1e2a 100644 --- a/frontend/src/lib-components/messages/MessageReplyEditor.tsx +++ b/frontend/src/lib-components/messages/MessageReplyEditor.tsx @@ -9,6 +9,7 @@ import { AccountType } from 'lib-common/generated/api-types/messaging' import { type cancelMutation, MutationDescription } from 'lib-common/query' import { UUID } from 'lib-common/types' import { Button } from 'lib-components/atoms/buttons/Button' +import SessionExpiredModal from 'lib-components/molecules/modals/SessionExpiredModal' import { faTrash } from 'lib-icons' import { MutateButton } from '../atoms/buttons/MutateButton' @@ -16,6 +17,7 @@ import TextArea from '../atoms/form/TextArea' import { useTranslations } from '../i18n' import ButtonContainer from '../layout/ButtonContainer' import { Label } from '../typography' +import { useKeepSessionAlive } from '../useKeepSessionAlive' import { defaultMargins } from '../white-space' import { ToggleableRecipient } from './ToggleableRecipient' @@ -54,6 +56,7 @@ interface Props { replyContent: string sendEnabled: boolean messageThreadSensitive?: boolean + sessionKeepAlive: () => Promise } function MessageReplyEditor({ @@ -66,9 +69,16 @@ function MessageReplyEditor({ recipients, replyContent, sendEnabled, - messageThreadSensitive = false + messageThreadSensitive = false, + sessionKeepAlive }: Props) { const i18n = useTranslations() + const { + keepSessionAlive, + showSessionExpiredModal, + setShowSessionExpiredModal + } = useKeepSessionAlive(sessionKeepAlive) + const handleSuccess = useCallback( (response: R) => { onUpdateContent('') @@ -76,6 +86,7 @@ function MessageReplyEditor({ }, [onUpdateContent, onSuccess] ) + return ( <> @@ -100,6 +111,7 @@ function MessageReplyEditor({ } value={replyContent} onChange={(value) => onUpdateContent(value)} + onKeyUp={keepSessionAlive} data-qa="message-reply-content" autoFocus preventAutoFocusScroll={true} @@ -126,6 +138,10 @@ function MessageReplyEditor({ /> + setShowSessionExpiredModal(false)} + /> ) } diff --git a/frontend/src/lib-components/molecules/modals/SessionExpiredModal.tsx b/frontend/src/lib-components/molecules/modals/SessionExpiredModal.tsx new file mode 100644 index 00000000000..3ffbd79a0ba --- /dev/null +++ b/frontend/src/lib-components/molecules/modals/SessionExpiredModal.tsx @@ -0,0 +1,40 @@ +import React from 'react' + +import { Button } from 'lib-components/atoms/buttons/Button' + +import { useTranslations } from '../../i18n' +import ButtonContainer from '../../layout/ButtonContainer' + +import BaseModal from './BaseModal' + +interface Props { + isOpen: boolean + onClose: () => void +} + +const SessionExpiredModal: React.FC = ({ isOpen, onClose }) => { + const i18n = useTranslations() + + return ( + isOpen && ( + +

{i18n.sessionTimeout.sessionExpiredMessage}

+ +