Uniformize styles for dialogs and popovers, add ContextMenu

Change-Id: I8687b2d171f9c15e8eb8dd5ba2a32cdaa27b70d6
diff --git a/client/src/components/ConversationListItem.tsx b/client/src/components/ConversationListItem.tsx
index 2f07076..2861eb8 100644
--- a/client/src/components/ConversationListItem.tsx
+++ b/client/src/components/ConversationListItem.tsx
@@ -15,28 +15,20 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
-import {
-  Box,
-  ListItem,
-  ListItemAvatar,
-  ListItemIcon,
-  ListItemText,
-  Menu,
-  MenuItem,
-  Stack,
-  Typography,
-} from '@mui/material';
+import { Box, ListItem, ListItemAvatar, ListItemText } from '@mui/material';
 import { Conversation } from 'jami-web-common';
 import { QRCodeCanvas } from 'qrcode.react';
-import { MouseEvent, useState } from 'react';
+import { useCallback, useMemo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
-import Modal from 'react-modal';
 import { useNavigate, useParams } from 'react-router-dom';
 
 import { useAuthContext } from '../contexts/AuthProvider';
 import { setRefreshFromSlice } from '../redux/appSlice';
 import { useAppDispatch } from '../redux/hooks';
+import ContextMenu, { ContextMenuHandler, useContextMenuHandler } from './ContextMenu';
 import ConversationAvatar from './ConversationAvatar';
+import { ConfirmationDialog, DialogContentList, InfosDialog, useDialogHandler } from './Dialog';
+import { PopoverListItemData } from './PopoverList';
 import {
   AudioCallIcon,
   BlockContactIcon,
@@ -47,75 +39,58 @@
   VideoCallIcon,
 } from './SvgIcon';
 
-const cancelStyles: Modal.Styles = {
-  content: {
-    left: '94px',
-    width: '300px',
-    height: '220px',
-    background: '#FFFFFF 0% 0% no-repeat padding-box',
-    boxShadow: '3px 3px 7px #00000029',
-    borderRadius: '20px',
-    opacity: '1',
-
-    textAlign: 'left',
-    font: 'normal normal normal 12px/26px Ubuntu',
-    letterSpacing: '0px',
-    color: '#000000',
-  },
-};
-
-const contactDetailsStyles: Modal.Styles = {
-  content: {
-    left: '94px',
-    width: '450px',
-    height: '450px',
-    background: '#FFFFFF 0% 0% no-repeat padding-box',
-    boxShadow: '3px 3px 7px #00000029',
-    borderRadius: '20px',
-    opacity: '1',
-
-    textAlign: 'left',
-    font: 'normal normal normal 12px/26px Ubuntu',
-    letterSpacing: '0px',
-    color: '#000000',
-  },
-};
-
-const iconColor = '#005699';
-
 type ConversationListItemProps = {
   conversation: Conversation;
 };
 
 export default function ConversationListItem({ conversation }: ConversationListItemProps) {
-  const { axiosInstance } = useAuthContext();
   const { conversationId, contactId } = useParams();
-  const dispatch = useAppDispatch();
+  const contextMenuHandler = useContextMenuHandler();
 
   const pathId = conversationId || contactId;
   const isSelected = conversation.getDisplayUri() === pathId;
   const navigate = useNavigate();
-  const { t } = useTranslation();
-
-  const [menuAnchorEl, setMenuAnchorEl] = useState<HTMLElement | null>(null);
-  const [modalDetailsIsOpen, setModalDetailsIsOpen] = useState(false);
-  const [modalDeleteIsOpen, setModalDeleteIsOpen] = useState(false);
-  const [blockOrRemove, setBlockOrRemove] = useState(true);
   const [userId] = useState(conversation?.getFirstMember()?.contact.getUri());
+  const uri = conversation.getId() ? `/conversation/${conversation.getId()}` : `/add-contact/${userId}`;
+  return (
+    <Box onContextMenu={contextMenuHandler.handleAnchorPosition}>
+      <ConversationMenu
+        userId={userId}
+        conversation={conversation}
+        uri={uri}
+        isSelected={isSelected}
+        contextMenuProps={contextMenuHandler.props}
+      />
+      <ListItem button alignItems="flex-start" selected={isSelected} onClick={() => navigate(uri)}>
+        <ListItemAvatar>
+          <ConversationAvatar displayName={conversation.getDisplayNameNoFallback()} />
+        </ListItemAvatar>
+        <ListItemText primary={conversation.getDisplayName()} secondary={conversation.getDisplayUri()} />
+      </ListItem>
+    </Box>
+  );
+}
+
+interface ConversationMenuProps {
+  userId: string;
+  conversation: Conversation;
+  uri: string;
+  isSelected: boolean;
+  contextMenuProps: ContextMenuHandler['props'];
+}
+
+const ConversationMenu = ({ userId, conversation, uri, isSelected, contextMenuProps }: ConversationMenuProps) => {
+  const { t } = useTranslation();
+  const { axiosInstance } = useAuthContext();
   const [isSwarm] = useState(true);
 
-  const openMenu = (e: MouseEvent<HTMLDivElement>) => {
-    e.preventDefault();
-    console.log(e);
-    setMenuAnchorEl(e.currentTarget);
-  };
-  const openModalDetails = () => setModalDetailsIsOpen(true);
-  const openModalDelete = () => setModalDeleteIsOpen(true);
-  const closeModal = () => setMenuAnchorEl(null);
-  const closeModalDetails = () => setModalDetailsIsOpen(false);
-  const closeModalDelete = () => setModalDeleteIsOpen(false);
+  const detailsDialogHandler = useDialogHandler();
+  const blockContactDialogHandler = useDialogHandler();
+  const removeContactDialogHandler = useDialogHandler();
 
-  const getContactDetails = async () => {
+  const navigate = useNavigate();
+
+  const getContactDetails = useCallback(async () => {
     const controller = new AbortController();
     try {
       const { data } = await axiosInstance.get(`/contacts/${userId}`, {
@@ -125,274 +100,210 @@
     } catch (e) {
       console.log('ERROR GET CONTACT DETAILS: ', e);
     }
-  };
+  }, [axiosInstance, userId]);
 
-  const removeOrBlock = async (block = false) => {
-    setBlockOrRemove(false);
+  const menuOptions: PopoverListItemData[] = useMemo(
+    () => [
+      {
+        label: t('conversation_message'),
+        Icon: MessageIcon,
+        onClick: () => {
+          navigate(uri);
+        },
+      },
+      {
+        label: t('conversation_start_audiocall'),
+        Icon: AudioCallIcon,
+        onClick: () => {
+          navigate(`/account/call/${conversation.getId()}`);
+        },
+      },
+      {
+        label: t('conversation_start_videocall'),
+        Icon: VideoCallIcon,
+        onClick: () => {
+          navigate(`call/${conversation.getId()}?video=true`);
+        },
+      },
+      ...(isSelected
+        ? [
+            {
+              label: t('conversation_close'),
+              Icon: CancelIcon,
+              onClick: () => {
+                navigate(`/`);
+              },
+            },
+          ]
+        : []),
+      {
+        label: t('conversation_details'),
+        Icon: ContactDetailsIcon,
+        onClick: () => {
+          detailsDialogHandler.openDialog();
+          getContactDetails();
+        },
+      },
+      {
+        label: t('conversation_block'),
+        Icon: BlockContactIcon,
+        onClick: () => {
+          blockContactDialogHandler.openDialog();
+        },
+      },
+      {
+        label: t('conversation_delete'),
+        Icon: RemoveContactIcon,
+        onClick: () => {
+          removeContactDialogHandler.openDialog();
+        },
+      },
+    ],
+    [
+      conversation,
+      navigate,
+      uri,
+      isSelected,
+      getContactDetails,
+      detailsDialogHandler,
+      blockContactDialogHandler,
+      removeContactDialogHandler,
+      t,
+    ]
+  );
 
+  return (
+    <>
+      <ContextMenu {...contextMenuProps} items={menuOptions} />
+
+      <DetailsDialog {...detailsDialogHandler.props} userId={userId} conversation={conversation} isSwarm={isSwarm} />
+
+      <RemoveContactDialog {...removeContactDialogHandler.props} userId={userId} conversation={conversation} />
+
+      <BlockContactDialog {...blockContactDialogHandler.props} userId={userId} conversation={conversation} />
+    </>
+  );
+};
+
+interface DetailsDialogProps {
+  userId: string;
+  conversation: Conversation;
+  open: boolean;
+  onClose: () => void;
+  isSwarm: boolean;
+}
+
+const DetailsDialog = ({ userId, conversation, open, onClose, isSwarm }: DetailsDialogProps) => {
+  const { t } = useTranslation();
+  const items = useMemo(
+    () => [
+      {
+        label: t('conversation_details_username'),
+        value: conversation.getDisplayNameNoFallback(),
+      },
+      {
+        label: t('conversation_details_identifier'),
+        value: userId,
+      },
+      {
+        label: t('conversation_details_qr_code'),
+        value: <QRCodeCanvas size={80} value={`${userId}`} />,
+      },
+      {
+        label: t('conversation_details_is_swarm'),
+        value: isSwarm ? t('conversation_details_is_swarm_true') : t('conversation_details_is_swarm_false'),
+      },
+    ],
+    [userId, conversation, isSwarm, t]
+  );
+  return (
+    <InfosDialog
+      open={open}
+      onClose={onClose}
+      icon={
+        <ConversationAvatar
+          sx={{ width: 'inherit', height: 'inherit' }}
+          displayName={conversation.getDisplayNameNoFallback()}
+        />
+      }
+      title={conversation.getDisplayNameNoFallback() || ''}
+      content={<DialogContentList title={t('conversation_details_informations')} items={items} />}
+    />
+  );
+};
+
+interface BlockContactDialogProps {
+  userId: string;
+  conversation: Conversation;
+  open: boolean;
+  onClose: () => void;
+}
+
+const BlockContactDialog = ({ userId, open, onClose }: BlockContactDialogProps) => {
+  const { axiosInstance } = useAuthContext();
+  const { t } = useTranslation();
+  const dispatch = useAppDispatch();
+
+  const block = async () => {
     const controller = new AbortController();
-    let url = `/contacts/${userId}`;
-    if (block) {
-      url += '/block';
-    }
     try {
-      await axiosInstance(url, {
+      await axiosInstance.post(`/contacts/${userId}/block`, {
         signal: controller.signal,
-        method: block ? 'POST' : 'DELETE',
       });
       dispatch(setRefreshFromSlice());
     } catch (e) {
-      console.error(`Error ${block ? 'blocking' : 'removing'} contact : `, e);
+      console.error(`Error $block contact : `, e);
       dispatch(setRefreshFromSlice());
     }
-    closeModalDelete();
+    onClose();
   };
 
-  const uri = conversation.getId() ? `/conversation/${conversation.getId()}` : `/add-contact/${userId}`;
   return (
-    <div onContextMenu={openMenu}>
-      <div>
-        <Menu open={!!menuAnchorEl} onClose={closeModal} anchorEl={menuAnchorEl}>
-          <MenuItem
-            onClick={() => {
-              navigate(uri);
-              closeModal();
-            }}
-          >
-            <ListItemIcon>
-              <MessageIcon style={{ color: iconColor }} />
-            </ListItemIcon>
-            <ListItemText>{t('conversation_message')}</ListItemText>
-          </MenuItem>
-          <MenuItem
-            onClick={() => {
-              navigate(`/account/call/${conversation.getId()}`);
-            }}
-          >
-            <ListItemIcon>
-              <AudioCallIcon style={{ color: iconColor }} />
-            </ListItemIcon>
-            <ListItemText>{t('conversation_start_audiocall')}</ListItemText>
-          </MenuItem>
-
-          <MenuItem
-            onClick={() => {
-              navigate(`call/${conversation.getId()}?video=true`);
-            }}
-          >
-            <ListItemIcon>
-              <VideoCallIcon style={{ color: iconColor }} />
-            </ListItemIcon>
-            <ListItemText>{t('conversation_start_videocall')}</ListItemText>
-          </MenuItem>
-
-          {isSelected && (
-            <MenuItem
-              onClick={() => {
-                navigate(`/`);
-                closeModal();
-              }}
-            >
-              <ListItemIcon>
-                <CancelIcon style={{ color: iconColor }} />
-              </ListItemIcon>
-              <ListItemText>{t('conversation_close')}</ListItemText>
-            </MenuItem>
-          )}
-
-          <MenuItem
-            onClick={() => {
-              console.log('open details contact for: ');
-              closeModal();
-              openModalDetails();
-              getContactDetails();
-            }}
-          >
-            <ListItemIcon>
-              <ContactDetailsIcon style={{ color: iconColor }} />
-            </ListItemIcon>
-            <ListItemText>{t('conversation_details')}</ListItemText>
-          </MenuItem>
-
-          <MenuItem
-            onClick={() => {
-              setBlockOrRemove(true);
-              closeModal();
-              openModalDelete();
-            }}
-          >
-            <ListItemIcon>
-              <BlockContactIcon style={{ color: iconColor }} />
-            </ListItemIcon>
-            <ListItemText>{t('conversation_block_contact')}</ListItemText>
-          </MenuItem>
-
-          <MenuItem
-            onClick={() => {
-              setBlockOrRemove(false);
-              closeModal();
-              openModalDelete();
-            }}
-          >
-            <ListItemIcon>
-              <RemoveContactIcon style={{ color: iconColor }} />
-            </ListItemIcon>
-            <ListItemText>{t('conversation_delete_contact')}</ListItemText>
-          </MenuItem>
-        </Menu>
-      </div>
-
-      <div>
-        <Modal
-          isOpen={modalDetailsIsOpen}
-          onRequestClose={closeModalDetails}
-          style={contactDetailsStyles}
-          contentLabel="Détails contact"
-        >
-          <Stack direction={'row'} alignContent="flex-end">
-            <Stack direction={'column'}>
-              <div style={{ height: '100px' }}>
-                <ConversationAvatar displayName={conversation.getDisplayNameNoFallback()} />
-              </div>
-
-              <div
-                style={{
-                  fontSize: '20px',
-                  marginBottom: '20px',
-                  height: '20px',
-                }}
-              >
-                Informations
-              </div>
-
-              <Typography variant="caption">Nom d&apos;utilisateur</Typography>
-              <div style={{ height: '20px' }} />
-              <Typography variant="caption">Identifiant </Typography>
-              <div style={{ height: '20px' }} />
-
-              <div
-                style={{
-                  flex: 1,
-                  height: '150px',
-                  flexDirection: 'column',
-                  // alignSelf: "flex-end",
-                }}
-              >
-                <Typography variant="caption">Code QR</Typography>
-              </div>
-
-              <Typography variant="caption">est un swarm </Typography>
-            </Stack>
-
-            <Stack direction={'column'}>
-              <div
-                style={{
-                  fontWeight: 'bold',
-                  fontSize: '20px',
-                  height: '100px',
-                }}
-              >
-                {conversation.getDisplayNameNoFallback() + '(resolved name)'}
-              </div>
-
-              <div
-                style={{
-                  height: '40px',
-                }}
-              />
-              <Typography variant="caption">
-                <div style={{ fontWeight: 'bold' }}>{conversation.getDisplayNameNoFallback()}</div>
-              </Typography>
-
-              <div style={{ height: '20px' }} />
-
-              <Typography variant="caption">
-                <div style={{ fontWeight: 'bold' }}> {userId}</div>
-              </Typography>
-
-              <div style={{ height: '20px' }} />
-
-              <div>
-                <QRCodeCanvas size={40} value={`${userId}`} />
-              </div>
-
-              <Typography variant="caption">
-                <div style={{ fontWeight: 'bold' }}> {String(isSwarm)}</div>
-              </Typography>
-            </Stack>
-          </Stack>
-          <div
-            onClick={closeModalDetails}
-            style={{
-              width: '100px',
-              borderStyle: 'solid',
-              textAlign: 'center',
-              borderRadius: '5px',
-              marginLeft: '150px',
-              marginTop: '10px',
-            }}
-          >
-            <Typography variant="caption">Fermer</Typography>
-          </div>
-        </Modal>
-      </div>
-
-      <div>
-        <Modal
-          isOpen={modalDeleteIsOpen}
-          onRequestClose={closeModalDelete}
-          style={cancelStyles}
-          contentLabel="Merci de confirmer"
-        >
-          <Typography variant="h4">Merci de confirmer</Typography>
-          <Stack direction={'column'} justifyContent="space-around" spacing={'75px'}>
-            <div style={{ textAlign: 'center', marginTop: '10%' }}>
-              <Typography variant="body2">
-                Voulez vous vraiment {blockOrRemove ? 'bloquer' : 'supprimer'} ce contact?
-              </Typography>
-            </div>
-
-            <Stack direction={'row'} top={'25px'} alignSelf="center" spacing={1}>
-              <Box
-                onClick={() => {
-                  if (blockOrRemove) removeOrBlock(true);
-                  else removeOrBlock(false);
-                }}
-                style={{
-                  width: '100px',
-                  textAlign: 'center',
-                  borderStyle: 'solid',
-                  borderColor: 'red',
-                  borderRadius: '10px',
-                  color: 'red',
-                }}
-              >
-                {blockOrRemove ? 'Bloquer' : 'Supprimer'}
-              </Box>
-              <Box
-                onClick={closeModalDelete}
-                style={{
-                  width: '100px',
-                  textAlign: 'center',
-                  paddingLeft: '12px',
-                  paddingRight: '12px',
-                  borderStyle: 'solid',
-                  borderRadius: '10px',
-                }}
-              >
-                Annuler
-              </Box>
-            </Stack>
-          </Stack>
-        </Modal>
-      </div>
-
-      <ListItem button alignItems="flex-start" selected={isSelected} onClick={() => navigate(uri)}>
-        <ListItemAvatar>
-          <ConversationAvatar displayName={conversation.getDisplayNameNoFallback()} />
-        </ListItemAvatar>
-        <ListItemText primary={conversation.getDisplayName()} secondary={conversation.getDisplayUri()} />
-      </ListItem>
-    </div>
+    <ConfirmationDialog
+      open={open}
+      onClose={onClose}
+      title={t('dialog_confirm_title_default')}
+      content={t('conversation_ask_confirm_block')}
+      onConfirm={block}
+      confirmButtonText={t('conversation_confirm_block')}
+    />
   );
+};
+
+interface RemoveContactDialogProps {
+  userId: string;
+  conversation: Conversation;
+  open: boolean;
+  onClose: () => void;
 }
+
+const RemoveContactDialog = ({ userId, open, onClose }: RemoveContactDialogProps) => {
+  const { axiosInstance } = useAuthContext();
+  const { t } = useTranslation();
+  const dispatch = useAppDispatch();
+
+  const remove = async () => {
+    const controller = new AbortController();
+    try {
+      await axiosInstance.delete(`/contacts/${userId}/remove`, {
+        signal: controller.signal,
+      });
+      dispatch(setRefreshFromSlice());
+    } catch (e) {
+      console.error(`Error removing contact : `, e);
+      dispatch(setRefreshFromSlice());
+    }
+    onClose();
+  };
+
+  return (
+    <ConfirmationDialog
+      open={open}
+      onClose={onClose}
+      title={t('dialog_confirm_title_default')}
+      content={t('conversation_ask_confirm_remove')}
+      onConfirm={remove}
+      confirmButtonText={t('conversation_confirm_remove')}
+    />
+  );
+};