blob: f30c15aec2424abbfa6e0719b6b01a34be8f5ff8 [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';
simona5c54ef2022-11-18 05:26:06 -050019import { ConversationMember, 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';
Issam E. Maghni0432cb72022-11-12 06:09:26 +000027import { WebSocketContext } from '../contexts/WebSocketProvider';
idillon6847e252022-11-04 11:50:08 -040028import { useMessagesQuery, useSendMessageMutation } from '../services/Conversation';
idilloncab81d72022-11-08 12:20:00 -050029import { FileHandler } from '../utils/files';
idillon6847e252022-11-04 11:50:08 -040030
31type ChatInterfaceProps = {
idillon6847e252022-11-04 11:50:08 -040032 conversationId: string;
33 members: ConversationMember[];
34};
simon5da8ca62022-11-09 15:21:25 -050035const ChatInterface = ({ conversationId, members }: ChatInterfaceProps) => {
Issam E. Maghni0432cb72022-11-12 06:09:26 +000036 const webSocket = useContext(WebSocketContext);
idillon6847e252022-11-04 11:50:08 -040037 const [messages, setMessages] = useState<Message[]>([]);
38 const [isLoading, setIsLoading] = useState(true);
39 const [error, setError] = useState(false);
40
simon5da8ca62022-11-09 15:21:25 -050041 const messagesQuery = useMessagesQuery(conversationId);
42 const sendMessageMutation = useSendMessageMutation(conversationId);
idillon6847e252022-11-04 11:50:08 -040043
idilloncab81d72022-11-08 12:20:00 -050044 const [fileHandlers, setFileHandlers] = useState<FileHandler[]>([]);
45
46 const onFilesDrop = useCallback(
47 (acceptedFiles: File[]) => {
48 const newFileHandlers = acceptedFiles.map((file) => new FileHandler(file));
49 setFileHandlers((oldFileHandlers) => [...oldFileHandlers, ...newFileHandlers]);
50 },
51 [setFileHandlers]
52 );
53
54 const removeFile = useCallback(
55 (fileId: string | number) => {
56 setFileHandlers((fileHandlers) => fileHandlers.filter((fileHandler) => fileHandler.id !== fileId));
57 },
58 [setFileHandlers]
59 );
60
61 const {
62 getRootProps,
63 getInputProps,
64 open: openFilePicker,
65 isDragActive,
66 } = useDropzone({
67 onDrop: onFilesDrop,
68 noClick: true,
69 noKeyboard: true,
70 });
71
idillon6847e252022-11-04 11:50:08 -040072 useEffect(() => {
73 if (messagesQuery.isSuccess) {
74 const sortedMessages = sortMessages(messagesQuery.data);
75 setMessages(sortedMessages);
76 }
77 }, [messagesQuery.isSuccess, messagesQuery.data]);
78
79 useEffect(() => {
80 setIsLoading(messagesQuery.isLoading);
81 }, [messagesQuery.isLoading]);
82
83 useEffect(() => {
84 setError(messagesQuery.isError);
85 }, [messagesQuery.isError]);
86
87 const sendMessage = useCallback((message: string) => sendMessageMutation.mutate(message), [sendMessageMutation]);
88
89 useEffect(() => {
Issam E. Maghni0432cb72022-11-12 06:09:26 +000090 if (webSocket) {
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050091 const conversationMessageListener = (data: ConversationMessage) => {
idillon6847e252022-11-04 11:50:08 -040092 console.log('newMessage');
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050093 setMessages((messages) => addMessage(messages, data.message));
simona5c54ef2022-11-18 05:26:06 -050094 };
95
96 webSocket.bind(WebSocketMessageType.ConversationMessage, conversationMessageListener);
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050097
simona5c54ef2022-11-18 05:26:06 -050098 return () => {
99 webSocket.unbind(WebSocketMessageType.ConversationMessage, conversationMessageListener);
100 };
idillon6847e252022-11-04 11:50:08 -0400101 }
simona5c54ef2022-11-18 05:26:06 -0500102 }, [webSocket]);
idillon6847e252022-11-04 11:50:08 -0400103
104 if (isLoading) {
105 return <LoadingPage />;
106 } else if (error) {
107 return <div>Error loading {conversationId}</div>;
108 }
109
110 return (
idilloncab81d72022-11-08 12:20:00 -0500111 <Stack flex={1} overflow="hidden" {...getRootProps()} paddingBottom="16px">
112 {isDragActive && (
113 // dark overlay when the user is dragging a file
114 <Box
115 sx={{
116 position: 'absolute',
117 width: '100%',
118 height: '100%',
119 backgroundColor: 'black',
120 opacity: '30%',
121 zIndex: 100,
122 }}
123 />
124 )}
125 <input {...getInputProps()} />
simon5da8ca62022-11-09 15:21:25 -0500126 <MessageList members={members} messages={messages} />
idillon6847e252022-11-04 11:50:08 -0400127 <Divider
128 sx={{
129 margin: '30px 16px 0px 16px',
130 borderTop: '1px solid #E5E5E5',
131 }}
132 />
simon5da8ca62022-11-09 15:21:25 -0500133 <SendMessageForm members={members} onSend={sendMessage} openFilePicker={openFilePicker} />
idilloncab81d72022-11-08 12:20:00 -0500134 {fileHandlers.length > 0 && <FilePreviewsList fileHandlers={fileHandlers} removeFile={removeFile} />}
135 </Stack>
136 );
137};
138
139interface FilePreviewsListProps {
140 fileHandlers: FileHandler[];
141 removeFile: (fileId: string | number) => void;
142}
143
144const FilePreviewsList = ({ fileHandlers, removeFile }: FilePreviewsListProps) => {
145 return (
146 <Stack
147 direction="row"
148 flexWrap="wrap"
149 gap="16px"
150 overflow="auto"
151 maxHeight="30%"
152 paddingX="16px"
153 marginTop="12px" // spacing with the component on top
154 paddingTop="4px" // spacing so "RemoveButton" are not cut
155 >
156 {fileHandlers.map((fileHandler) => (
157 <FilePreviewRemovable
158 key={fileHandler.id}
159 remove={() => removeFile(fileHandler.id)}
160 fileHandler={fileHandler}
161 borderColor={'#005699' /* Should be same color as message bubble */}
162 />
163 ))}
idillon6847e252022-11-04 11:50:08 -0400164 </Stack>
165 );
166};
167
168const addMessage = (sortedMessages: Message[], message: Message) => {
169 if (sortedMessages.length === 0) {
170 return [message];
171 } else if (message.id === sortedMessages[sortedMessages.length - 1].linearizedParent) {
172 return [...sortedMessages, message];
173 } else if (message.linearizedParent === sortedMessages[0].id) {
174 return [message, ...sortedMessages];
175 } else {
176 console.error("Can't insert message " + message.id);
177 return sortedMessages;
178 }
179};
180
181const sortMessages = (messages: Message[]) => {
182 let sortedMessages: Message[] = [];
183 messages.forEach((message) => (sortedMessages = addMessage(sortedMessages, message)));
184 return sortedMessages;
185};
186
187export default ChatInterface;