Skip to content

Commit 5ee723c

Browse files
Update from samaritans.org b0d69ee5
1 parent 7f21fdd commit 5ee723c

File tree

4 files changed

+144
-8
lines changed

4 files changed

+144
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import React, { useCallback, useEffect, useRef } from 'react';
2+
import { useSelector } from 'react-redux';
3+
4+
import type { RootState } from '../../store';
5+
import config from '../../config';
6+
7+
/**
8+
* Poll a callback at a given interval
9+
*
10+
* The interval is the time between the end of the previous callback and the
11+
* start of the next callback. It does not use setInterval.
12+
*
13+
* */
14+
const usePoller = (callback: () => Promise<void>, interval: number) => {
15+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
16+
const isMounted = useRef(true);
17+
18+
useEffect(
19+
() => () => {
20+
isMounted.current = false;
21+
22+
// When the component unmounts, clear the timer
23+
// so that it doesn't trigger the callback after unmounting
24+
if (timerRef.current) {
25+
clearTimeout(timerRef.current);
26+
}
27+
},
28+
[],
29+
);
30+
31+
useEffect(() => {
32+
const poll = async () => {
33+
await callback();
34+
35+
// Only set the timer if the component is still mounted
36+
if (isMounted.current) {
37+
timerRef.current = setTimeout(poll, interval);
38+
}
39+
};
40+
41+
poll();
42+
43+
return () => {
44+
// If the callback or interval changes, clear the timer
45+
// so that the previous timer doesn't trigger a potentially outdated
46+
// callback
47+
if (timerRef.current) {
48+
clearTimeout(timerRef.current);
49+
}
50+
};
51+
}, [callback, interval]);
52+
};
53+
54+
type PollerProps = {
55+
callback: () => Promise<void>;
56+
interval: number;
57+
};
58+
59+
const Poller = ({ callback, interval }: PollerProps) => {
60+
usePoller(callback, interval);
61+
62+
return null;
63+
};
64+
65+
const HeartBeatPoller = () => {
66+
const {
67+
screen,
68+
chatEnded,
69+
sessionEnded,
70+
failedToReconnect,
71+
amazonConnectContactId,
72+
} = useSelector((state: RootState) => state.webchat);
73+
74+
const callback = useCallback(async () => {
75+
if (!amazonConnectContactId) {
76+
return;
77+
}
78+
79+
try {
80+
await fetch(config.heartbeatEndpoint, {
81+
method: 'POST',
82+
headers: {
83+
'Content-Type': 'application/json',
84+
'x-api-key': config.heartbeatApiKey,
85+
},
86+
body: JSON.stringify({
87+
contactId: amazonConnectContactId,
88+
}),
89+
});
90+
} catch (e) {
91+
console.error('Failed to ping health check endpoint', e);
92+
}
93+
}, [amazonConnectContactId]);
94+
95+
// Ping the health check endpoint every 10 seconds
96+
if (
97+
// Only if the user is in the waiting or chat screen
98+
['waiting', 'chat'].includes(screen) &&
99+
// Only if they have a contact ID (which means they're connected to a
100+
// chat)
101+
amazonConnectContactId &&
102+
// Only if the chat hasn't ended yet (e.g. they're on the chat screen
103+
// but the chat has ended)
104+
!chatEnded &&
105+
// Only if the session hasn't ended yet (e.g. they're reconnecting)
106+
!sessionEnded &&
107+
// Only if they haven't failed to reconnect
108+
!failedToReconnect
109+
) {
110+
return <Poller callback={callback} interval={10_000} />;
111+
}
112+
113+
return null;
114+
};
115+
116+
export default HeartBeatPoller;

static_src/typescript/config.ts

+8
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ type Props = {
2323
metricsApiKey: string;
2424
feedbackEndpoint: string;
2525
feedbackApiKey: string;
26+
heartbeatEndpoint: string;
27+
heartbeatApiKey: string;
2628
assets: Assets;
2729
banner?: Banner;
2830
};
@@ -41,6 +43,8 @@ class Configuration {
4143
public metricsApiKey: string,
4244
public feedbackEndpoint: string,
4345
public feedbackApiKey: string,
46+
public heartbeatEndpoint: string,
47+
public heartbeatApiKey: string,
4448
public assets: Assets,
4549
public banner?: Banner,
4650
) {}
@@ -62,6 +66,8 @@ class Configuration {
6266
obj.metricsApiKey,
6367
obj.feedbackEndpoint,
6468
obj.feedbackApiKey,
69+
obj.heartbeatEndpoint,
70+
obj.heartbeatApiKey,
6571
obj.assets,
6672
obj.banner,
6773
);
@@ -95,6 +101,8 @@ class Configuration {
95101
metricsApiKey: parsedData.metrics_api_key || '',
96102
feedbackEndpoint: parsedData.feedback_endpoint || '',
97103
feedbackApiKey: parsedData.feedback_api_key || '',
104+
heartbeatEndpoint: parsedData.heartbeat_endpoint || '',
105+
heartbeatApiKey: parsedData.heartbeat_api_key || '',
98106
assets: {
99107
appIcon: parsedData.assets?.app_icon || '',
100108
audioNotification: parsedData.assets?.audio_notification || '',

static_src/typescript/screens/LandingScreen.tsx

+17-6
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,24 @@ const LandingScreen = () => {
5252
)}
5353
<Room>
5454
<MainPanel>
55-
<h1>Chat with us online</h1>
55+
<h1>
56+
If you are under 19 you can get support from Childline.
57+
</h1>
5658
<p>
57-
Sometimes typing is easier than talking. Our trained
58-
volunteers will read your messages and respond in real
59-
time, helping you work through what&apos;s on your mind.
60-
They won&apos;t judge or tell you what to do, and you
61-
don&apos;t have to be suicidal to reach out.
59+
Childline is a free 24-hour counselling service for
60+
children and young people up to 19 years old. You can
61+
call, talk to a counsellor online, send an email or post
62+
on the message boards.
63+
</p>
64+
65+
<p>
66+
<a
67+
href="https://www.childline.org.uk/get-support"
68+
target="_blank"
69+
rel="noopener noreferrer"
70+
>
71+
Get support
72+
</a>
6273
</p>
6374

6475
<h2>Enter the waiting room when you are ready</h2>

static_src/typescript/webchat.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import 'react-toggle/style.css';
1010
import { LexWebUiProvider } from 'utils/lex-web-ui';
1111
import { Provider } from 'react-redux';
1212
import { Workbox } from 'workbox-window';
13+
import HeartBeatPoller from 'components/HeartBeatPoller';
14+
import { dataLayerPush } from 'utils/dataLayer';
1315
import App from './App';
1416
import SentryBoundary from './components/SentryBoundary';
1517
import './index.css';
@@ -22,13 +24,11 @@ import {
2224
setFailedToEstablish,
2325
setFailedToReconnect,
2426
setIsConfirmExitVisible,
25-
setScreen,
2627
setSucceededToReconnect,
2728
setVolunteerJoined,
2829
} from './slices/webchatSlice';
2930
import { refreshQueueStatus } from './slices/queueSlice';
3031
import { ping } from './slices/networkSlice';
31-
import { dataLayerPush } from 'utils/dataLayer';
3232

3333
/**
3434
* Render the Webchat app
@@ -48,6 +48,7 @@ const render = (Component: React.ComponentType) => {
4848
queueName={config.queueName}
4949
baseUrl={config.lexWebUiBaseUrl}
5050
>
51+
<HeartBeatPoller />
5152
<GlobalStyle />
5253
<SentryBoundary>
5354
<Component />

0 commit comments

Comments
 (0)