blob: 37e75e2f7af29d107d198e17a24cf6934f73f3db [file] [log] [blame]
idillon6847e252022-11-04 11:50:08 -04001/*
2 * Copyright (C) 2022 Savoir-faire Linux Inc.
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU Affero General Public License as
6 * published by the Free Software Foundation; either version 3 of the
7 * License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Affero General Public License for more details.
13 *
14 * You should have received a copy of the GNU Affero General Public
15 * License along with this program. If not, see
16 * <https://www.gnu.org/licenses/>.
17 */
idillonb0ec86a2022-12-06 17:54:55 -050018import { Divider, Stack } from '@mui/material';
simonf9d78f22022-11-25 15:47:15 -050019import { ConversationMessage, Message, WebSocketMessageType } from 'jami-web-common';
idillon6847e252022-11-04 11:50:08 -040020import { useCallback, useContext, useEffect, useState } from 'react';
idilloncab81d72022-11-08 12:20:00 -050021import { useDropzone } from 'react-dropzone';
idillon6847e252022-11-04 11:50:08 -040022
idilloncab81d72022-11-08 12:20:00 -050023import { FilePreviewRemovable } from '../components/FilePreview';
idillon6847e252022-11-04 11:50:08 -040024import LoadingPage from '../components/Loading';
25import MessageList from '../components/MessageList';
idillonb0ec86a2022-12-06 17:54:55 -050026import { FileDragOverlay } from '../components/Overlay';
idillon6847e252022-11-04 11:50:08 -040027import SendMessageForm from '../components/SendMessageForm';
simon09fe4822022-11-30 23:36:25 -050028import { useConversationContext } from '../contexts/ConversationProvider';
Issam E. Maghni0432cb72022-11-12 06:09:26 +000029import { WebSocketContext } from '../contexts/WebSocketProvider';
Misha Krieger-Raynauld6bbdacf2022-11-29 21:45:40 -050030import { useMessagesQuery, useSendMessageMutation } from '../services/conversationQueries';
idilloncab81d72022-11-08 12:20:00 -050031import { FileHandler } from '../utils/files';
idillon6847e252022-11-04 11:50:08 -040032
simonf9d78f22022-11-25 15:47:15 -050033const ChatInterface = () => {
Issam E. Maghni0432cb72022-11-12 06:09:26 +000034 const webSocket = useContext(WebSocketContext);
idillon07d31cc2022-12-06 22:40:14 -050035 const { conversationId } = useConversationContext();
idillon6847e252022-11-04 11:50:08 -040036 const [messages, setMessages] = useState<Message[]>([]);
37 const [isLoading, setIsLoading] = useState(true);
38 const [error, setError] = useState(false);
39
simon5da8ca62022-11-09 15:21:25 -050040 const messagesQuery = useMessagesQuery(conversationId);
41 const sendMessageMutation = useSendMessageMutation(conversationId);
idillon6847e252022-11-04 11:50:08 -040042
idilloncab81d72022-11-08 12:20:00 -050043 const [fileHandlers, setFileHandlers] = useState<FileHandler[]>([]);
44
45 const onFilesDrop = useCallback(
46 (acceptedFiles: File[]) => {
47 const newFileHandlers = acceptedFiles.map((file) => new FileHandler(file));
48 setFileHandlers((oldFileHandlers) => [...oldFileHandlers, ...newFileHandlers]);
49 },
50 [setFileHandlers]
51 );
52
53 const removeFile = useCallback(
54 (fileId: string | number) => {
55 setFileHandlers((fileHandlers) => fileHandlers.filter((fileHandler) => fileHandler.id !== fileId));
56 },
57 [setFileHandlers]
58 );
59
60 const {
61 getRootProps,
62 getInputProps,
63 open: openFilePicker,
64 isDragActive,
65 } = useDropzone({
66 onDrop: onFilesDrop,
67 noClick: true,
68 noKeyboard: true,
69 });
70
idillon6847e252022-11-04 11:50:08 -040071 useEffect(() => {
72 if (messagesQuery.isSuccess) {
73 const sortedMessages = sortMessages(messagesQuery.data);
74 setMessages(sortedMessages);
75 }
76 }, [messagesQuery.isSuccess, messagesQuery.data]);
77
78 useEffect(() => {
79 setIsLoading(messagesQuery.isLoading);
80 }, [messagesQuery.isLoading]);
81
82 useEffect(() => {
83 setError(messagesQuery.isError);
84 }, [messagesQuery.isError]);
85
86 const sendMessage = useCallback((message: string) => sendMessageMutation.mutate(message), [sendMessageMutation]);
87
88 useEffect(() => {
Issam E. Maghni0432cb72022-11-12 06:09:26 +000089 if (webSocket) {
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050090 const conversationMessageListener = (data: ConversationMessage) => {
idillon6847e252022-11-04 11:50:08 -040091 console.log('newMessage');
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050092 setMessages((messages) => addMessage(messages, data.message));
simona5c54ef2022-11-18 05:26:06 -050093 };
94
95 webSocket.bind(WebSocketMessageType.ConversationMessage, conversationMessageListener);
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050096
simona5c54ef2022-11-18 05:26:06 -050097 return () => {
98 webSocket.unbind(WebSocketMessageType.ConversationMessage, conversationMessageListener);
99 };
idillon6847e252022-11-04 11:50:08 -0400100 }
simona5c54ef2022-11-18 05:26:06 -0500101 }, [webSocket]);
idillon6847e252022-11-04 11:50:08 -0400102
103 if (isLoading) {
104 return <LoadingPage />;
105 } else if (error) {
106 return <div>Error loading {conversationId}</div>;
107 }
108
109 return (
idilloncab81d72022-11-08 12:20:00 -0500110 <Stack flex={1} overflow="hidden" {...getRootProps()} paddingBottom="16px">
idillonb0ec86a2022-12-06 17:54:55 -0500111 {isDragActive && <FileDragOverlay />}
idilloncab81d72022-11-08 12:20:00 -0500112 <input {...getInputProps()} />
idillon07d31cc2022-12-06 22:40:14 -0500113 <MessageList messages={messages} />
idillon6847e252022-11-04 11:50:08 -0400114 <Divider
115 sx={{
116 margin: '30px 16px 0px 16px',
117 borderTop: '1px solid #E5E5E5',
118 }}
119 />
idillon07d31cc2022-12-06 22:40:14 -0500120 <SendMessageForm onSend={sendMessage} openFilePicker={openFilePicker} />
idilloncab81d72022-11-08 12:20:00 -0500121 {fileHandlers.length > 0 && <FilePreviewsList fileHandlers={fileHandlers} removeFile={removeFile} />}
122 </Stack>
123 );
124};
125
126interface FilePreviewsListProps {
127 fileHandlers: FileHandler[];
128 removeFile: (fileId: string | number) => void;
129}
130
131const FilePreviewsList = ({ fileHandlers, removeFile }: FilePreviewsListProps) => {
132 return (
133 <Stack
134 direction="row"
135 flexWrap="wrap"
136 gap="16px"
137 overflow="auto"
138 maxHeight="30%"
139 paddingX="16px"
140 marginTop="12px" // spacing with the component on top
141 paddingTop="4px" // spacing so "RemoveButton" are not cut
142 >
143 {fileHandlers.map((fileHandler) => (
144 <FilePreviewRemovable
145 key={fileHandler.id}
146 remove={() => removeFile(fileHandler.id)}
147 fileHandler={fileHandler}
148 borderColor={'#005699' /* Should be same color as message bubble */}
149 />
150 ))}
idillon6847e252022-11-04 11:50:08 -0400151 </Stack>
152 );
153};
154
155const addMessage = (sortedMessages: Message[], message: Message) => {
156 if (sortedMessages.length === 0) {
157 return [message];
158 } else if (message.id === sortedMessages[sortedMessages.length - 1].linearizedParent) {
159 return [...sortedMessages, message];
160 } else if (message.linearizedParent === sortedMessages[0].id) {
161 return [message, ...sortedMessages];
162 } else {
163 console.error("Can't insert message " + message.id);
164 return sortedMessages;
165 }
166};
167
168const sortMessages = (messages: Message[]) => {
169 let sortedMessages: Message[] = [];
170 messages.forEach((message) => (sortedMessages = addMessage(sortedMessages, message)));
171 return sortedMessages;
172};
173
174export default ChatInterface;