add 'Scroll to the end of the conversation' bar

Change-Id: I63e41433db4999d021bc13375cf5b7ba07843c3d
diff --git a/client/package.json b/client/package.json
index 875bf02..523d9f1 100644
--- a/client/package.json
+++ b/client/package.json
@@ -57,6 +57,7 @@
     "react-modal": "^3.15.1",
     "react-redux": "^8.0.2",
     "react-router-dom": "^6.3.0",
+    "react-waypoint": "^10.3.0",
     "socket.io-client": "^4.5.2"
   },
   "devDependencies": {
diff --git a/client/src/components/ConversationView.tsx b/client/src/components/ConversationView.tsx
index a269c89..1242541 100644
--- a/client/src/components/ConversationView.tsx
+++ b/client/src/components/ConversationView.tsx
@@ -101,31 +101,25 @@
 
   return (
     <Stack height="100%">
-      <Stack padding="16px">
-        <ConversationHeader
-          account={account}
-          members={conversation.getMembers()}
-          adminTitle={conversation.infos.title as string}
-          conversationId={conversationId}
-        />
-      </Stack>
+      <ConversationHeader
+        account={account}
+        members={conversation.getMembers()}
+        adminTitle={conversation.infos.title as string}
+        conversationId={conversationId}
+      />
       <Divider
         sx={{
           borderTop: '1px solid #E5E5E5',
         }}
       />
-      <Stack flex={1} overflow="auto" direction="column-reverse" padding="0px 16px">
-        <MessageList account={account} members={conversation.getMembers()} messages={messages} />
-      </Stack>
+      <MessageList account={account} members={conversation.getMembers()} messages={messages} />
       <Divider
         sx={{
           margin: '30px 16px 0px 16px',
           borderTop: '1px solid #E5E5E5',
         }}
       />
-      <Stack padding="16px">
-        <SendMessageForm account={account} members={conversation.getMembers()} onSend={sendMessage} />
-      </Stack>
+      <SendMessageForm account={account} members={conversation.getMembers()} onSend={sendMessage} />
     </Stack>
   );
 };
@@ -173,7 +167,7 @@
   };
 
   return (
-    <Stack direction="row">
+    <Stack direction="row" padding="16px">
       <Stack flex={1} justifyContent="center" whiteSpace="nowrap" overflow="hidden">
         <Typography variant="h3" textOverflow="ellipsis">
           {title}
diff --git a/client/src/components/MessageList.tsx b/client/src/components/MessageList.tsx
index 6e5e233..7b9bf2d 100644
--- a/client/src/components/MessageList.tsx
+++ b/client/src/components/MessageList.tsx
@@ -15,10 +15,15 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
-import { Stack } from '@mui/system';
+import { Typography } from '@mui/material';
+import { Box, Stack } from '@mui/system';
 import { Account, ConversationMember, Message } from 'jami-web-common';
+import { MutableRefObject, useRef, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Waypoint } from 'react-waypoint';
 
 import { MessageRow } from './Message';
+import { ArrowDownIcon } from './SvgIcon';
 
 interface MessageListProps {
   account: Account;
@@ -27,31 +32,82 @@
 }
 
 export default function MessageList({ account, members, messages }: MessageListProps) {
+  const [showScrollButton, setShowScrollButton] = useState(false);
+  const listBottomRef = useRef<HTMLElement>();
+
   return (
-    <Stack direction="column-reverse">
-      {
-        // most recent messages first
-        messages.map((message, index) => {
-          const isAccountMessage = message.author === account.getUri();
-          let author;
-          if (isAccountMessage) {
-            author = account;
-          } else {
-            const member = members.find((member) => message.author === member.contact.getUri());
-            author = member?.contact;
+    <>
+      <Stack flex={1} overflow="auto" padding="0px 16px" direction="column-reverse">
+        {/* Here is the bottom of the list of messages because of 'column-reverse' */}
+        <Box ref={listBottomRef} />
+        <Waypoint
+          onEnter={() => setShowScrollButton(false)}
+          onLeave={() => setShowScrollButton(true)}
+          bottomOffset="-100px"
+        />
+        <Stack direction="column-reverse">
+          {
+            // most recent messages first
+            messages.map((message, index) => {
+              const isAccountMessage = message.author === account.getUri();
+              let author;
+              if (isAccountMessage) {
+                author = account;
+              } else {
+                const member = members.find((member) => message.author === member.contact.getUri());
+                author = member?.contact;
+              }
+              if (!author) {
+                return null;
+              }
+              const props = {
+                messageIndex: index,
+                messages,
+                isAccountMessage,
+                author,
+              };
+              return <MessageRow key={message.id} {...props} />;
+            })
           }
-          if (!author) {
-            return null;
-          }
-          const props = {
-            messageIndex: index,
-            messages,
-            isAccountMessage,
-            author,
-          };
-          return <MessageRow key={message.id} {...props} />;
-        })
-      }
-    </Stack>
+        </Stack>
+        <Waypoint onEnter={() => console.log('should load more messages')} topOffset="-200px" />
+      </Stack>
+      {showScrollButton && (
+        <Box position="relative">
+          <Box position="absolute" bottom="10px" left="50%" sx={{ transform: 'translate(-50%)' }}>
+            <ScrollToEndButton listBottomRef={listBottomRef} />
+          </Box>
+        </Box>
+      )}
+    </>
   );
 }
+
+interface ScrollToEndButtonProps {
+  listBottomRef: MutableRefObject<HTMLElement | undefined>;
+}
+
+const ScrollToEndButton = ({ listBottomRef }: ScrollToEndButtonProps) => {
+  const { t } = useTranslation();
+  const textColor = 'white';
+  return (
+    <Stack
+      direction="row"
+      borderRadius="5px"
+      height="30px"
+      alignItems="center"
+      padding="0 16px"
+      spacing="12px"
+      sx={{
+        backgroundColor: '#005699', // Should be same color as message bubble
+        cursor: 'pointer',
+      }}
+      onClick={() => listBottomRef.current?.scrollIntoView({ behavior: 'smooth' })}
+    >
+      <ArrowDownIcon sx={{ fontSize: '12px', color: textColor }} />
+      <Typography variant="caption" fontWeight="bold" color={textColor}>
+        {t('messages_scroll_to_end')}
+      </Typography>
+    </Stack>
+  );
+};
diff --git a/client/src/components/SendMessageForm.tsx b/client/src/components/SendMessageForm.tsx
index 7230dd0..f003aae 100644
--- a/client/src/components/SendMessageForm.tsx
+++ b/client/src/components/SendMessageForm.tsx
@@ -55,7 +55,7 @@
   );
 
   return (
-    <Stack component="form" onSubmit={handleSubmit} direction="row" alignItems="center" spacing="20px">
+    <Stack component="form" onSubmit={handleSubmit} direction="row" alignItems="center" spacing="20px" padding="16px">
       <UploadFileButton />
       <RecordVoiceMessageButton />
       <RecordVideoMessageButton />
diff --git a/client/src/components/SvgIcon.tsx b/client/src/components/SvgIcon.tsx
index 9e560dc..0b8b9ed 100644
--- a/client/src/components/SvgIcon.tsx
+++ b/client/src/components/SvgIcon.tsx
@@ -64,6 +64,17 @@
   );
 };
 
+export const ArrowDownIcon = (props: SvgIconProps) => {
+  return (
+    <SvgIcon {...props} viewBox="0 0 8 11.607">
+      <path
+        fillRule="evenodd"
+        d="m4.353 11.43 3.441-3.5.058-.058a.564.564 0 0 0-.058-.816.62.62 0 0 0-.816.058l-2.45 2.508V.583A.551.551 0 0 0 3.945 0a.551.551 0 0 0-.581.583v9.039L.912 7.114a.613.613 0 0 0-.758 0 .621.621 0 0 0 0 .816l3.441 3.5a.452.452 0 0 0 .583.117c.058 0 .117-.058.175-.117"
+      />
+    </SvgIcon>
+  );
+};
+
 export const ArrowLeftCurved = (props: SvgIconProps) => {
   return (
     <SvgIcon {...props} viewBox="0 0 16 8.814">
diff --git a/client/src/locale/en/translation.json b/client/src/locale/en/translation.json
index bef16ed..049bc55 100644
--- a/client/src/locale/en/translation.json
+++ b/client/src/locale/en/translation.json
@@ -26,6 +26,8 @@
   "message_input_placeholder_four": "Write to {{member0}}, {{member1}}, {{member2}}, +1 other member",
   "message_input_placeholder_more": "Write to {{member0}}, {{member1}}, {{member2}}, +{{excess}} other members",
 
+  "messages_scroll_to_end": "Scroll to the end of the conversation",
+
   "username_input_default_helper_text": "",
   "username_input_success_helper_text": "Username available",
   "username_input_taken_helper_text": "Username already taken",
diff --git a/client/src/locale/en/translation_old.json b/client/src/locale/en/translation_old.json
new file mode 100644
index 0000000..a1a3e9a
--- /dev/null
+++ b/client/src/locale/en/translation_old.json
@@ -0,0 +1,15 @@
+{
+  "password_input_default_helper_text": "",
+  "password_input_medium_helper_text": "Medium",
+  "password_input_registration_failed_helper_text": "Choose another password!",
+  "password_input_strong_helper_text": "Strong",
+  "password_input_too_weak_helper_text": "Too weak",
+  "password_input_weak_helper_text": "Weak",
+  "severity_error": "Error",
+  "severity_success": "Success",
+  "username_input_default_helper_text": "",
+  "username_input_invalid_helper_text": "Username doesn't follow required pattern",
+  "username_input_registration_failed_helper_text": "Username not correct!",
+  "username_input_success_helper_text": "Username available",
+  "username_input_taken_helper_text": "Username already taken"
+}
diff --git a/client/src/locale/fr/translation.json b/client/src/locale/fr/translation.json
index 97a1cf6..346f0aa 100644
--- a/client/src/locale/fr/translation.json
+++ b/client/src/locale/fr/translation.json
@@ -24,7 +24,9 @@
   "message_input_placeholder_two": "Écrire à {{member0}} et {{member1}}",
   "message_input_placeholder_three": "Écrire à {{member0}}, {{member1}} et {{member2}}",
   "message_input_placeholder_four": "Écrire à {{member0}}, {{member1}}, {{member2}}, +1 autre membre",
-  "message_input_placeholder_more": "Écrire à {{member01}}, {{member1}}, {{member2}}, +{{excess}} autres membres",
+  "message_input_placeholder_more": "Écrire à {{member0}}, {{member1}}, {{member2}}, +{{excess}} autres membres",
+
+  "messages_scroll_to_end": "Faire défiler jusqu'à la fin de la conversation",
 
   "share_screen": "Partager votre écran",
   "share_screen_area": "Partager une partie de l'écran",
diff --git a/package-lock.json b/package-lock.json
index 37da114..5ab6d9e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -99,6 +99,7 @@
         "react-modal": "^3.15.1",
         "react-redux": "^8.0.2",
         "react-router-dom": "^6.3.0",
+        "react-waypoint": "^10.3.0",
         "socket.io-client": "^4.5.2"
       },
       "devDependencies": {
@@ -6599,6 +6600,11 @@
       "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
       "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="
     },
+    "node_modules/consolidated-events": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/consolidated-events/-/consolidated-events-2.0.2.tgz",
+      "integrity": "sha512-2/uRVMdRypf5z/TW/ncD/66l75P5hH2vM/GR8Jf8HLc2xnfJtmina6F6du8+v4Z2vTrMo7jC+W1tmEEuuELgkQ=="
+    },
     "node_modules/content-disposition": {
       "version": "0.5.4",
       "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -14468,6 +14474,20 @@
         "react-dom": ">=16.6.0"
       }
     },
+    "node_modules/react-waypoint": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/react-waypoint/-/react-waypoint-10.3.0.tgz",
+      "integrity": "sha512-iF1y2c1BsoXuEGz08NoahaLFIGI9gTUAAOKip96HUmylRT6DUtpgoBPjk/Y8dfcFVmfVDvUzWjNXpZyKTOV0SQ==",
+      "dependencies": {
+        "@babel/runtime": "^7.12.5",
+        "consolidated-events": "^1.1.0 || ^2.0.0",
+        "prop-types": "^15.0.0",
+        "react-is": "^17.0.1 || ^18.0.0"
+      },
+      "peerDependencies": {
+        "react": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
+      }
+    },
     "node_modules/read": {
       "version": "1.0.7",
       "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz",
@@ -22397,6 +22417,11 @@
       "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
       "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="
     },
+    "consolidated-events": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/consolidated-events/-/consolidated-events-2.0.2.tgz",
+      "integrity": "sha512-2/uRVMdRypf5z/TW/ncD/66l75P5hH2vM/GR8Jf8HLc2xnfJtmina6F6du8+v4Z2vTrMo7jC+W1tmEEuuELgkQ=="
+    },
     "content-disposition": {
       "version": "0.5.4",
       "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -25538,6 +25563,7 @@
         "react-modal": "^3.15.1",
         "react-redux": "^8.0.2",
         "react-router-dom": "^6.3.0",
+        "react-waypoint": "*",
         "sass": "^1.54.5",
         "socket.io-client": "^4.5.2",
         "typescript": "^4.7.4",
@@ -28289,6 +28315,17 @@
         "prop-types": "^15.6.2"
       }
     },
+    "react-waypoint": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/react-waypoint/-/react-waypoint-10.3.0.tgz",
+      "integrity": "sha512-iF1y2c1BsoXuEGz08NoahaLFIGI9gTUAAOKip96HUmylRT6DUtpgoBPjk/Y8dfcFVmfVDvUzWjNXpZyKTOV0SQ==",
+      "requires": {
+        "@babel/runtime": "^7.12.5",
+        "consolidated-events": "^1.1.0 || ^2.0.0",
+        "prop-types": "^15.0.0",
+        "react-is": "^17.0.1 || ^18.0.0"
+      }
+    },
     "read": {
       "version": "1.0.7",
       "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz",