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

58 - Apply fixed conversation flow mode to Chat Widget #63

Merged
merged 15 commits into from
Dec 1, 2023
12 changes: 9 additions & 3 deletions src/components/chat-message/message-types/admin-message.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { useMemo } from "react";
import { motion } from "framer-motion";
import classNames from "classnames";
import { Message } from "../../../model/message-model";
Expand All @@ -18,6 +18,8 @@ import {
} from "../../../slices/chat-slice";
import { useAppDispatch } from "../../../store";
import ChatButtonGroup from "./chat-button-group";
import { parseButtons } from '../../../utils/chat-utils';


const leftAnimation = {
animate: { opacity: 1, x: 0 },
Expand All @@ -37,6 +39,10 @@ const AdminMessage = ({ message }: { message: Message }): JSX.Element => {
dispatch(sendMessageWithRating(updatedMessage));
};

const hasButtons = useMemo(() => {
return parseButtons(message).length > 0;
}, [message.buttons]);

return (
<motion.div
animate={leftAnimation.animate}
Expand Down Expand Up @@ -77,7 +83,7 @@ const AdminMessage = ({ message }: { message: Message }): JSX.Element => {
>
{![CHAT_EVENTS.GREETING, CHAT_EVENTS.EMERGENCY_NOTICE].includes(
message.event as CHAT_EVENTS
) && (
) && !hasButtons && (
<div>
<button
type="button"
Expand Down Expand Up @@ -113,7 +119,7 @@ const AdminMessage = ({ message }: { message: Message }): JSX.Element => {
)}
</div>
</div>
{message.buttons && <ChatButtonGroup message={message} />}
{hasButtons && <ChatButtonGroup message={message} />}
</div>
</motion.div>
);
Expand Down
18 changes: 5 additions & 13 deletions src/components/chat-message/message-types/chat-button-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,22 @@ import { AUTHOR_ROLES, CHAT_MODES } from '../../../constants';
import { useAppDispatch } from '../../../store';
import { addMessage, initChat, queueMessage, sendNewMessage } from '../../../slices/chat-slice';
import useChatSelector from '../../../hooks/use-chat-selector';
import { parseButtons } from '../../../utils/chat-utils';
import styles from '../chat-message.module.scss';

type MessageButton = {
title: string;
payload: string;
}

const ChatButtonGroup = ({ message }: { message: Message }): JSX.Element => {
const dispatch = useAppDispatch();
const { chatId, loading, chatMode, messages } = useChatSelector();

const parsedButtons: MessageButton[] = useMemo(() => {
try {
return JSON.parse(decodeURIComponent(message.buttons!)) as MessageButton[];
} catch(e) {
console.error(e);
return [];
}
const parsedButtons = useMemo(() => {
return parseButtons(message);
}, [message.buttons]);

const addNewMessageToState = (buttonPayload: string): void => {
const message: Message = {
chatId,
content: encodeURIComponent(buttonPayload),
content: buttonPayload,
authorTimestamp: new Date().toISOString(),
authorRole: AUTHOR_ROLES.END_USER,
};
Expand All @@ -48,11 +40,11 @@ const ChatButtonGroup = ({ message }: { message: Message }): JSX.Element => {

const enabled = messages[messages.length - 1] === message && chatMode === CHAT_MODES.FLOW;


return (
<div className={styles.buttonsRow}>
{parsedButtons?.map(({ title, payload }) => (
<button
key={payload}
type="button"
className={styles['action-button']}
onClick={() => addNewMessageToState(payload)}
Expand Down
30 changes: 19 additions & 11 deletions src/hooks/use-get-chat.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useAppDispatch } from '../store';
import sse from '../services/sse-service';
import useChatSelector from './use-chat-selector';
Expand All @@ -7,22 +7,30 @@ import { setChat } from '../slices/chat-slice';
import { Chat } from '../model/chat-model';

const useGetChat = (): void => {
const { lastReadMessageTimestamp, isChatEnded, isChatRedirected, chatId } = useChatSelector();
const { isChatEnded, chatId } = useChatSelector();
const dispatch = useAppDispatch();
const [sseUrl, setSseUrl] = useState('');

useEffect(() => {
if (!chatId || isChatEnded) return undefined;

const events = sse(
`${RUUTER_ENDPOINTS.GET_CHAT_BY_ID}?id=${chatId}`,
(data: Chat) => dispatch(setChat(data))
);
if (isChatEnded || !chatId){
setSseUrl('');
} else if(chatId) {
setSseUrl(`${RUUTER_ENDPOINTS.GET_CHAT_BY_ID}?id=${chatId}`);
}
}, [chatId, isChatEnded]);

useEffect(() => {
let events: EventSource | undefined;
if (sseUrl){
events = sse(
sseUrl,
(data: Chat) => dispatch(setChat(data))
);
}
return () => {
events.close();
events?.close();
};

}, [dispatch, lastReadMessageTimestamp, chatId, isChatEnded, isChatRedirected]);
}, [sseUrl]);
};

export default useGetChat;
67 changes: 40 additions & 27 deletions src/hooks/use-get-new-messages.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useAppDispatch } from '../store';
import sse from '../services/sse-service';
import useChatSelector from './use-chat-selector';
Expand All @@ -12,38 +12,51 @@ const useGetNewMessages = (): void => {
const { lastReadMessageTimestamp, isChatEnded, chatId } = useChatSelector();
const { jwtCookie } = useAuthenticationSelector();
const dispatch = useAppDispatch();

const [sseUrl, setSseUrl] = useState('');
const [lastReadMessageTimestampValue, setLastReadMessageTimestampValue] = useState('');

useEffect(() => {
if (!chatId || isChatEnded || !lastReadMessageTimestamp) return undefined;

const onMessage = (messages: Message[]) => {
const nonDisplayableEvent = [
CHAT_EVENTS.GREETING.toString(),
TERMINATE_STATUS.CLIENT_LEFT_WITH_ACCEPTED.toString(),
TERMINATE_STATUS.CLIENT_LEFT_WITH_NO_RESOLUTION.toString(),
TERMINATE_STATUS.CLIENT_LEFT_FOR_UNKNOWN_REASONS.toString(),
TERMINATE_STATUS.ACCEPTED.toString(),
TERMINATE_STATUS.HATE_SPEECH.toString(),
TERMINATE_STATUS.OTHER.toString(),
TERMINATE_STATUS.RESPONSE_SENT_TO_CLIENT_EMAIL.toString(),
]
if(lastReadMessageTimestamp && !lastReadMessageTimestampValue){
setLastReadMessageTimestampValue(lastReadMessageTimestamp);
}
}, [lastReadMessageTimestamp]);

const newDisplayableMessages = messages.filter((msg) => !nonDisplayableEvent.includes(msg.event!));
const stateChangingEventMessages = messages.filter((msg) => isStateChangingEventMessage(msg));
dispatch(addMessagesToDisplay(newDisplayableMessages));
dispatch(handleStateChangingEventMessages(stateChangingEventMessages));
};
useEffect(() => {
if(isChatEnded || !chatId) {
setSseUrl('');
}
else if (chatId && lastReadMessageTimestampValue) {
setSseUrl(`${RUUTER_ENDPOINTS.GET_NEW_MESSAGES}?chatId=${chatId}&timeRangeBegin=${lastReadMessageTimestampValue.split('+')[0]}`);
}
}, [isChatEnded, chatId, lastReadMessageTimestampValue]);

const events = sse(
`${RUUTER_ENDPOINTS.GET_NEW_MESSAGES}?chatId=${chatId}&timeRangeBegin=${lastReadMessageTimestamp.split('+')[0]}`,
onMessage
);
useEffect(() => {
let events: EventSource | undefined;
if (sseUrl) {
const onMessage = (messages: Message[]) => {
const nonDisplayableEvent = [
CHAT_EVENTS.GREETING.toString(),
TERMINATE_STATUS.CLIENT_LEFT_WITH_ACCEPTED.toString(),
TERMINATE_STATUS.CLIENT_LEFT_WITH_NO_RESOLUTION.toString(),
TERMINATE_STATUS.CLIENT_LEFT_FOR_UNKNOWN_REASONS.toString(),
TERMINATE_STATUS.ACCEPTED.toString(),
TERMINATE_STATUS.HATE_SPEECH.toString(),
TERMINATE_STATUS.OTHER.toString(),
TERMINATE_STATUS.RESPONSE_SENT_TO_CLIENT_EMAIL.toString(),
]

const newDisplayableMessages = messages.filter((msg) => !nonDisplayableEvent.includes(msg.event!));
const stateChangingEventMessages = messages.filter((msg) => isStateChangingEventMessage(msg));
dispatch(addMessagesToDisplay(newDisplayableMessages));
dispatch(handleStateChangingEventMessages(stateChangingEventMessages));
};

events = sse(sseUrl, onMessage);
}
return () => {
events.close();
events?.close();
};

}, [dispatch, lastReadMessageTimestamp, chatId, isChatEnded, jwtCookie]);
}, [sseUrl]);
};

export default useGetNewMessages;
5 changes: 5 additions & 0 deletions src/model/message-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,8 @@ export enum AttachmentTypes {
OGG = 'video/ogg',
MOV = 'video/quicktime',
}

export interface MessageButton {
title: string;
payload: string;
}
4 changes: 2 additions & 2 deletions src/services/sse-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ const sse = <T>(url: string, onMessage: (data: T) => void): EventSource => {
};

eventSource.onopen = () => {
console.log("SSE connection Opened");
console.log('SSE connection opened, url:', url);
};

eventSource.onerror = () => {
console.error('SSE error');
console.error('SSE error, url:', url);
};

return eventSource;
Expand Down
6 changes: 4 additions & 2 deletions src/slices/chat-slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
} from '../utils/state-management-utils';
import { setToLocalStorage } from '../utils/local-storage-utils';
import getHolidays from '../utils/holidays';
import { getChatModeBasedOnLastMessage } from '../utils/chat-utils';

export interface EstimatedWaiting {
positionInUnassignedChats: string;
Expand Down Expand Up @@ -352,8 +353,7 @@ export const chatSlice = createSlice({
state.messages.push(...receivedMessages);
setToSessionStorage('newMessagesAmount', state.newMessagesAmount);

state.chatMode = receivedMessages[receivedMessages.length - 1]?.buttons
? CHAT_MODES.FLOW : CHAT_MODES.FREE;
state.chatMode = getChatModeBasedOnLastMessage(state.messages);
},
handleStateChangingEventMessages: (state, action: PayloadAction<Message[]>) => {
action.payload.forEach((msg) => {
Expand Down Expand Up @@ -440,6 +440,8 @@ export const chatSlice = createSlice({
if (!action.payload) return;
state.lastReadMessageTimestamp = new Date().toISOString();
state.messages = action.payload;

state.chatMode = getChatModeBasedOnLastMessage(state.messages);
});
builder.addCase(getGreeting.fulfilled, (state, action) => {
if (!action.payload.isActive) return;
Expand Down
Binary file modified src/static/ding.mp3
Binary file not shown.
23 changes: 23 additions & 0 deletions src/utils/chat-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { CHAT_MODES } from "../constants";
import { Message, MessageButton } from "../model/message-model";

export const parseButtons = (message: Message): MessageButton[] => {
try {
return JSON.parse(decodeURIComponent(message?.buttons || '[]')) as MessageButton[];
} catch(e) {
console.error(e);
return [];
}
}

export const getChatModeBasedOnLastMessage = (messages: Message[]): CHAT_MODES => {
let lastMsgButtonsCount = 0;

if(messages && messages.length > 0) {
const lastMsg = messages[messages.length - 1];
const buttons = parseButtons(lastMsg);
lastMsgButtonsCount = buttons.length;
}

return lastMsgButtonsCount === 0 ? CHAT_MODES.FREE : CHAT_MODES.FLOW;
}