blob: 4ad1830d085293d691c95bc5a2bfb20fea0b505f [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';
idillon6847e252022-11-04 11:50:08 -040019import { Account, ConversationMember, Message } from 'jami-web-common';
20import { 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';
27import { SocketContext } from '../contexts/Socket';
28import { 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 = {
32 account: Account;
33 conversationId: string;
34 members: ConversationMember[];
35};
36const ChatInterface = ({ account, conversationId, members }: ChatInterfaceProps) => {
37 const socket = useContext(SocketContext);
38 const [messages, setMessages] = useState<Message[]>([]);
39 const [isLoading, setIsLoading] = useState(true);
40 const [error, setError] = useState(false);
41
42 const messagesQuery = useMessagesQuery(account.getId(), conversationId);
43 const sendMessageMutation = useSendMessageMutation(account.getId(), conversationId);
44
idilloncab81d72022-11-08 12:20:00 -050045 const [fileHandlers, setFileHandlers] = useState<FileHandler[]>([]);
46
47 const onFilesDrop = useCallback(
48 (acceptedFiles: File[]) => {
49 const newFileHandlers = acceptedFiles.map((file) => new FileHandler(file));
50 setFileHandlers((oldFileHandlers) => [...oldFileHandlers, ...newFileHandlers]);
51 },
52 [setFileHandlers]
53 );
54
55 const removeFile = useCallback(
56 (fileId: string | number) => {
57 setFileHandlers((fileHandlers) => fileHandlers.filter((fileHandler) => fileHandler.id !== fileId));
58 },
59 [setFileHandlers]
60 );
61
62 const {
63 getRootProps,
64 getInputProps,
65 open: openFilePicker,
66 isDragActive,
67 } = useDropzone({
68 onDrop: onFilesDrop,
69 noClick: true,
70 noKeyboard: true,
71 });
72
idillon6847e252022-11-04 11:50:08 -040073 useEffect(() => {
74 if (messagesQuery.isSuccess) {
75 const sortedMessages = sortMessages(messagesQuery.data);
76 setMessages(sortedMessages);
77 }
78 }, [messagesQuery.isSuccess, messagesQuery.data]);
79
80 useEffect(() => {
81 setIsLoading(messagesQuery.isLoading);
82 }, [messagesQuery.isLoading]);
83
84 useEffect(() => {
85 setError(messagesQuery.isError);
86 }, [messagesQuery.isError]);
87
88 const sendMessage = useCallback((message: string) => sendMessageMutation.mutate(message), [sendMessageMutation]);
89
90 useEffect(() => {
91 if (socket) {
92 socket.off('newMessage');
93 socket.on('newMessage', (data) => {
94 console.log('newMessage');
95 setMessages((messages) => addMessage(messages, data));
96 });
97 }
98 }, [conversationId, socket]);
99
100 if (isLoading) {
101 return <LoadingPage />;
102 } else if (error) {
103 return <div>Error loading {conversationId}</div>;
104 }
105
106 return (
idilloncab81d72022-11-08 12:20:00 -0500107 <Stack flex={1} overflow="hidden" {...getRootProps()} paddingBottom="16px">
108 {isDragActive && (
109 // dark overlay when the user is dragging a file
110 <Box
111 sx={{
112 position: 'absolute',
113 width: '100%',
114 height: '100%',
115 backgroundColor: 'black',
116 opacity: '30%',
117 zIndex: 100,
118 }}
119 />
120 )}
121 <input {...getInputProps()} />
idillon6847e252022-11-04 11:50:08 -0400122 <MessageList account={account} members={members} messages={messages} />
123 <Divider
124 sx={{
125 margin: '30px 16px 0px 16px',
126 borderTop: '1px solid #E5E5E5',
127 }}
128 />
idilloncab81d72022-11-08 12:20:00 -0500129 <SendMessageForm account={account} members={members} onSend={sendMessage} openFilePicker={openFilePicker} />
130 {fileHandlers.length > 0 && <FilePreviewsList fileHandlers={fileHandlers} removeFile={removeFile} />}
131 </Stack>
132 );
133};
134
135interface FilePreviewsListProps {
136 fileHandlers: FileHandler[];
137 removeFile: (fileId: string | number) => void;
138}
139
140const FilePreviewsList = ({ fileHandlers, removeFile }: FilePreviewsListProps) => {
141 return (
142 <Stack
143 direction="row"
144 flexWrap="wrap"
145 gap="16px"
146 overflow="auto"
147 maxHeight="30%"
148 paddingX="16px"
149 marginTop="12px" // spacing with the component on top
150 paddingTop="4px" // spacing so "RemoveButton" are not cut
151 >
152 {fileHandlers.map((fileHandler) => (
153 <FilePreviewRemovable
154 key={fileHandler.id}
155 remove={() => removeFile(fileHandler.id)}
156 fileHandler={fileHandler}
157 borderColor={'#005699' /* Should be same color as message bubble */}
158 />
159 ))}
idillon6847e252022-11-04 11:50:08 -0400160 </Stack>
161 );
162};
163
164const addMessage = (sortedMessages: Message[], message: Message) => {
165 if (sortedMessages.length === 0) {
166 return [message];
167 } else if (message.id === sortedMessages[sortedMessages.length - 1].linearizedParent) {
168 return [...sortedMessages, message];
169 } else if (message.linearizedParent === sortedMessages[0].id) {
170 return [message, ...sortedMessages];
171 } else {
172 console.error("Can't insert message " + message.id);
173 return sortedMessages;
174 }
175};
176
177const sortMessages = (messages: Message[]) => {
178 let sortedMessages: Message[] = [];
179 messages.forEach((message) => (sortedMessages = addMessage(sortedMessages, message)));
180 return sortedMessages;
181};
182
183export default ChatInterface;