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'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')}
+ />
+ );
+};