Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #2828 Clear idle sample #3376

Merged
merged 5 commits into from
Aug 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

- Fixes [#2675](https://github.com/microsoft/BotFramework-WebChat/issues/2675). Added alt text to images in suggested actions, by [@compulim](https://github.com/compulim) in PR [#3375](https://github.com/microsoft/BotFramework-WebChat/pull/3375)

### Samples

- Fixes [#2828](https://github.com/microsoft.com/BotFramework-WebChat/issues/2828). Updated [`04.api/h.clear-after-idle` sample](https://microsoft.github.io/BotFramework-WebChat/04.api/h.clear-after-idle/), by [@compulim](https://github.com/compulim), in PR [#3376](https://github.com/microsoft/BotFramework-WebChat/pull/3376)

## [4.9.2] - 2020-07-14

### Added
Expand Down
163 changes: 72 additions & 91 deletions samples/04.api/h.clear-after-idle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,96 +23,75 @@ This sample shows how to replace Web Chat's store to clear the conversation.

# Code

> Jump to [completed code](#completed-code) to see the end-result `App.js`, `Timer.js`, and `useTimer.js`.
> Jump to [completed code](#completed-code) to see the end-result `App.js`, `useTimeoutAt.js`, `Countdown.js`, and `useInterval.js`.

## Overview

This sample demonstrates how to clear the conversation data and start a new conversation with the user after the conversation has sat idle for a set time. To accomplish this, we created a custom hook - `useTimer` - that takes a callback as a parameter, which is called when the timer expires, and returns an array containing the time remaining in milliseconds - `timeRemaining` - and a method to set the time remaining - `setTimeRemaining`.
This sample demonstrates how to clear the conversation data and start a new conversation with the user after the conversation has sat idle for a set time.

To accomplish this, we created a state - `resetAt` - to indicate when we should reset the UI, in epoch time. Then, we created a custom hook - `useTimeoutAt` - that takes a callback as a parameter, which is called when the timer expires.

<!-- prettier-ignore-start -->
```js
import { useEffect, useState } from 'react';

export default function useTimer(fn, step = 1000) {
const [timeRemaining, setTimeRemaining] = useState();
import { useEffect } from 'react';

export default function useTimeoutAt(fn, at) {
useEffect(() => {
let timeout;
if (timeRemaining > 0) {
timeout = setTimeout(() => setTimeRemaining(ms => (ms > step ? ms - step : 0)), step);
} else if (timeRemaining === 0) {
setTimeRemaining();
fn();
}
const timer = setTimeout(fn, Math.max(0, at - Date.now()));

return () => clearTimeout(timeout);
}, [fn, timeRemaining, setTimeRemaining, step]);

return [timeRemaining, setTimeRemaining];
return () => clearTimeout(timer);
}, [fn, at]);
}
```
<!-- prettier-ignore-end -->

We also created a custom store middleware that resets the timer by calling `setTimeRemaining` with the default time interval when the user submits the send box.
We also created a custom store middleware that resets the timer by calling `setResetAt` with the default time interval when the user submits the send box, or when the connection established.

<!-- prettier-ignore-start -->
```js
setStore(
createStore({}, ({ dispatch }) => next => action => {
if (action.type === 'DIRECT_LINE/CONNECT_FULFILLED') {
dispatch({
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'webchat/join',
value: { language: window.navigator.language }
}
});
} else if (action.type === 'WEB_CHAT/SUBMIT_SEND_BOX') {
// Reset the timer when the user sends an activity
setTimeRemaining(TIME_INTERVAL);
setSession({
directLine: createDirectLine({ token }),
key,
store: createStore({}, () => next => action => {
if (action.type === 'DIRECT_LINE/CONNECT_FULFILLED' || action.type === 'WEB_CHAT/SUBMIT_SEND_BOX') {
// Reset the timer when the connection established, or the user sends an activity
setResetAt(Date.now() + IDLE_TIMEOUT);
}

return next(action);
})
);
});
```
<!-- prettier-ignore-end -->

If the user stops participating in the conversation and the timer expires, we will replace the store to clear the conversation data. However, when the store is replaced, Web Chat dispatches a `'DIRECT_LINE/DISCONNECT'`, so we also need to request a new token. The `initConversation` method handles both replacing the custom store and requesting a new Direct Line token to start a new conversation with the bot. This function is passed to the `useTimer` hook so the conversation will be restarted when the timer expires.
If the user stops participating in the conversation and the timer expires, we will replace the store to clear the conversation data. However, when the store is replaced, Web Chat dispatches a `'DIRECT_LINE/DISCONNECT'`, so we also need to request a new token. The `initConversation` method handles both replacing the custom store and requesting a new Direct Line token to start a new conversation with the bot. This function is passed to the `useTimeoutAt` hook so the conversation will be restarted when the timer expires.

<!-- prettier-ignore-start -->
```js
const initConversation = useCallback(() => {
setStore(
createStore({}, ({ dispatch }) => next => action => {
if (action.type === 'DIRECT_LINE/CONNECT_FULFILLED') {
dispatch({
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'webchat/join',
value: { language: window.navigator.language }
}
});
} else if (action.type === 'WEB_CHAT/SUBMIT_SEND_BOX') {
// Reset the timer when the user sends an activity
setTimeRemaining(TIME_INTERVAL);
}

return next(action);
})
);
setSession(false);

(async function() {
const res = await fetch('https://webchat-mockbot.azurewebsites.net/directline/token', { method: 'POST' });
const { token } = await res.json();

setDirectLine(createDirectLine({ token }));
})().catch(error => console.log(error));
}, [setStore, setDirectLine]);
const token = await fetchToken();
const key = Date.now();

setSession({
directLine: createDirectLine({ token }),
key,
store: createStore({}, () => next => action => {
if (action.type === 'DIRECT_LINE/CONNECT_FULFILLED' || action.type === 'WEB_CHAT/SUBMIT_SEND_BOX') {
// Reset the timer when the connection established, or the user sends an activity
setResetAt(Date.now() + IDLE_TIMEOUT);
}

useEffect(initConversation, []);
return next(action);
})
});
})();
}, [setResetAt, setSession]);

const [timeRemaining, setTimeRemaining] = useTimer(initConversation);
useTimeoutAt(initConversation, resetAt);
useEffect(initConversation, [initConversation]);
```
<!-- prettier-ignore-end -->

Expand All @@ -126,50 +105,52 @@ import React, { useCallback, useEffect, useState } from 'react';
import ReactWebChat, { createDirectLine, createStore } from 'botframework-webchat';

import './App.css';
import Timer from './Timer';
import useTimer from './utils/useTimer';
import Countdown from './Countdown';
import useTimeoutAt from './utils/useTimeoutAt';

const IDLE_TIMEOUT = 30000;

async function fetchToken() {
const res = await fetch('https://webchat-mockbot2.azurewebsites.net/api/token/directline', { method: 'POST' });
const { token } = await res.json();

const TIME_INTERVAL = 30000;
return token;
}

function App() {
const [directLine, setDirectLine] = useState(createDirectLine({}));
const [store, setStore] = useState();
const [resetAt, setResetAt] = useState(() => Date.now() + IDLE_TIMEOUT);
const [session, setSession] = useState();

const initConversation = useCallback(() => {
setStore(
createStore({}, ({ dispatch }) => next => action => {
if (action.type === 'DIRECT_LINE/CONNECT_FULFILLED') {
dispatch({
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'webchat/join',
value: { language: window.navigator.language }
}
});
} else if (action.type === 'WEB_CHAT/SUBMIT_SEND_BOX') {
setTimeRemaining(TIME_INTERVAL);
}

return next(action);
})
);
setSession(false);

(async function() {
const res = await fetch('https://webchat-mockbot.azurewebsites.net/directline/token', { method: 'POST' });
const { token } = await res.json();

setDirectLine(createDirectLine({ token }));
})().catch(error => console.log(error));
}, [setStore, setDirectLine]);
const token = await fetchToken();
const key = Date.now();

setSession({
directLine: createDirectLine({ token }),
key,
store: createStore({}, () => next => action => {
if (action.type === 'DIRECT_LINE/CONNECT_FULFILLED' || action.type === 'WEB_CHAT/SUBMIT_SEND_BOX') {
setResetAt(Date.now() + IDLE_TIMEOUT);
}

useEffect(initConversation, []);
return next(action);
})
});
})();
}, [setResetAt, setSession]);

const [timeRemaining, setTimeRemaining] = useTimer(initConversation);
useTimeoutAt(initConversation, resetAt);
useEffect(initConversation, [initConversation]);

return (
<div className="App">
<Timer timeRemaining={timeRemaining} />
<ReactWebChat className="chat" directLine={directLine} store={store} />
<Countdown to={resetAt} />
{!!session && (
<ReactWebChat className="chat" directLine={session.directLine} key={session.key} store={session.store} />
)}
</div>
);
}
Expand Down
Loading