Add helper components for UI

Changes:
- Extracted Copyright from LoginDialog file to its own
- Added some helper components
- Removed unnecessary setup from welcome animation
- Added custom React type for svg element
- Fixed ThemeDemonstrator

GitLab: #12
Change-Id: Ie7158520983dab5c7069f179b6f9531b5106ba85
diff --git a/client/src/components/Button.tsx b/client/src/components/Button.tsx
index 25ddf39..0d401a2 100644
--- a/client/src/components/Button.tsx
+++ b/client/src/components/Button.tsx
@@ -49,6 +49,7 @@
   VideoCameraIcon,
   VolumeIcon,
 } from './SvgIcon';
+import CustomTooltip from './Tooltip';
 
 type ShapedButtonProps = IconButtonProps & {
   Icon: ComponentType<SvgIconProps>;
@@ -99,8 +100,15 @@
   return <RoundButton {...props} aria-label="take picture" Icon={CameraIcon} size="large" />;
 };
 
-export const InfoButton = (props: IconButtonProps) => {
-  return <RoundButton {...props} aria-label="informations" Icon={InfoIcon} size="small" />;
+type InfoButtonProps = IconButtonProps & {
+  tooltipTitle: string;
+};
+export const InfoButton = ({ tooltipTitle, ...props }: InfoButtonProps) => {
+  return (
+    <CustomTooltip className="tooltip" title={tooltipTitle}>
+      <RoundButton {...props} aria-label="informations" Icon={InfoIcon} size="small" />
+    </CustomTooltip>
+  );
 };
 
 export const TipButton = (props: IconButtonProps) => {
diff --git a/client/src/components/Copyright.tsx b/client/src/components/Copyright.tsx
new file mode 100644
index 0000000..a89855f
--- /dev/null
+++ b/client/src/components/Copyright.tsx
@@ -0,0 +1,34 @@
+/*
+ * 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 { Link } from '@mui/material';
+import Typography from '@mui/material/Typography';
+
+// TODO: Choose on which pages to put this.
+export default function Copyright() {
+  return (
+    <Typography variant="body2" color="textSecondary" align="center" sx={{ position: 'absolute', bottom: 0, left: 0 }}>
+      {'Copyright © 2016-'}
+      {new Date().getFullYear()}
+      {' Savoir-faire Linux Inc.'}
+      <Link color="inherit" href="https://jami.net/">
+        Jami.net
+      </Link>{' '}
+      {'.'}
+    </Typography>
+  );
+}
diff --git a/client/src/components/Input.tsx b/client/src/components/Input.tsx
index 47ad45b..e867e26 100644
--- a/client/src/components/Input.tsx
+++ b/client/src/components/Input.tsx
@@ -15,11 +15,22 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
-import { IconButtonProps, Stack, TextField, TextFieldProps } from '@mui/material';
+import { GppMaybe, Warning } from '@mui/icons-material';
+import {
+  IconButtonProps,
+  List,
+  ListItem,
+  ListItemIcon,
+  Stack,
+  TextField,
+  TextFieldProps,
+  Typography,
+} from '@mui/material';
 import { styled } from '@mui/material/styles';
 import { ChangeEvent, ReactElement, useCallback, useEffect, useState } from 'react';
 
 import { InfoButton, ToggleVisibilityButton } from './Button';
+import RulesDialog from './RulesDialog';
 import { CheckedIcon, LockIcon, PenIcon, PersonIcon, RoundSaltireIcon } from './SvgIcon';
 
 const iconsHeight = '16px';
@@ -36,15 +47,17 @@
 const StyledPersonIconLight = styled(PersonIcon)({ height: iconsHeight, color: '#03B9E9' });
 const StyledLockIcon = styled(LockIcon)({ height: iconsHeight, color: '#03B9E9' });
 
-type InputProps = TextFieldProps & {
+export type InputProps = TextFieldProps & {
   infoButtonProps?: IconButtonProps;
   success?: boolean;
+  tooltipTitle: string;
 };
 
-export const UsernameInput = ({ infoButtonProps, onChange: _onChange, ...props }: InputProps) => {
+export const UsernameInput = ({ infoButtonProps, onChange: _onChange, tooltipTitle, ...props }: InputProps) => {
   const [isSelected, setIsSelected] = useState(false);
   const [input, setInput] = useState(props.defaultValue);
   const [startAdornment, setStartAdornment] = useState<ReactElement | undefined>();
+  const [isDialogOpened, setIsDialogOpened] = useState<boolean>(false);
 
   const onChange = useCallback(
     (event: ChangeEvent<HTMLInputElement>) => {
@@ -69,27 +82,35 @@
   }, [props.error, props.success, isSelected, input]);
 
   return (
-    <TextField
-      {...props}
-      label="Choose an identifier"
-      variant="standard"
-      InputLabelProps={{ shrink: !!(isSelected || input) }}
-      onChange={onChange}
-      InputProps={{
-        startAdornment,
-        endAdornment: <InfoButton {...infoButtonProps} />,
-      }}
-      onFocus={() => setIsSelected(true)}
-      onBlur={() => setIsSelected(false)}
-    />
+    <>
+      <RulesDialog openDialog={isDialogOpened} title="Username rules :" closeDialog={() => setIsDialogOpened(false)}>
+        <UsernameRules />
+      </RulesDialog>
+      <TextField
+        {...props}
+        label={'Choose an identifier'}
+        variant="standard"
+        InputLabelProps={{ shrink: !!(isSelected || input) }}
+        onChange={onChange}
+        InputProps={{
+          startAdornment,
+          endAdornment: (
+            <InfoButton tooltipTitle={tooltipTitle} {...infoButtonProps} onClick={() => setIsDialogOpened(true)} />
+          ),
+        }}
+        onFocus={() => setIsSelected(true)}
+        onBlur={() => setIsSelected(false)}
+      />
+    </>
   );
 };
 
-export const PasswordInput = ({ infoButtonProps, onChange: _onChange, ...props }: InputProps) => {
+export const PasswordInput = ({ infoButtonProps, onChange: _onChange, tooltipTitle, ...props }: InputProps) => {
   const [showPassword, setShowPassword] = useState(false);
   const [isSelected, setIsSelected] = useState(false);
   const [input, setInput] = useState(props.defaultValue);
   const [startAdornment, setStartAdornment] = useState<ReactElement | undefined>();
+  const [isDialogOpened, setIsDialogOpened] = useState<boolean>(false);
 
   const toggleShowPassword = () => {
     setShowPassword((showPassword) => !showPassword);
@@ -118,26 +139,31 @@
   }, [props.error, props.success, isSelected, input]);
 
   return (
-    <TextField
-      {...props}
-      label="Password"
-      type={showPassword ? 'text' : 'password'}
-      variant="standard"
-      autoComplete="current-password"
-      InputLabelProps={{ shrink: !!(isSelected || input) }}
-      onChange={onChange}
-      InputProps={{
-        startAdornment,
-        endAdornment: (
-          <Stack direction="row" spacing="14px">
-            <InfoButton {...infoButtonProps} />
-            <ToggleVisibilityButton visible={showPassword} onClick={toggleShowPassword} />
-          </Stack>
-        ),
-      }}
-      onFocus={() => setIsSelected(true)}
-      onBlur={() => setIsSelected(false)}
-    />
+    <>
+      <RulesDialog openDialog={isDialogOpened} title="Password rules :" closeDialog={() => setIsDialogOpened(false)}>
+        <PasswordRules />
+      </RulesDialog>
+      <TextField
+        {...props}
+        label="Password"
+        type={showPassword ? 'text' : 'password'}
+        variant="standard"
+        autoComplete="current-password"
+        InputLabelProps={{ shrink: !!(isSelected || input) }}
+        onChange={onChange}
+        InputProps={{
+          startAdornment,
+          endAdornment: (
+            <Stack direction="row" spacing="14px" alignItems="center">
+              <InfoButton tooltipTitle={tooltipTitle} {...infoButtonProps} onClick={() => setIsDialogOpened(true)} />
+              <ToggleVisibilityButton visible={showPassword} onClick={toggleShowPassword} />
+            </Stack>
+          ),
+        }}
+        onFocus={() => setIsSelected(true)}
+        onBlur={() => setIsSelected(false)}
+      />
+    </>
   );
 };
 
@@ -208,3 +234,82 @@
     />
   );
 };
+
+export function inputColor(
+  error?: boolean,
+  success?: boolean
+): 'success' | 'error' | 'primary' | 'secondary' | 'info' | 'warning' | undefined {
+  return error ? 'error' : success ? 'success' : 'primary';
+}
+
+const PasswordRules = () => {
+  return (
+    <Typography variant="body1">
+      <List>
+        <ListItem>
+          <ListItemIcon>
+            <GppMaybe />
+          </ListItemIcon>
+          The password must contain at least 1 lowercase alphabetical character.
+        </ListItem>
+        <ListItem>
+          <ListItemIcon>
+            <GppMaybe />
+          </ListItemIcon>
+          The password must contain at least 1 uppercase alphabetical character.
+        </ListItem>
+        <ListItem>
+          <ListItemIcon>
+            <GppMaybe />
+          </ListItemIcon>
+          The password must contain at least 1 numeric character.
+        </ListItem>
+        <ListItem>
+          <ListItemIcon>
+            <GppMaybe />
+          </ListItemIcon>
+          The password must contain at least one special character.
+        </ListItem>
+        <ListItem>
+          <ListItemIcon>
+            <GppMaybe />
+          </ListItemIcon>
+          The password must be eight characters or longer for Strong strength.
+        </ListItem>
+      </List>
+    </Typography>
+  );
+};
+
+const UsernameRules = () => {
+  return (
+    <Typography variant="body1">
+      <List>
+        <ListItem>
+          <ListItemIcon>
+            <Warning />
+          </ListItemIcon>
+          The username must be 3 to 32 characters long.
+        </ListItem>
+        <ListItem>
+          <ListItemIcon>
+            <Warning />
+          </ListItemIcon>
+          The username may contain lowercase and uppercase alphabetical characters.
+        </ListItem>
+        <ListItem>
+          <ListItemIcon>
+            <Warning />
+          </ListItemIcon>
+          The username may contain hyphens {'(-)'}.
+        </ListItem>
+        <ListItem>
+          <ListItemIcon>
+            <Warning />
+          </ListItemIcon>
+          The username may contain underscores {'(_)'}.
+        </ListItem>
+      </List>
+    </Typography>
+  );
+};
diff --git a/client/src/components/JamiWelcomeLogo.tsx b/client/src/components/JamiWelcomeLogo.tsx
new file mode 100644
index 0000000..8002aab
--- /dev/null
+++ b/client/src/components/JamiWelcomeLogo.tsx
@@ -0,0 +1,45 @@
+/*
+ * 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 { Box, SxProps, Typography } from '@mui/material';
+
+import { ReactComponent as JamiLogo } from '../icons/jami-logo-icon.svg';
+import { jamiLogoDefaultSize } from '../utils/constants';
+
+interface WelcomeLogoProps {
+  logoWidth?: string;
+  logoHeight?: string;
+  boxSxProps?: SxProps;
+}
+
+export default function JamiWelcomeLogo(props: WelcomeLogoProps) {
+  return (
+    <Box
+      sx={{
+        display: 'flex',
+        alignItems: 'center',
+        justifyContent: 'center',
+        textAlign: 'center',
+        flexDirection: 'column',
+        ...props.boxSxProps,
+      }}
+    >
+      <JamiLogo width={props.logoWidth ?? jamiLogoDefaultSize} height={props.logoHeight ?? jamiLogoDefaultSize} />
+      <Typography variant="h1">Welcome to Jami!</Typography>
+    </Box>
+  );
+}
diff --git a/client/src/components/ProcessingRequest.tsx b/client/src/components/ProcessingRequest.tsx
new file mode 100644
index 0000000..c7da9a4
--- /dev/null
+++ b/client/src/components/ProcessingRequest.tsx
@@ -0,0 +1,27 @@
+/*
+ * 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 Backdrop from '@mui/material/Backdrop';
+import CircularProgress from '@mui/material/CircularProgress';
+
+export default function ProcessingRequest(props: { open: boolean }) {
+  return (
+    <Backdrop sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }} open={props.open} onClick={() => {}}>
+      <CircularProgress color="inherit" />
+    </Backdrop>
+  );
+}
diff --git a/client/src/components/RulesDialog.tsx b/client/src/components/RulesDialog.tsx
new file mode 100644
index 0000000..5e1e929
--- /dev/null
+++ b/client/src/components/RulesDialog.tsx
@@ -0,0 +1,42 @@
+/*
+ * 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 { Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material';
+
+interface RulesDialogProps {
+  openDialog: boolean;
+  title: string;
+  closeDialog: () => void;
+  children: React.ReactNode;
+}
+
+export default function RulesDialog(props: RulesDialogProps) {
+  return (
+    <Dialog open={props.openDialog} onClose={props.closeDialog}>
+      <DialogTitle>
+        {props.title}
+        <br />
+      </DialogTitle>
+      <DialogContent>{props.children}</DialogContent>
+      <DialogActions>
+        <Button onClick={props.closeDialog} autoFocus>
+          Close
+        </Button>
+      </DialogActions>
+    </Dialog>
+  );
+}
diff --git a/client/src/components/Tooltip.tsx b/client/src/components/Tooltip.tsx
new file mode 100644
index 0000000..a6ea84b
--- /dev/null
+++ b/client/src/components/Tooltip.tsx
@@ -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/>.
+ */
+import { Stack } from '@mui/material';
+import { styled } from '@mui/material/styles';
+import Tooltip, { tooltipClasses, TooltipProps } from '@mui/material/Tooltip';
+
+export default styled(({ className, title, ...props }: TooltipProps) => (
+  <Tooltip {...props} classes={{ popper: className }} title={<span style={{ whiteSpace: 'pre-line' }}>{title}</span>}>
+    <Stack>{props.children}</Stack>
+  </Tooltip>
+))(({ theme }) => ({
+  [`& .${tooltipClasses.tooltip}`]: {
+    backgroundColor: theme.InfoTooltip.backgroundColor.main,
+    color: theme.InfoTooltip.color.main,
+    maxWidth: 220,
+    fontSize: theme.typography.pxToRem(15),
+    boxShadow: '3px 3px 7px #00000029',
+    borderRadius: '5px',
+  },
+}));
diff --git a/client/src/components/welcome.jsx b/client/src/components/welcome.jsx
index 39e222b..9f3978d 100644
--- a/client/src/components/welcome.jsx
+++ b/client/src/components/welcome.jsx
@@ -15,10 +15,12 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
-import { Button, Container } from '@mui/material';
+import { Container } from '@mui/material';
 import { AnimatePresence, motion } from 'framer-motion';
 import { useState } from 'react';
 
+import { ReactComponent as JamiLogo } from '../icons/jami-logo-icon.svg';
+
 const list = {
   hidden: { opacity: 0 },
   visible: {
@@ -49,29 +51,15 @@
             onAnimationComplete={(a) => {
               if (a === 'hidden') {
                 props.onComplete();
-              } else if (!props.showSetup) {
+              } else {
                 setVisible(false);
               }
             }}
           >
             <motion.div variants={item}>
-              <img
-                src="/jami-logo-icon.svg"
-                style={{
-                  width: '32',
-                  height: '32',
-                }}
-                alt="jami n/logo"
-              />
+              <JamiLogo width="95%" />
             </motion.div>
             <motion.h1 variants={item}>Welcome to Jami</motion.h1>
-            {props.showSetup && (
-              <motion.div variants={item}>
-                <Button variant="outlined" onClick={() => setVisible(false)}>
-                  Start setup
-                </Button>
-              </motion.div>
-            )}
           </motion.div>
         )}
       </AnimatePresence>
diff --git a/client/public/jami-logo-icon.svg b/client/src/icons/jami-logo-icon.svg
similarity index 100%
rename from client/public/jami-logo-icon.svg
rename to client/src/icons/jami-logo-icon.svg
diff --git a/client/src/pages/LoginDialog.tsx b/client/src/pages/LoginDialog.tsx
index fb01ccd..ee990bc 100644
--- a/client/src/pages/LoginDialog.tsx
+++ b/client/src/pages/LoginDialog.tsx
@@ -22,27 +22,11 @@
 import DialogContent from '@mui/material/DialogContent';
 import DialogTitle from '@mui/material/DialogTitle';
 import FormControlLabel from '@mui/material/FormControlLabel';
-import Link from '@mui/material/Link';
 import TextField from '@mui/material/TextField';
-import Typography from '@mui/material/Typography';
 import { ChangeEvent, Component, MouseEvent } from 'react';
 
 import authManager from '../AuthManager';
 
-function Copyright() {
-  return (
-    <Typography variant="body2" color="textSecondary" align="center">
-      {'Copyright © 2016-'}
-      {new Date().getFullYear()}
-      {' Savoir-faire Linux Inc.'}
-      <Link color="inherit" href="https://jami.net/">
-        Jami.net
-      </Link>{' '}
-      {'.'}
-    </Typography>
-  );
-}
-
 type SignInPageProps = {
   open: boolean;
 };
diff --git a/client/src/themes/Default.ts b/client/src/themes/Default.ts
index 3ba20ab..c0f5884 100644
--- a/client/src/themes/Default.ts
+++ b/client/src/themes/Default.ts
@@ -15,7 +15,23 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
-import { createTheme } from '@mui/material';
+import { createTheme, SimplePaletteColorOptions } from '@mui/material';
+
+declare module '@mui/material/styles' {
+  interface Theme {
+    InfoTooltip: {
+      backgroundColor: SimplePaletteColorOptions;
+      color: SimplePaletteColorOptions;
+    };
+  }
+
+  interface ThemeOptions {
+    InfoTooltip: {
+      backgroundColor: SimplePaletteColorOptions;
+      color: SimplePaletteColorOptions;
+    };
+  }
+}
 
 const theme = createTheme({
   palette: {
@@ -31,6 +47,14 @@
       main: '#009980',
     },
   },
+  InfoTooltip: {
+    backgroundColor: {
+      main: '#FFFFFF',
+    },
+    color: {
+      main: '#292929',
+    },
+  },
   typography: {
     fontFamily: 'Ubuntu',
     allVariants: {
diff --git a/client/src/themes/ThemeDemonstrator.tsx b/client/src/themes/ThemeDemonstrator.tsx
index 8b3ceb1..dd82953 100644
--- a/client/src/themes/ThemeDemonstrator.tsx
+++ b/client/src/themes/ThemeDemonstrator.tsx
@@ -59,7 +59,7 @@
           <TakePictureButton />
         </Stack>
         <Stack direction="row" spacing="5px">
-          <InfoButton />
+          <InfoButton tooltipTitle={''} />
           <TipButton />
         </Stack>
         <Stack direction="row" spacing="5px">
@@ -70,10 +70,10 @@
           <Switch />
         </Stack>
         <Stack padding="5px" width="300px">
-          <UsernameInput />
-          <UsernameInput error defaultValue="Cyrille" />
-          <PasswordInput />
-          <PasswordInput error defaultValue="SavoirFaireLinux" />
+          <UsernameInput onChange={() => {}} tooltipTitle={'Test'} />
+          <UsernameInput onChange={() => {}} tooltipTitle={'Test'} error defaultValue="Cyrille" />
+          <PasswordInput onChange={() => {}} tooltipTitle={'Test'} />
+          <PasswordInput onChange={() => {}} tooltipTitle={'Test'} error defaultValue="SavoirFaireLinux" />
           <NickNameInput />
           <RegularInput />
         </Stack>