set styles for uploading files

Change-Id: I7d3f934ea4f31769d72d1f21c733ab0c16b325f6
diff --git a/client/package.json b/client/package.json
index 523d9f1..2bab75d 100644
--- a/client/package.json
+++ b/client/package.json
@@ -44,13 +44,16 @@
     "check-password-strength": "^2.0.7",
     "dayjs": "^1.11.5",
     "emoji-picker-react": "^3.6.1",
+    "filesize": "^10.0.5",
     "framer-motion": "^7.3.5",
     "i18next": "^21.9.2",
     "jami-web-common": "file:../common",
+    "mime": "^3.0.0",
     "qrcode.react": "^3.1.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "react-draggable": "^4.4.5",
+    "react-dropzone": "^14.2.3",
     "react-emoji-render": "^1.2.4",
     "react-fetch-hook": "^1.9.5",
     "react-i18next": "^11.18.6",
diff --git a/client/src/components/FilePreview.tsx b/client/src/components/FilePreview.tsx
new file mode 100644
index 0000000..68c3424
--- /dev/null
+++ b/client/src/components/FilePreview.tsx
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2022 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+import { AttachFile } from '@mui/icons-material';
+import { IconButton, IconButtonProps, Stack, Typography } from '@mui/material';
+import * as mime from 'mime';
+import { useRef } from 'react';
+
+import { FileHandler } from '../utils/files';
+import { useDataSizeUnits } from '../utils/units';
+import { SaltireIcon } from './SvgIcon';
+
+interface FilePreviewIconProps {
+  fileHandler: FileHandler;
+  size: string;
+}
+
+const FilePreviewIcon = ({ fileHandler, size }: FilePreviewIconProps) => {
+  if (fileHandler.file.type.split('/')[0] === 'image') {
+    return (
+      <img
+        src={fileHandler.url}
+        alt={fileHandler.file.name}
+        style={{ height: size, width: size, objectFit: 'cover' }}
+      />
+    );
+  }
+
+  const paddedSize = parseInt(size) * 0.8 + 'px';
+  return <AttachFile sx={{ fontSize: paddedSize }} />;
+};
+
+interface FilePreviewInfosProps {
+  fileHandler: FileHandler;
+}
+
+const FilePreviewInfos = ({ fileHandler }: FilePreviewInfosProps) => {
+  const file = fileHandler.file;
+  const fileSize = useDataSizeUnits(file.size);
+  const fileType = mime.getExtension(file.type)?.toUpperCase() || '';
+  return (
+    <Stack overflow="hidden">
+      <Typography variant="body1" fontWeight="bold" overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">
+        {file.name}
+      </Typography>
+      <Typography variant="body1">{`${fileType} ${fileSize}`}</Typography>
+    </Stack>
+  );
+};
+
+const RemoveButton = (props: IconButtonProps) => {
+  const removeButtonSize = '24px';
+  const paddingPart = 0.25;
+  return (
+    <IconButton
+      {...props}
+      aria-label="remove file"
+      disableRipple={true}
+      sx={{
+        position: 'absolute',
+        height: removeButtonSize,
+        width: removeButtonSize,
+        right: -parseInt(removeButtonSize) * paddingPart + 'px',
+        top: -parseInt(removeButtonSize) * paddingPart + 'px',
+        fontSize: parseInt(removeButtonSize) * paddingPart * 2 + 'px',
+        color: 'black',
+        backgroundColor: 'white',
+        borderRadius: '100%',
+        boxShadow: '3px 3px 7px #00000029',
+        '&:hover': {
+          background: (theme) => theme.palette.primary.light,
+        },
+      }}
+    >
+      <SaltireIcon fontSize="inherit" />
+    </IconButton>
+  );
+};
+
+interface FilePreviewDeletableProps {
+  fileHandler: FileHandler;
+  remove: () => void;
+  borderColor: string;
+}
+
+export const FilePreviewRemovable = ({ fileHandler, remove, borderColor }: FilePreviewDeletableProps) => {
+  const linkRef = useRef<HTMLAnchorElement>(null);
+  const iconSize = '57px';
+
+  return (
+    <Stack
+      direction="row"
+      sx={{
+        flex: '1 1 200px',
+        minWidth: '100px',
+        maxWidth: '300px',
+        cursor: 'pointer',
+      }}
+      onClick={() => linkRef.current?.click()}
+    >
+      <a ref={linkRef} href={fileHandler.url} download hidden />
+      <Stack
+        sx={{
+          position: 'relative',
+          height: iconSize,
+          width: iconSize,
+          minWidth: iconSize,
+          marginRight: '16px',
+          borderRadius: '5px',
+          borderWidth: '3px',
+          borderColor: borderColor,
+          borderStyle: 'solid',
+          justifyContent: 'center',
+          alignItems: 'center',
+        }}
+      >
+        <RemoveButton
+          onClick={(e) => {
+            // Prevent the parent's link to be triggered
+            e.preventDefault();
+            e.stopPropagation();
+            remove();
+          }}
+        />
+        <FilePreviewIcon fileHandler={fileHandler} size={iconSize} />
+      </Stack>
+      <FilePreviewInfos fileHandler={fileHandler} />
+    </Stack>
+  );
+};
diff --git a/client/src/components/SendMessageForm.tsx b/client/src/components/SendMessageForm.tsx
index f003aae..797d810 100644
--- a/client/src/components/SendMessageForm.tsx
+++ b/client/src/components/SendMessageForm.tsx
@@ -34,16 +34,17 @@
   account: Account;
   members: ConversationMember[];
   onSend: (message: string) => void;
+  openFilePicker: () => void;
 };
 
-export default function SendMessageForm(props: SendMessageFormProps) {
+export default function SendMessageForm({ account, members, onSend, openFilePicker }: SendMessageFormProps) {
   const [currentMessage, setCurrentMessage] = useState('');
-  const placeholder = usePlaceholder(props.account, props.members);
+  const placeholder = usePlaceholder(account, members);
 
   const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
     e.preventDefault();
     if (currentMessage) {
-      props.onSend(currentMessage);
+      onSend(currentMessage);
       setCurrentMessage('');
     }
   };
@@ -55,8 +56,16 @@
   );
 
   return (
-    <Stack component="form" onSubmit={handleSubmit} direction="row" alignItems="center" spacing="20px" padding="16px">
-      <UploadFileButton />
+    <Stack
+      component="form"
+      onSubmit={handleSubmit}
+      direction="row"
+      alignItems="center"
+      spacing="20px"
+      paddingX="16px"
+      paddingTop="16px"
+    >
+      <UploadFileButton onClick={openFilePicker} />
       <RecordVoiceMessageButton />
       <RecordVideoMessageButton />
       <Stack flexGrow={1}>
diff --git a/client/src/pages/ChatInterface.tsx b/client/src/pages/ChatInterface.tsx
index 86f77da..4ad1830 100644
--- a/client/src/pages/ChatInterface.tsx
+++ b/client/src/pages/ChatInterface.tsx
@@ -15,15 +15,18 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
-import { Divider, Stack } from '@mui/material';
+import { Box, Divider, Stack } from '@mui/material';
 import { Account, ConversationMember, Message } from 'jami-web-common';
 import { useCallback, useContext, useEffect, useState } from 'react';
+import { useDropzone } from 'react-dropzone';
 
+import { FilePreviewRemovable } from '../components/FilePreview';
 import LoadingPage from '../components/Loading';
 import MessageList from '../components/MessageList';
 import SendMessageForm from '../components/SendMessageForm';
 import { SocketContext } from '../contexts/Socket';
 import { useMessagesQuery, useSendMessageMutation } from '../services/Conversation';
+import { FileHandler } from '../utils/files';
 
 type ChatInterfaceProps = {
   account: Account;
@@ -39,6 +42,34 @@
   const messagesQuery = useMessagesQuery(account.getId(), conversationId);
   const sendMessageMutation = useSendMessageMutation(account.getId(), conversationId);
 
+  const [fileHandlers, setFileHandlers] = useState<FileHandler[]>([]);
+
+  const onFilesDrop = useCallback(
+    (acceptedFiles: File[]) => {
+      const newFileHandlers = acceptedFiles.map((file) => new FileHandler(file));
+      setFileHandlers((oldFileHandlers) => [...oldFileHandlers, ...newFileHandlers]);
+    },
+    [setFileHandlers]
+  );
+
+  const removeFile = useCallback(
+    (fileId: string | number) => {
+      setFileHandlers((fileHandlers) => fileHandlers.filter((fileHandler) => fileHandler.id !== fileId));
+    },
+    [setFileHandlers]
+  );
+
+  const {
+    getRootProps,
+    getInputProps,
+    open: openFilePicker,
+    isDragActive,
+  } = useDropzone({
+    onDrop: onFilesDrop,
+    noClick: true,
+    noKeyboard: true,
+  });
+
   useEffect(() => {
     if (messagesQuery.isSuccess) {
       const sortedMessages = sortMessages(messagesQuery.data);
@@ -73,7 +104,21 @@
   }
 
   return (
-    <Stack flex={1} overflow="hidden">
+    <Stack flex={1} overflow="hidden" {...getRootProps()} paddingBottom="16px">
+      {isDragActive && (
+        // dark overlay when the user is dragging a file
+        <Box
+          sx={{
+            position: 'absolute',
+            width: '100%',
+            height: '100%',
+            backgroundColor: 'black',
+            opacity: '30%',
+            zIndex: 100,
+          }}
+        />
+      )}
+      <input {...getInputProps()} />
       <MessageList account={account} members={members} messages={messages} />
       <Divider
         sx={{
@@ -81,7 +126,37 @@
           borderTop: '1px solid #E5E5E5',
         }}
       />
-      <SendMessageForm account={account} members={members} onSend={sendMessage} />
+      <SendMessageForm account={account} members={members} onSend={sendMessage} openFilePicker={openFilePicker} />
+      {fileHandlers.length > 0 && <FilePreviewsList fileHandlers={fileHandlers} removeFile={removeFile} />}
+    </Stack>
+  );
+};
+
+interface FilePreviewsListProps {
+  fileHandlers: FileHandler[];
+  removeFile: (fileId: string | number) => void;
+}
+
+const FilePreviewsList = ({ fileHandlers, removeFile }: FilePreviewsListProps) => {
+  return (
+    <Stack
+      direction="row"
+      flexWrap="wrap"
+      gap="16px"
+      overflow="auto"
+      maxHeight="30%"
+      paddingX="16px"
+      marginTop="12px" // spacing with the component on top
+      paddingTop="4px" // spacing so "RemoveButton" are not cut
+    >
+      {fileHandlers.map((fileHandler) => (
+        <FilePreviewRemovable
+          key={fileHandler.id}
+          remove={() => removeFile(fileHandler.id)}
+          fileHandler={fileHandler}
+          borderColor={'#005699' /* Should be same color as message bubble */}
+        />
+      ))}
     </Stack>
   );
 };
diff --git a/client/src/utils/files.ts b/client/src/utils/files.ts
new file mode 100644
index 0000000..49c3171
--- /dev/null
+++ b/client/src/utils/files.ts
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2022 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+
+export class FileHandler {
+  private static lastId = 0;
+
+  private static getNewId() {
+    return ++this.lastId;
+  }
+
+  readonly id: number;
+  readonly file: File;
+  readonly url: string;
+
+  constructor(file: File) {
+    this.id = FileHandler.getNewId();
+    this.file = file;
+    this.url = URL.createObjectURL(file);
+  }
+}
diff --git a/client/src/utils/units.ts b/client/src/utils/units.ts
new file mode 100644
index 0000000..553b66c
--- /dev/null
+++ b/client/src/utils/units.ts
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2022 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+import { filesize } from 'filesize';
+import { useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+
+// 'filesize.js' library requires us to define the symbols ourselves for localization:
+// https://github.com/avoidwork/filesize.js/issues/64
+// https://github.com/avoidwork/filesize.js/issues/96
+// Could not find a library doing it by itself
+const dataSizeSymbols: Record<string, Record<string, string>> = {
+  fr: { B: 'o', kB: 'ko', MB: 'Mo', GB: 'Go', TB: 'To', PB: 'Po', EB: 'Eo', ZB: 'Zo', YB: 'Yo' },
+  ru: { B: 'Б', kB: 'кБ', MB: 'МБ', GB: 'ГБ', TB: 'ТБ', PB: 'ПБ', EB: 'ЭБ', ZB: 'ЗБ', YB: 'ЙБ' },
+  default: { B: 'B', kB: 'kB', MB: 'MB', GB: 'GB', TB: 'TB', PB: 'PB', EB: 'EB', ZB: 'ZB', YB: 'YB' },
+};
+
+export const useDataSizeUnits = (nbBytes: number) => {
+  const { i18n } = useTranslation();
+  return useMemo(() => {
+    const options = {
+      symbols: dataSizeSymbols[i18n.language] || dataSizeSymbols['default'], // undefined is not supported
+      locale: i18n.language,
+    };
+    return filesize(nbBytes, options);
+  }, [i18n, nbBytes]);
+};
diff --git a/package-lock.json b/package-lock.json
index 5ab6d9e..9c524bd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -86,13 +86,16 @@
         "check-password-strength": "^2.0.7",
         "dayjs": "^1.11.5",
         "emoji-picker-react": "^3.6.1",
+        "filesize": "^10.0.5",
         "framer-motion": "^7.3.5",
         "i18next": "^21.9.2",
         "jami-web-common": "file:../common",
+        "mime": "^3.0.0",
         "qrcode.react": "^3.1.0",
         "react": "^18.2.0",
         "react-dom": "^18.2.0",
         "react-draggable": "^4.4.5",
+        "react-dropzone": "^14.2.3",
         "react-emoji-render": "^1.2.4",
         "react-fetch-hook": "^1.9.5",
         "react-i18next": "^11.18.6",
@@ -123,6 +126,17 @@
       "integrity": "sha512-Bq7G3AErwe5A/Zki5fdD3O6+0zDChhg671NfPjtIcbtzDNZTv4NPKMRFr7gtYPG7y+B8uTiNK4Ngd9T0FTar6Q==",
       "dev": true
     },
+    "client/node_modules/mime": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
+      "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
+      "bin": {
+        "mime": "cli.js"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
     "common": {
       "name": "jami-web-common",
       "version": "1.0.0",
@@ -5243,6 +5257,14 @@
         "node": ">= 4.0.0"
       }
     },
+    "node_modules/attr-accept": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
+      "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==",
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/aws-sign2": {
       "version": "0.7.0",
       "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
@@ -8846,6 +8868,22 @@
         "node": "^10.12.0 || >=12.0.0"
       }
     },
+    "node_modules/file-selector": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz",
+      "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==",
+      "dependencies": {
+        "tslib": "^2.4.0"
+      },
+      "engines": {
+        "node": ">= 12"
+      }
+    },
+    "node_modules/file-selector/node_modules/tslib": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
+      "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
+    },
     "node_modules/file-uri-to-path": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@@ -8879,6 +8917,14 @@
         "node": ">=10"
       }
     },
+    "node_modules/filesize": {
+      "version": "10.0.5",
+      "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.0.5.tgz",
+      "integrity": "sha512-qrzyt8gLh86nsyYiC3ibI5KyIYRCWg2yqIklYrWF4a0qNfekik4OQfn7AoPJG2hRrPMSlH6fET4VEITweZAzjA==",
+      "engines": {
+        "node": ">= 14.0.0"
+      }
+    },
     "node_modules/fill-range": {
       "version": "7.0.1",
       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -11915,9 +11961,9 @@
       }
     },
     "node_modules/loader-utils": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
-      "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.3.tgz",
+      "integrity": "sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==",
       "dev": true,
       "dependencies": {
         "big.js": "^5.2.2",
@@ -14309,6 +14355,22 @@
         "react-dom": ">= 16.3.0"
       }
     },
+    "node_modules/react-dropzone": {
+      "version": "14.2.3",
+      "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz",
+      "integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==",
+      "dependencies": {
+        "attr-accept": "^2.2.2",
+        "file-selector": "^0.6.0",
+        "prop-types": "^15.8.1"
+      },
+      "engines": {
+        "node": ">= 10.13"
+      },
+      "peerDependencies": {
+        "react": ">= 16.8 || 18.0.0"
+      }
+    },
     "node_modules/react-emoji-render": {
       "version": "1.2.4",
       "resolved": "https://registry.npmjs.org/react-emoji-render/-/react-emoji-render-1.2.4.tgz",
@@ -21411,6 +21473,11 @@
       "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
       "dev": true
     },
+    "attr-accept": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
+      "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg=="
+    },
     "aws-sign2": {
       "version": "0.7.0",
       "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
@@ -24025,6 +24092,21 @@
         "flat-cache": "^3.0.4"
       }
     },
+    "file-selector": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz",
+      "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==",
+      "requires": {
+        "tslib": "^2.4.0"
+      },
+      "dependencies": {
+        "tslib": {
+          "version": "2.4.1",
+          "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
+          "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
+        }
+      }
+    },
     "file-uri-to-path": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@@ -24057,6 +24139,11 @@
         }
       }
     },
+    "filesize": {
+      "version": "10.0.5",
+      "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.0.5.tgz",
+      "integrity": "sha512-qrzyt8gLh86nsyYiC3ibI5KyIYRCWg2yqIklYrWF4a0qNfekik4OQfn7AoPJG2hRrPMSlH6fET4VEITweZAzjA=="
+    },
     "fill-range": {
       "version": "7.0.1",
       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -25549,21 +25636,24 @@
         "emoji-picker-react": "^3.6.1",
         "eslint-plugin-react": "^7.31.8",
         "eslint-plugin-react-hooks": "^4.6.0",
+        "filesize": "^10.0.5",
         "framer-motion": "^7.3.5",
         "i18next": "^21.9.2",
         "i18next-parser": "^6.5.0",
         "jami-web-common": "file:../common",
+        "mime": "^3.0.0",
         "qrcode.react": "^3.1.0",
         "react": "^18.2.0",
         "react-dom": "^18.2.0",
         "react-draggable": "^4.4.5",
+        "react-dropzone": "^14.2.3",
         "react-emoji-render": "^1.2.4",
         "react-fetch-hook": "^1.9.5",
         "react-i18next": "^11.18.6",
         "react-modal": "^3.15.1",
         "react-redux": "^8.0.2",
         "react-router-dom": "^6.3.0",
-        "react-waypoint": "*",
+        "react-waypoint": "^10.3.0",
         "sass": "^1.54.5",
         "socket.io-client": "^4.5.2",
         "typescript": "^4.7.4",
@@ -25576,6 +25666,11 @@
           "resolved": "https://registry.npmjs.org/@types/node/-/node-18.8.5.tgz",
           "integrity": "sha512-Bq7G3AErwe5A/Zki5fdD3O6+0zDChhg671NfPjtIcbtzDNZTv4NPKMRFr7gtYPG7y+B8uTiNK4Ngd9T0FTar6Q==",
           "dev": true
+        },
+        "mime": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
+          "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="
         }
       }
     },
@@ -26436,9 +26531,9 @@
       "dev": true
     },
     "loader-utils": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
-      "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.3.tgz",
+      "integrity": "sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==",
       "dev": true,
       "requires": {
         "big.js": "^5.2.2",
@@ -28220,6 +28315,16 @@
         "prop-types": "^15.8.1"
       }
     },
+    "react-dropzone": {
+      "version": "14.2.3",
+      "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz",
+      "integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==",
+      "requires": {
+        "attr-accept": "^2.2.2",
+        "file-selector": "^0.6.0",
+        "prop-types": "^15.8.1"
+      }
+    },
     "react-emoji-render": {
       "version": "1.2.4",
       "resolved": "https://registry.npmjs.org/react-emoji-render/-/react-emoji-render-1.2.4.tgz",