blob: 0db7171388ee7189af76b8e1fe0c3c02c8dbdd7e [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 */
idilloncab81d72022-11-08 12:20:00 -050018import { Box, 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';
26import SendMessageForm from '../components/SendMessageForm';
simon09fe4822022-11-30 23:36:25 -050027import { useConversationContext } from '../contexts/ConversationProvider';
Issam E. Maghni0432cb72022-11-12 06:09:26 +000028import { WebSocketContext } from '../contexts/WebSocketProvider';
Misha Krieger-Raynauld6bbdacf2022-11-29 21:45:40 -050029import { useMessagesQuery, useSendMessageMutation } from '../services/conversationQueries';
idilloncab81d72022-11-08 12:20:00 -050030import { FileHandler } from '../utils/files';
idillon6847e252022-11-04 11:50:08 -040031
simonf9d78f22022-11-25 15:47:15 -050032const ChatInterface = () => {
Issam E. Maghni0432cb72022-11-12 06:09:26 +000033 const webSocket = useContext(WebSocketContext);
idillon07d31cc2022-12-06 22:40:14 -050034 const { conversationId } = useConversationContext();
idillon6847e252022-11-04 11:50:08 -040035 const [messages, setMessages] = useState<Message[]>([]);
36 const [isLoading, setIsLoading] = useState(true);
37 const [error, setError] = useState(false);
38
simon5da8ca62022-11-09 15:21:25 -050039 const messagesQuery = useMessagesQuery(conversationId);
40 const sendMessageMutation = useSendMessageMutation(conversationId);
idillon6847e252022-11-04 11:50:08 -040041
idilloncab81d72022-11-08 12:20:00 -050042 const [fileHandlers, setFileHandlers] = useState<FileHandler[]>([]);
43
44 const onFilesDrop = useCallback(
45 (acceptedFiles: File[]) => {
46 const newFileHandlers = acceptedFiles.map((file) => new FileHandler(file));
47 setFileHandlers((oldFileHandlers) => [...oldFileHandlers, ...newFileHandlers]);
48 },
49 [setFileHandlers]
50 );
51
52 const removeFile = useCallback(
53 (fileId: string | number) => {
54 setFileHandlers((fileHandlers) => fileHandlers.filter((fileHandler) => fileHandler.id !== fileId));
55 },
56 [setFileHandlers]
57 );
58
59 const {
60 getRootProps,
61 getInputProps,
62 open: openFilePicker,
63 isDragActive,
64 } = useDropzone({
65 onDrop: onFilesDrop,
66 noClick: true,
67 noKeyboard: true,
68 });
69
idillon6847e252022-11-04 11:50:08 -040070 useEffect(() => {
71 if (messagesQuery.isSuccess) {
72 const sortedMessages = sortMessages(messagesQuery.data);
73 setMessages(sortedMessages);
74 }
75 }, [messagesQuery.isSuccess, messagesQuery.data]);
76
77 useEffect(() => {
78 setIsLoading(messagesQuery.isLoading);
79 }, [messagesQuery.isLoading]);
80
81 useEffect(() => {
82 setError(messagesQuery.isError);
83 }, [messagesQuery.isError]);
84
85 const sendMessage = useCallback((message: string) => sendMessageMutation.mutate(message), [sendMessageMutation]);
86
87 useEffect(() => {
Issam E. Maghni0432cb72022-11-12 06:09:26 +000088 if (webSocket) {
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050089 const conversationMessageListener = (data: ConversationMessage) => {
idillon6847e252022-11-04 11:50:08 -040090 console.log('newMessage');
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050091 setMessages((messages) => addMessage(messages, data.message));
simona5c54ef2022-11-18 05:26:06 -050092 };
93
94 webSocket.bind(WebSocketMessageType.ConversationMessage, conversationMessageListener);
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050095
simona5c54ef2022-11-18 05:26:06 -050096 return () => {
97 webSocket.unbind(WebSocketMessageType.ConversationMessage, conversationMessageListener);
98 };
idillon6847e252022-11-04 11:50:08 -040099 }
simona5c54ef2022-11-18 05:26:06 -0500100 }, [webSocket]);
idillon6847e252022-11-04 11:50:08 -0400101
102 if (isLoading) {
103 return <LoadingPage />;
104 } else if (error) {
105 return <div>Error loading {conversationId}</div>;
106 }
107
108 return (
idilloncab81d72022-11-08 12:20:00 -0500109 <Stack flex={1} overflow="hidden" {...getRootProps()} paddingBottom="16px">
110 {isDragActive && (
111 // dark overlay when the user is dragging a file
112 <Box
113 sx={{
114 position: 'absolute',
115 width: '100%',
116 height: '100%',
117 backgroundColor: 'black',
118 opacity: '30%',
119 zIndex: 100,
120 }}
121 />
122 )}
123 <input {...getInputProps()} />
idillon07d31cc2022-12-06 22:40:14 -0500124 <MessageList messages={messages} />
idillon6847e252022-11-04 11:50:08 -0400125 <Divider
126 sx={{
127 margin: '30px 16px 0px 16px',
128 borderTop: '1px solid #E5E5E5',
129 }}
130 />
idillon07d31cc2022-12-06 22:40:14 -0500131 <SendMessageForm onSend={sendMessage} openFilePicker={openFilePicker} />
idilloncab81d72022-11-08 12:20:00 -0500132 {fileHandlers.length > 0 && <FilePreviewsList fileHandlers={fileHandlers} removeFile={removeFile} />}
133 </Stack>
134 );
135};
136
137interface FilePreviewsListProps {
138 fileHandlers: FileHandler[];
139 removeFile: (fileId: string | number) => void;
140}
141
142const FilePreviewsList = ({ fileHandlers, removeFile }: FilePreviewsListProps) => {
143 return (
144 <Stack
145 direction="row"
146 flexWrap="wrap"
147 gap="16px"
148 overflow="auto"
149 maxHeight="30%"
150 paddingX="16px"
151 marginTop="12px" // spacing with the component on top
152 paddingTop="4px" // spacing so "RemoveButton" are not cut
153 >
154 {fileHandlers.map((fileHandler) => (
155 <FilePreviewRemovable
156 key={fileHandler.id}
157 remove={() => removeFile(fileHandler.id)}
158 fileHandler={fileHandler}
159 borderColor={'#005699' /* Should be same color as message bubble */}
160 />
161 ))}
idillon6847e252022-11-04 11:50:08 -0400162 </Stack>
163 );
164};
165
166const addMessage = (sortedMessages: Message[], message: Message) => {
167 if (sortedMessages.length === 0) {
168 return [message];
169 } else if (message.id === sortedMessages[sortedMessages.length - 1].linearizedParent) {
170 return [...sortedMessages, message];
171 } else if (message.linearizedParent === sortedMessages[0].id) {
172 return [message, ...sortedMessages];
173 } else {
174 console.error("Can't insert message " + message.id);
175 return sortedMessages;
176 }
177};
178
179const sortMessages = (messages: Message[]) => {
180 let sortedMessages: Message[] = [];
181 messages.forEach((message) => (sortedMessages = addMessage(sortedMessages, message)));
182 return sortedMessages;
183};
184
185export default ChatInterface;