From 67dfee3ae28c429a1f350e38a5d46d83ef4d9d12 Mon Sep 17 00:00:00 2001 From: Erwan LE BESCOND Date: Mon, 6 Apr 2020 17:03:48 +0200 Subject: [PATCH 1/3] fix #20 : Server-Sent Events (SSE) Support --- src/Sse.ts | 26 ++++++++++++++ src/TockContext.tsx | 14 ++++++-- src/TockTheme.ts | 1 + src/components/Chat/Chat.tsx | 4 +-- src/components/Loader/Loader.tsx | 35 +++++++++++++------ src/useTock.ts | 60 +++++++++++++++++++++++++------- 6 files changed, 113 insertions(+), 27 deletions(-) create mode 100644 src/Sse.ts diff --git a/src/Sse.ts b/src/Sse.ts new file mode 100644 index 0000000..55b7a92 --- /dev/null +++ b/src/Sse.ts @@ -0,0 +1,26 @@ +export namespace Sse { + let sseIsEnabled = false; + let eventSource: EventSource; + + export function init(tockEndPoint: string, userId: string, handleBotResponse: (botResponse: any) => void) { + if (typeof (EventSource) !== "undefined" && tockEndPoint && !eventSource) { + eventSource = new EventSource(tockEndPoint + '/sse?userid=' + userId); + eventSource.addEventListener('message', (e: MessageEvent) => { + handleBotResponse(JSON.parse(e.data)) + }, false); + eventSource.addEventListener('open', (e: Event) => { + sseIsEnabled = true; + }, false); + eventSource.addEventListener('error', (e: Event) => { + // @ts-ignore + if (e.readyState == EventSource.CLOSED) { + sseIsEnabled = false; + } + }, false); + } + } + + export function isEnable(): boolean { + return sseIsEnabled + } +} \ No newline at end of file diff --git a/src/TockContext.tsx b/src/TockContext.tsx index 411d2d4..0a95842 100644 --- a/src/TockContext.tsx +++ b/src/TockContext.tsx @@ -69,12 +69,14 @@ export interface TockState { quickReplies: QuickReply[]; messages: (Message | Card | Carousel | Widget)[]; userId: string; + loading: boolean; } export interface TockAction { - type: 'SET_QUICKREPLIES' | 'ADD_MESSAGE'; + type: 'SET_QUICKREPLIES' | 'ADD_MESSAGE' | 'SET_LOADING'; quickReplies?: QuickReply[]; messages?: (Message | Card | Carousel | Widget)[]; + loading?: boolean; } export const tockReducer: Reducer = ( @@ -96,6 +98,13 @@ export const tockReducer: Reducer = ( messages: [...state.messages, ...action.messages], }; } + case 'SET_LOADING': + if (action.loading != undefined) { + return { + ...state, + loading: action.loading + } + } default: break; } @@ -110,7 +119,8 @@ const TockContext: (props: { children?: ReactNode }) => JSX.Element = ({ const [state, dispatch]: [TockState, Dispatch] = useReducer(tockReducer, { quickReplies: [], messages: [], - userId: (Date.now().toString(36) + Math.random().toString(36).substr(2, 5)).toUpperCase() + userId: (Date.now().toString(36) + Math.random().toString(36).substr(2, 5)).toUpperCase(), + loading: false }); return ( diff --git a/src/TockTheme.ts b/src/TockTheme.ts index d7872c4..1d7c18f 100644 --- a/src/TockTheme.ts +++ b/src/TockTheme.ts @@ -10,6 +10,7 @@ export interface TockTheme { inputColor?: string; borderRadius?: string; conversationWidth?: string; + loaderSize?: string, styles?: { card?: TockThemeCardStyle; carouselContainer?: string; diff --git a/src/components/Chat/Chat.tsx b/src/components/Chat/Chat.tsx index 795a7de..6d11763 100644 --- a/src/components/Chat/Chat.tsx +++ b/src/components/Chat/Chat.tsx @@ -21,7 +21,7 @@ export interface ChatProps { } const Chat: (props: ChatProps) => JSX.Element = ({endPoint, referralParameter, timeoutBetweenMessage = 700, widgets = {}}: ChatProps) => { - const {messages, quickReplies, sendMessage, sendQuickReply, sendAction, sendReferralParameter}: UseTock = useTock( + const {messages, quickReplies, loading, sendMessage, sendQuickReply, sendAction, sendReferralParameter}: UseTock = useTock( endPoint ); const [displayableMessageCount, setDisplayableMessageCount] = useState(0); @@ -40,7 +40,6 @@ const Chat: (props: ChatProps) => JSX.Element = ({endPoint, referralParameter, t return ( - {referralParameter && displayableMessageCount == 0 && } {messages.slice(0, displayableMessageCount).map((message: Message | Card | Carousel | Widget, i: number) => { if (message.type === 'widget') { let WidgetComponent = DefaultWidget; @@ -81,6 +80,7 @@ const Chat: (props: ChatProps) => JSX.Element = ({endPoint, referralParameter, t } return null; })} + {loading && } {displayableMessageCount == messages.length && {quickReplies.map((qr: QuickReply, i: number) => ( diff --git a/src/components/Loader/Loader.tsx b/src/components/Loader/Loader.tsx index a243de6..9a3bdc0 100644 --- a/src/components/Loader/Loader.tsx +++ b/src/components/Loader/Loader.tsx @@ -5,16 +5,29 @@ import {css, keyframes} from "@emotion/core"; import {Keyframes} from "@emotion/serialize"; import TockTheme from 'TockTheme'; -const LoaderContainer: StyledComponent< - DetailedHTMLProps, HTMLDivElement>, +const LoaderContainer: StyledComponent, HTMLDivElement>, {}, - TockTheme - > = styled.div` + TockTheme> = styled.div` width: 100%; max-width: ${props => (props.theme && props.theme.conversationWidth) || '720px'}; margin: 0.5em auto; `; +const BulletList: StyledComponent<{}, {}, TockTheme> = styled.div` + display: inline-block; + color: ${props => readableColor((props.theme && props.theme.botColor) || 'black')}; + padding: 0.5em 1.5em; + margin-left: 1em; + white-space: pre-line; + border-radius: ${props => + (props.theme && + props.theme.borderRadius && + `${props.theme.borderRadius} ${props.theme.borderRadius} ${props.theme.borderRadius} 0`) || + '1em'}; + + ${props => (props.theme && props.theme.styles && props.theme.styles.messageBot) || ''} +`; + const beat: Keyframes = keyframes` 50% {transform: scale(0.75);opacity: 0.2} 100% {transform: scale(1);opacity: 1} @@ -24,9 +37,9 @@ const Bullet: StyledComponent<{}, {}, TockTheme> = styled.div(props => ( css` display: inline-block; background-color: ${readableColor((props.theme && props.theme.botColor) || 'black')}; - width: ${props.theme.fontSize}; - height: ${props.theme.fontSize}; - margin: 0.5em; + width: ${props.theme.loaderSize || '8px'}; + height: ${props.theme.loaderSize || '8px'}; + margin: 0.5em 0.5em 0.5em 0; border-radius: 100%; animation: ${beat} 0.7s linear ${props["data-rank"] % 2 ? "0s" : "0.35s"} infinite normal both running; ` @@ -35,9 +48,11 @@ const Bullet: StyledComponent<{}, {}, TockTheme> = styled.div(props => ( const Loader = () => { return ( - - - + + + + + ); }; diff --git a/src/useTock.ts b/src/useTock.ts index 8da10bb..d075a9e 100644 --- a/src/useTock.ts +++ b/src/useTock.ts @@ -9,10 +9,12 @@ import { Card, Carousel, Widget, WidgetData, } from './TockContext'; +import { Sse } from "./Sse"; export interface UseTock { messages: (Message | Card | Carousel | Widget)[]; quickReplies: QuickReply[]; + loading: boolean; addMessage: (message: string, author: 'bot' | 'user') => void; sendMessage: (message: string) => Promise; addCard: ( @@ -29,18 +31,25 @@ export interface UseTock { sendReferralParameter: (referralParameter: string) => Promise; } + + const useTock: (tockEndPoint: string) => UseTock = (tockEndPoint: string) => { - const { messages, quickReplies, userId }: TockState = useTockState(); + const { messages, quickReplies, userId, loading }: TockState = useTockState(); const dispatch: Dispatch = useTockDispatch(); - const addMessage: (message: string, author: 'bot' | 'user') => void = useCallback( - (message: string, author: 'bot' | 'user') => - dispatch({ - type: 'ADD_MESSAGE', - messages: [{ author, message, type: 'message' }], - }), - [] - ); + const startLoading: () => void = () => { + dispatch({ + type: 'SET_LOADING', + loading: true, + }); + }; + + const stopLoading: () => void = () => { + dispatch({ + type: 'SET_LOADING', + loading: false, + }); + }; const handleBotResponse: (botResponse: any) => void = ({ responses }: any) => { if (Array.isArray(responses) && responses.length > 0) { @@ -107,11 +116,27 @@ const useTock: (tockEndPoint: string) => UseTock = (tockEndPoint: string) => { } }; + const handleBotResponseIfSseDisabled: (botResponse: any) => void = (botResponse: any) => { + if (!Sse.isEnable()) { + handleBotResponse(botResponse) + } + }; + + const addMessage: (message: string, author: 'bot' | 'user') => void = useCallback( + (message: string, author: 'bot' | 'user') => + dispatch({ + type: 'ADD_MESSAGE', + messages: [{author, message, type: 'message'}], + }), + [] + ); + const sendMessage: (message: string) => Promise = useCallback((message: string) => { dispatch({ type: 'ADD_MESSAGE', messages: [{ author: 'user', message, type: 'message' }], }); + startLoading(); return fetch(tockEndPoint, { body: JSON.stringify({ query: message, @@ -123,10 +148,12 @@ const useTock: (tockEndPoint: string) => UseTock = (tockEndPoint: string) => { }, }) .then(res => res.json()) - .then(handleBotResponse); + .then(handleBotResponseIfSseDisabled) + .finally(stopLoading); }, []); const sendReferralParameter: (referralParameter: string) => Promise = useCallback((referralParameter: string) => { + startLoading(); return fetch(tockEndPoint, { body: JSON.stringify({ ref: referralParameter, @@ -137,8 +164,9 @@ const useTock: (tockEndPoint: string) => UseTock = (tockEndPoint: string) => { 'Content-Type': 'application/json', }, }) - .then(res => res.json()) - .then(handleBotResponse); + .then(res => res.json()) + .then(handleBotResponseIfSseDisabled) + .finally(stopLoading); }, []); const sendQuickReply: (label: string, payload?: string) => Promise = ( @@ -147,6 +175,7 @@ const useTock: (tockEndPoint: string) => UseTock = (tockEndPoint: string) => { ) => { if (payload) { addMessage(label, 'user'); + startLoading(); return fetch(tockEndPoint, { body: JSON.stringify({ payload, @@ -158,7 +187,8 @@ const useTock: (tockEndPoint: string) => UseTock = (tockEndPoint: string) => { }, }) .then(res => res.json()) - .then(handleBotResponse); + .then(handleBotResponseIfSseDisabled) + .finally(stopLoading); } else { return sendMessage(label); } @@ -241,9 +271,13 @@ const useTock: (tockEndPoint: string) => UseTock = (tockEndPoint: string) => { [] ); + + Sse.init(tockEndPoint, userId, handleBotResponse); + return { messages, quickReplies, + loading, addCard, addCarousel, addMessage, From e98eb3f411367e8333574279997f002d66a9e4da Mon Sep 17 00:00:00 2001 From: Erwan LE BESCOND Date: Tue, 7 Apr 2020 09:55:08 +0200 Subject: [PATCH 2/3] fix #22 : Buttons bar is under input bar --- src/components/Container/Container.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Container/Container.tsx b/src/components/Container/Container.tsx index d780bf5..9e03b0c 100644 --- a/src/components/Container/Container.tsx +++ b/src/components/Container/Container.tsx @@ -27,7 +27,7 @@ const Container: StyledComponent< } & > *:not(:first-child) { - flex: 0; + flex: unset; } & * { From 87c116acd8015196dde075067827a156f39cbf2e Mon Sep 17 00:00:00 2001 From: Erwan LE BESCOND Date: Wed, 8 Apr 2020 09:14:43 +0200 Subject: [PATCH 3/3] fix #23 : hide quickreplies after click --- src/useTock.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/useTock.ts b/src/useTock.ts index d075a9e..a425cef 100644 --- a/src/useTock.ts +++ b/src/useTock.ts @@ -174,6 +174,7 @@ const useTock: (tockEndPoint: string) => UseTock = (tockEndPoint: string) => { payload?: string ) => { if (payload) { + setQuickReplies([]); addMessage(label, 'user'); startLoading(); return fetch(tockEndPoint, {