Uniformize styles for dialogs and popovers, add ContextMenu
Change-Id: I8687b2d171f9c15e8eb8dd5ba2a32cdaa27b70d6
diff --git a/client/src/components/ContextMenu.tsx b/client/src/components/ContextMenu.tsx
new file mode 100644
index 0000000..6ac1363
--- /dev/null
+++ b/client/src/components/ContextMenu.tsx
@@ -0,0 +1,80 @@
+/*
+ * 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 { Menu, MenuProps, PopoverPosition, PopoverReference, styled } from '@mui/material';
+import { MouseEventHandler, useCallback, useMemo, useState } from 'react';
+
+import PopoverList, { PopoverListItemData } from './PopoverList';
+
+export interface ContextMenuHandler {
+ props: {
+ open: boolean;
+ onClose: () => void;
+ anchorPosition: PopoverPosition | undefined;
+ anchorReference: PopoverReference | undefined;
+ };
+ handleAnchorPosition: MouseEventHandler;
+}
+
+export const useContextMenuHandler = (): ContextMenuHandler => {
+ const [anchorPosition, setAnchorPosition] = useState<PopoverPosition | undefined>(undefined);
+
+ const handleAnchorPosition = useCallback<MouseEventHandler>(
+ (event) => {
+ event.preventDefault();
+ setAnchorPosition((anchorPosition) =>
+ anchorPosition === undefined ? { top: event.clientY, left: event.clientX } : undefined
+ );
+ },
+ [setAnchorPosition]
+ );
+
+ const onClose = useCallback(() => setAnchorPosition(undefined), [setAnchorPosition]);
+
+ return useMemo(
+ () => ({
+ props: {
+ open: !!anchorPosition,
+ onClose,
+ anchorPosition,
+ anchorReference: 'anchorPosition',
+ },
+ handleAnchorPosition,
+ }),
+ [anchorPosition, handleAnchorPosition, onClose]
+ );
+};
+
+interface ContextMenuProps extends MenuProps {
+ items: PopoverListItemData[];
+}
+
+const ContextMenu = styled(({ items, ...props }: ContextMenuProps) => (
+ <Menu {...props}>
+ <PopoverList items={items} onClose={props.onClose} />
+ </Menu>
+))(() => ({
+ '& .MuiPaper-root': {
+ borderRadius: '5px 20px 20px 20px',
+ boxShadow: '3px 3px 7px #00000029',
+ },
+ '& .MuiMenu-list': {
+ padding: '0px',
+ },
+}));
+
+export default ContextMenu;
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')}
+ />
+ );
+};
diff --git a/client/src/components/Dialog.tsx b/client/src/components/Dialog.tsx
new file mode 100644
index 0000000..9a6790d
--- /dev/null
+++ b/client/src/components/Dialog.tsx
@@ -0,0 +1,161 @@
+/*
+ * 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, Button, List, ListItem, ListItemIcon, Stack, SvgIconProps, Typography } from '@mui/material';
+import Dialog from '@mui/material/Dialog';
+import DialogActions from '@mui/material/DialogActions';
+import DialogContent from '@mui/material/DialogContent';
+import DialogTitle from '@mui/material/DialogTitle';
+import { ComponentType, ReactNode, useCallback, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+
+interface DialogHandler {
+ props: {
+ open: boolean;
+ onClose: () => void;
+ };
+ openDialog: () => void;
+}
+
+export const useDialogHandler = (): DialogHandler => {
+ const [open, setOpen] = useState(false);
+
+ const onClose = useCallback(() => {
+ setOpen(false);
+ }, []);
+
+ const openDialog = useCallback(() => {
+ setOpen(true);
+ }, []);
+
+ return useMemo(
+ () => ({
+ props: { open, onClose },
+ openDialog,
+ }),
+ [open, onClose, openDialog]
+ );
+};
+
+interface BaseDialogProps {
+ open: boolean;
+ onClose: () => void;
+ icon?: ReactNode;
+ title: string;
+ content: ReactNode;
+ actions: ReactNode;
+}
+
+export const BaseDialog = ({ open, onClose, icon, title, content, actions }: BaseDialogProps) => {
+ return (
+ <Dialog open={open} onClose={onClose}>
+ <DialogTitle>
+ <Stack direction="row" alignItems="center" spacing="16px">
+ {icon && (
+ <Box height="80px" width="80px">
+ {icon}
+ </Box>
+ )}
+ <Box>
+ <Typography variant="h2">{title}</Typography>
+ </Box>
+ </Stack>
+ </DialogTitle>
+ <DialogContent>{content}</DialogContent>
+ <DialogActions>{actions}</DialogActions>
+ </Dialog>
+ );
+};
+
+type InfosDialogProps = Omit<BaseDialogProps, 'actions'>;
+
+export const InfosDialog = (props: InfosDialogProps) => {
+ const { t } = useTranslation();
+ return (
+ <BaseDialog
+ {...props}
+ actions={
+ <Button onClick={props.onClose} autoFocus>
+ {t('dialog_close')}
+ </Button>
+ }
+ />
+ );
+};
+
+interface ConfirmationDialogProps extends Omit<BaseDialogProps, 'actions'> {
+ onConfirm: () => void;
+ confirmButtonText: string;
+}
+
+export const ConfirmationDialog = ({ onConfirm, confirmButtonText, ...props }: ConfirmationDialogProps) => {
+ const { t } = useTranslation();
+ props.title = props.title || t('dialog_confirm_title_default');
+
+ return (
+ <BaseDialog
+ {...props}
+ actions={
+ <>
+ <Button onClick={onConfirm}>{confirmButtonText}</Button>
+ <Button onClick={props.onClose}>{t('dialog_cancel')}</Button>
+ </>
+ }
+ />
+ );
+};
+
+interface DialogContentListItem {
+ Icon?: ComponentType<SvgIconProps>;
+ label?: string;
+ value: ReactNode;
+}
+
+interface DialogContentListProps {
+ title?: string;
+ items: DialogContentListItem[];
+}
+
+export const DialogContentList = ({ title, items }: DialogContentListProps) => {
+ return (
+ <List subheader={<Typography variant="h3">{title}</Typography>}>
+ {items.map(({ Icon, label, value }, index) => (
+ <ListItem key={index}>
+ {Icon && (
+ <ListItemIcon>
+ <Icon />
+ </ListItemIcon>
+ )}
+ <Stack direction="row" alignItems="center" spacing="24px">
+ {label && (
+ <Stack direction="row" width="100px" justifyContent="end">
+ <Typography variant="body2" color="#a0a0a0">
+ {label}
+ </Typography>
+ </Stack>
+ )}
+ <Box>
+ <Typography variant="body2" sx={{ fontWeight: 'bold' }}>
+ {value}
+ </Typography>
+ </Box>
+ </Stack>
+ </ListItem>
+ ))}
+ </List>
+ );
+};
diff --git a/client/src/components/Input.tsx b/client/src/components/Input.tsx
index b21b2d2..ec840c4 100644
--- a/client/src/components/Input.tsx
+++ b/client/src/components/Input.tsx
@@ -16,23 +16,14 @@
* <https://www.gnu.org/licenses/>.
*/
import { GppMaybe, Warning } from '@mui/icons-material';
-import {
- IconButtonProps,
- List,
- ListItem,
- ListItemIcon,
- Stack,
- TextField,
- TextFieldProps,
- Typography,
-} from '@mui/material';
+import { IconButtonProps, Stack, TextField, TextFieldProps } from '@mui/material';
import { styled } from '@mui/material/styles';
-import { ChangeEvent, ReactElement, useCallback, useEffect, useState } from 'react';
+import { ChangeEvent, ReactElement, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { StrengthValueCode } from '../utils/auth';
import { InfoButton, ToggleVisibilityButton } from './Button';
-import RulesDialog from './RulesDialog';
+import { DialogContentList, InfosDialog, useDialogHandler } from './Dialog';
import { CheckedIcon, LockIcon, PenIcon, PersonIcon, RoundSaltireIcon } from './SvgIcon';
const iconsHeight = '16px';
@@ -71,7 +62,7 @@
const [isSelected, setIsSelected] = useState(false);
const [input, setInput] = useState(props.defaultValue);
const [startAdornment, setStartAdornment] = useState<ReactElement | undefined>();
- const [isDialogOpened, setIsDialogOpened] = useState<boolean>(false);
+ const dialogHandler = useDialogHandler();
const onChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
@@ -105,13 +96,7 @@
return (
<>
- <RulesDialog
- openDialog={isDialogOpened}
- title={t('username_rules_dialog_title')}
- closeDialog={() => setIsDialogOpened(false)}
- >
- <UsernameRules />
- </RulesDialog>
+ <InfosDialog {...dialogHandler.props} title={t('username_rules_dialog_title')} content={<UsernameRules />} />
<TextField
color={inputColor(props.error, success)}
label={t('username_input_label')}
@@ -128,7 +113,7 @@
InputProps={{
startAdornment,
endAdornment: (
- <InfoButton tooltipTitle={tooltipTitle} {...infoButtonProps} onClick={() => setIsDialogOpened(true)} />
+ <InfoButton tooltipTitle={tooltipTitle} {...infoButtonProps} onClick={dialogHandler.openDialog} />
),
...props.InputProps,
}}
@@ -150,7 +135,7 @@
const [isSelected, setIsSelected] = useState(false);
const [input, setInput] = useState(props.defaultValue);
const [startAdornment, setStartAdornment] = useState<ReactElement | undefined>();
- const [isDialogOpened, setIsDialogOpened] = useState<boolean>(false);
+ const dialogHandler = useDialogHandler();
const toggleShowPassword = () => {
setShowPassword((showPassword) => !showPassword);
@@ -189,13 +174,7 @@
return (
<>
- <RulesDialog
- openDialog={isDialogOpened}
- title={t('password_rules_dialog_title')}
- closeDialog={() => setIsDialogOpened(false)}
- >
- <PasswordRules />
- </RulesDialog>
+ <InfosDialog {...dialogHandler.props} title={t('password_rules_dialog_title')} content={<PasswordRules />} />
<TextField
color={inputColor(props.error, success)}
label={t('password_input_label')}
@@ -212,7 +191,7 @@
startAdornment,
endAdornment: (
<Stack direction="row" spacing="14px" alignItems="center">
- <InfoButton tooltipTitle={tooltipTitle} {...infoButtonProps} onClick={() => setIsDialogOpened(true)} />
+ <InfoButton tooltipTitle={tooltipTitle} {...infoButtonProps} onClick={dialogHandler.openDialog} />
<ToggleVisibilityButton visible={showPassword} onClick={toggleShowPassword} />
</Stack>
),
@@ -300,72 +279,56 @@
const PasswordRules = () => {
const { t } = useTranslation();
-
- return (
- <List>
- <ListItem>
- <ListItemIcon>
- <GppMaybe />
- </ListItemIcon>
- <Typography variant="body1">{t('password_rule_one')}</Typography>
- </ListItem>
- <ListItem>
- <ListItemIcon>
- <GppMaybe />
- </ListItemIcon>
- <Typography variant="body1">{t('password_rule_two')}</Typography>
- </ListItem>
- <ListItem>
- <ListItemIcon>
- <GppMaybe />
- </ListItemIcon>
- <Typography variant="body1">{t('password_rule_three')}</Typography>
- </ListItem>
- <ListItem>
- <ListItemIcon>
- <GppMaybe />
- </ListItemIcon>
- <Typography variant="body1">{t('password_rule_four')}</Typography>
- </ListItem>
- <ListItem>
- <ListItemIcon>
- <GppMaybe />
- </ListItemIcon>
- <Typography variant="body1">{t('password_rule_five')}</Typography>
- </ListItem>
- </List>
+ const items = useMemo(
+ () => [
+ {
+ Icon: GppMaybe,
+ value: t('password_rule_one'),
+ },
+ {
+ Icon: GppMaybe,
+ value: t('password_rule_two'),
+ },
+ {
+ Icon: GppMaybe,
+ value: t('password_rule_three'),
+ },
+ {
+ Icon: GppMaybe,
+ value: t('password_rule_four'),
+ },
+ {
+ Icon: GppMaybe,
+ value: t('password_rule_five'),
+ },
+ ],
+ [t]
);
+ return <DialogContentList items={items} />;
};
const UsernameRules = () => {
const { t } = useTranslation();
-
- return (
- <List>
- <ListItem>
- <ListItemIcon>
- <Warning />
- </ListItemIcon>
- <Typography variant="body1">{t('username_rule_one')}</Typography>
- </ListItem>
- <ListItem>
- <ListItemIcon>
- <Warning />
- </ListItemIcon>
- <Typography variant="body1">{t('username_rule_two')}</Typography>
- </ListItem>
- <ListItem>
- <ListItemIcon>
- <Warning />
- </ListItemIcon>
- <Typography variant="body1">{t('username_rule_three')}</Typography>
- </ListItem>
- <ListItem>
- <ListItemIcon>
- <Warning />
- </ListItemIcon>
- <Typography variant="body1">{t('username_rule_four')}</Typography>
- </ListItem>
- </List>
+ const items = useMemo(
+ () => [
+ {
+ Icon: Warning,
+ value: t('username_rule_one'),
+ },
+ {
+ Icon: Warning,
+ value: t('username_rule_two'),
+ },
+ {
+ Icon: Warning,
+ value: t('username_rule_three'),
+ },
+ {
+ Icon: Warning,
+ value: t('username_rule_four'),
+ },
+ ],
+ [t]
);
+ return <DialogContentList items={items} />;
};
diff --git a/client/src/components/Message.tsx b/client/src/components/Message.tsx
index 6b2fad8..7cf7225 100644
--- a/client/src/components/Message.tsx
+++ b/client/src/components/Message.tsx
@@ -15,18 +15,7 @@
* License along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
-import {
- Box,
- Chip,
- Divider,
- List,
- ListItemButton,
- ListItemText,
- Stack,
- Theme,
- Tooltip,
- Typography,
-} from '@mui/material';
+import { Box, Chip, Divider, Stack, Tooltip, Typography } from '@mui/material';
import { styled } from '@mui/material/styles';
import { Dayjs } from 'dayjs';
import { Account, Contact, Message } from 'jami-web-common';
@@ -36,6 +25,7 @@
import dayjs from '../dayjsInitializer';
import { EmojiButton, MoreButton, ReplyMessageButton } from './Button';
import ConversationAvatar from './ConversationAvatar';
+import PopoverList, { PopoverListItemData } from './PopoverList';
import {
ArrowLeftCurved,
ArrowLeftDown,
@@ -408,21 +398,21 @@
const MessageTooltip = styled(({ className, position, children }: MessageTooltipProps) => {
const [open, setOpen] = useState(false);
const emojis = ['😎', '😄', '😍']; // Should be last three used emojis
- const additionalOptions = [
+ const additionalOptions: PopoverListItemData[] = [
{
Icon: TwoSheetsIcon,
- text: 'Copy',
- action: () => {},
+ label: 'Copy',
+ onClick: () => {},
},
{
Icon: OppositeArrowsIcon,
- text: 'Transfer',
- action: () => {},
+ label: 'Transfer',
+ onClick: () => {},
},
{
Icon: TrashBinIcon,
- text: 'Delete message',
- action: () => {},
+ label: 'Delete message',
+ onClick: () => {},
},
];
@@ -449,10 +439,10 @@
onClose={onClose}
title={
<Stack>
- {/* Whole tooltip's content */}
<Stack // Main options
direction="row"
spacing="16px"
+ padding="16px"
>
{emojis.map((emoji) => (
<EmojiButton key={emoji} emoji={emoji} />
@@ -460,43 +450,10 @@
<ReplyMessageButton />
<MoreButton onClick={toggleMoreMenu} />
</Stack>
- {open && ( // Additional menu options
+ {open && (
<>
- <Divider sx={{ paddingTop: '16px' }} />
- <List sx={{ padding: 0, paddingTop: '8px', marginBottom: '-8px' }}>
- {additionalOptions.map((option) => (
- <ListItemButton
- key={option.text}
- sx={{
- padding: '8px',
- }}
- >
- <Stack // Could not find proper way to set spacing between ListItemIcon and ListItemText
- direction="row"
- spacing="16px"
- >
- <option.Icon
- sx={{
- height: '16px',
- margin: 0,
- color: (theme: Theme) => theme?.palette?.primary?.dark,
- }}
- />
- <ListItemText
- primary={option.text}
- primaryTypographyProps={{
- fontSize: '12px',
- lineHeight: '16px',
- }}
- sx={{
- height: '16px',
- margin: 0,
- }}
- />
- </Stack>
- </ListItemButton>
- ))}
- </List>
+ <Divider sx={{ marginX: '16px' }} />
+ <PopoverList items={additionalOptions} />
</>
)}
</Stack>
@@ -511,11 +468,12 @@
const smallRadius = '5px';
return {
backgroundColor: 'white',
- padding: '16px',
+ padding: '0px',
boxShadow: '3px 3px 7px #00000029',
borderRadius: largeRadius,
borderStartStartRadius: position === 'start' ? smallRadius : largeRadius,
borderStartEndRadius: position === 'end' ? smallRadius : largeRadius,
+ overflow: 'hidden',
};
});
diff --git a/client/src/components/PopoverList.tsx b/client/src/components/PopoverList.tsx
new file mode 100644
index 0000000..7c81344
--- /dev/null
+++ b/client/src/components/PopoverList.tsx
@@ -0,0 +1,107 @@
+/*
+ * 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 {
+ ListItemText,
+ ListItemTextProps,
+ MenuItem,
+ MenuItemProps,
+ MenuList,
+ MenuListProps,
+ MenuProps,
+ Stack,
+ styled,
+ SvgIconProps,
+} from '@mui/material';
+import { ComponentType } from 'react';
+
+export type PopoverListItemData = {
+ label: string;
+ Icon: ComponentType<SvgIconProps>;
+ onClick: () => void;
+};
+
+interface ListIconProps extends SvgIconProps {
+ Icon: ComponentType<SvgIconProps>;
+}
+
+const ListIcon = styled(({ Icon, ...props }: ListIconProps) => <Icon {...props} />)(({ theme }) => ({
+ height: '16px',
+ fontSize: '16px',
+ color: theme?.palette?.primary?.dark,
+}));
+
+interface ListLabelProps extends ListItemTextProps {
+ label: string;
+}
+
+const ListLabel = styled(({ label, ...props }: ListLabelProps) => (
+ <ListItemText
+ {...props}
+ primary={label}
+ primaryTypographyProps={{
+ fontSize: '12px',
+ lineHeight: '16px',
+ }}
+ />
+))(() => ({
+ height: '16px',
+}));
+
+interface PopoverListItemProps extends MenuItemProps {
+ item: PopoverListItemData;
+ closeMenu?: MenuProps['onClose'];
+}
+
+const PopoverListItem = styled(({ item, closeMenu, ...props }: PopoverListItemProps) => (
+ <MenuItem
+ {...props}
+ onClick={() => {
+ item.onClick();
+ closeMenu?.({}, 'backdropClick');
+ }}
+ sx={{
+ paddingY: '11px',
+ paddingX: '16px',
+ }}
+ >
+ <Stack direction="row" spacing="10px">
+ <ListIcon Icon={item.Icon} />
+ <ListLabel label={item.label} />
+ </Stack>
+ </MenuItem>
+))(() => ({
+ // Failed to modify the styles from here
+}));
+
+interface PopoverListProps extends MenuListProps {
+ items: PopoverListItemData[];
+ onClose?: MenuProps['onClose'];
+}
+
+// A list intended to be used as a menu into a popover
+const PopoverList = styled(({ items, onClose, ...props }: PopoverListProps) => (
+ <MenuList {...props}>
+ {items.map((item, index) => (
+ <PopoverListItem key={index} item={item} closeMenu={onClose} />
+ ))}
+ </MenuList>
+))(() => ({
+ padding: '3px 0px',
+}));
+
+export default PopoverList;
diff --git a/client/src/components/RulesDialog.tsx b/client/src/components/RulesDialog.tsx
deleted file mode 100644
index 3b65f33..0000000
--- a/client/src/components/RulesDialog.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * 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';
-import { useTranslation } from 'react-i18next';
-
-interface RulesDialogProps {
- openDialog: boolean;
- title: string;
- closeDialog: () => void;
- children: React.ReactNode;
-}
-
-export default function RulesDialog(props: RulesDialogProps) {
- const { t } = useTranslation();
-
- return (
- <Dialog open={props.openDialog} onClose={props.closeDialog}>
- <DialogTitle>
- {props.title}
- <br />
- </DialogTitle>
- <DialogContent>{props.children}</DialogContent>
- <DialogActions>
- <Button onClick={props.closeDialog} autoFocus>
- {t('dialog_close')}
- </Button>
- </DialogActions>
- </Dialog>
- );
-}
diff --git a/client/src/locale/en/translation.json b/client/src/locale/en/translation.json
index 8a2ee50..9774ddc 100644
--- a/client/src/locale/en/translation.json
+++ b/client/src/locale/en/translation.json
@@ -9,13 +9,27 @@
"conversation_start_videocall": "Start video call",
"conversation_close": "Close this conversation",
"conversation_details": "Conversation details",
- "conversation_block_contact": "Block this contact",
- "conversation_delete_contact": "Delete this contact",
+ "conversation_block": "Block conversation",
+ "conversation_delete": "Remove conversation",
+ "conversation_details_username": "Username",
+ "conversation_details_identifier": "Identifier",
+ "conversation_details_qr_code": "QR code",
+ "conversation_details_is_swarm": "Is swarm",
+ "conversation_details_is_swarm_true": "True",
+ "conversation_details_is_swarm_false": "False",
+ "conversation_details_informations": "Informations",
+ "dialog_confirm_title_default": "Confirm action",
+ "conversation_ask_confirm_block": "Would you really like to block this conversation?",
+ "conversation_confirm_block": "Block",
+ "conversation_ask_confirm_remove": "Would you really like to remove this conversation?",
+ "conversation_confirm_remove": "Remove",
"conversation_title_one": "{{member0}}",
"conversation_title_two": "{{member0}} and {{member1}}",
"conversation_title_three": "{{member0}}, {{member1}} and {{member2}}",
"conversation_title_four": "{{member0}}, {{member1}}, {{member2}}, +1 other member",
"conversation_title_more": "{{member0}}, {{member1}}, {{member2}}, +{{excess}} other members",
+ "dialog_close": "Close",
+ "dialog_cancel": "Cancel",
"logout": "Log out",
"username_input_helper_text_success": "Username available",
"username_input_helper_text_taken": "Username already taken",
@@ -48,8 +62,7 @@
"message_call_incoming": "Incoming call - {{duration}}",
"message_swarm_created": "Swarm created",
"message_user_joined": "{{user}} joined",
- "messages_scroll_to_end": "",
- "dialog_close": "Close",
+ "messages_scroll_to_end": "Scroll to end of conversation",
"message_input_placeholder_one": "Write to {{member0}}",
"message_input_placeholder_two": "Write to {{member0}} and {{member1}}",
"message_input_placeholder_three": "Write to {{member0}}, {{member1}} and {{member2}}",
@@ -58,8 +71,8 @@
"conversation_add_contact": "Add contact",
"calling": "Calling...",
"connecting": "Connecting...",
- "incoming_call_{medium}": "",
"end_call": "End call",
+ "incoming_call_medium": "",
"login_username_not_found": "Username not found",
"login_invalid_password": "Incorrect password",
"login_form_title": "Login",
@@ -83,10 +96,5 @@
"setup_login_password_placeholder_repeat": "Repeat password",
"admin_creation_submit_button": "Create admin account",
"severity_error": "Error",
- "severity_success": "Success",
- "incoming_call_audio": "Incoming audio call from {{member0}}",
- "incoming_call_video": "Incoming video call from {{member0}}",
- "refuse_call": "Refuse",
- "accept_call_audio": "Accept in audio",
- "accept_call_video": "Accept in video"
+ "severity_success": "Success"
}
diff --git a/client/src/locale/fr/translation.json b/client/src/locale/fr/translation.json
index 272b4f1..9cd572d 100644
--- a/client/src/locale/fr/translation.json
+++ b/client/src/locale/fr/translation.json
@@ -9,13 +9,27 @@
"conversation_start_videocall": "Démarrer appel vidéo",
"conversation_close": "Fermer la conversation",
"conversation_details": "Détails de la conversation",
- "conversation_block_contact": "Bloquer le contact",
- "conversation_delete_contact": "Supprimer le contact",
+ "conversation_block": "Bloquer la conversation",
+ "conversation_delete": "Supprimer la conversation",
+ "conversation_details_username": "Nom d'utilisateur",
+ "conversation_details_identifier": "Identifiant",
+ "conversation_details_qr_code": "Code QR",
+ "conversation_details_is_swarm": "Est un swarm",
+ "conversation_details_is_swarm_true": "Oui",
+ "conversation_details_is_swarm_false": "Non",
+ "conversation_details_informations": "Informations",
+ "dialog_confirm_title_default": "Merci de confirmer",
+ "conversation_ask_confirm_block": "Souhaitez-vous vraiment bloquer cette conversation ?",
+ "conversation_confirm_block": "Bloquer",
+ "conversation_ask_confirm_remove": "Souhaitez-vous vraiment supprimer cette conversation ?",
+ "conversation_confirm_remove": "Supprimer",
"conversation_title_one": "{{member0}}",
"conversation_title_two": "{{member0}} et {{member1}}",
"conversation_title_three": "{{member0}}, {{member1}} et {{member2}}",
"conversation_title_four": "{{member0}}, {{member1}}, {{member2}}, +1 autre membre",
"conversation_title_more": "{{member0}}, {{member1}}, {{member2}}, +{{excess}} autres membres",
+ "dialog_close": "Fermer",
+ "dialog_cancel": "Annuler",
"logout": "Se déconnecter",
"username_input_helper_text_success": "Nom d'utilisateur disponible",
"username_input_helper_text_taken": "Nom d'utilisateur déjà pris",
@@ -48,8 +62,7 @@
"message_call_incoming": "Appel sortant - {{duration}}",
"message_swarm_created": "Le Swarm a été créé",
"message_user_joined": "{{user}} s'est joint",
- "messages_scroll_to_end": "",
- "dialog_close": "Fermer",
+ "messages_scroll_to_end": "Faire défiler jusqu'à la fin de la conversation",
"message_input_placeholder_one": "Écrire à {{member0}}",
"message_input_placeholder_two": "Écrire à {{member0}} et {{member1}}",
"message_input_placeholder_three": "Écrire à {{member0}}, {{member1}} et {{member2}}",
@@ -58,8 +71,8 @@
"conversation_add_contact": "Ajouter le contact",
"calling": "Appel en cours...",
"connecting": "Connexion en cours...",
- "incoming_call_{medium}": "",
"end_call": "Fin d'appel",
+ "incoming_call_medium": "",
"login_username_not_found": "Nom d'utilisateur introuvable",
"login_invalid_password": "Mot de passe incorrect",
"login_form_title": "Connexion",
@@ -83,10 +96,5 @@
"setup_login_password_placeholder_repeat": "Répéter le mot de passe",
"admin_creation_submit_button": "Créer un compte admin",
"severity_error": "Erreur",
- "severity_success": "Succès",
- "incoming_call_audio": "Appel audio entrant de {{member0}}",
- "incoming_call_video": "Appel vidéo entrant de {{member0}}",
- "refuse_call": "Refuser",
- "accept_call_audio": "Accepter en audio",
- "accept_call_video": "Accepter en vidéo"
+ "severity_success": "Succès"
}
diff --git a/client/src/themes/Default.ts b/client/src/themes/Default.ts
index 60fb1ec..977de0b 100644
--- a/client/src/themes/Default.ts
+++ b/client/src/themes/Default.ts
@@ -205,6 +205,38 @@
},
},
},
+ MuiDialog: {
+ styleOverrides: {
+ paper: {
+ padding: '16px',
+ boxShadow: '3px 3px 7px #00000029',
+ borderRadius: '20px',
+ },
+ },
+ },
+ MuiDialogActions: {
+ styleOverrides: {
+ root: {
+ padding: '0px',
+ },
+ },
+ },
+ MuiDialogContent: {
+ styleOverrides: {
+ root: {
+ padding: '0px',
+ margin: '16px 0px',
+ minWidth: '500px',
+ },
+ },
+ },
+ MuiDialogTitle: {
+ styleOverrides: {
+ root: {
+ padding: '0px',
+ },
+ },
+ },
MuiSwitch: {
defaultProps: {
disableRipple: true,