From d760be58c3b64629e6fc6483dc6c5a2b343ab55b Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Mon, 25 Apr 2022 20:21:21 +0530 Subject: [PATCH] Replace confirm and prompt with custom dialogs (#500) --- .../confirm-dialog/ConfirmDialog.jsx | 58 ++++++++++++++++++ .../confirm-dialog/ConfirmDialog.scss | 11 ++++ src/app/molecules/message/Message.jsx | 15 +++-- .../room-encryption/RoomEncryption.jsx | 21 ++++--- .../molecules/room-options/RoomOptions.jsx | 20 ++++--- .../molecules/room-profile/RoomProfile.jsx | 9 ++- .../molecules/space-options/SpaceOptions.jsx | 17 ++++-- .../profile-editor/ProfileEditor.jsx | 11 +++- .../profile-viewer/ProfileViewer.jsx | 14 +++-- src/app/organisms/room/RoomSettings.jsx | 14 +++-- src/app/organisms/settings/DeviceManage.jsx | 60 ++++++++++++++++--- src/app/organisms/settings/DeviceManage.scss | 11 ++++ src/app/organisms/settings/Settings.jsx | 7 ++- .../space-settings/SpaceSettings.jsx | 14 +++-- 14 files changed, 232 insertions(+), 50 deletions(-) create mode 100644 src/app/molecules/confirm-dialog/ConfirmDialog.jsx create mode 100644 src/app/molecules/confirm-dialog/ConfirmDialog.scss diff --git a/src/app/molecules/confirm-dialog/ConfirmDialog.jsx b/src/app/molecules/confirm-dialog/ConfirmDialog.jsx new file mode 100644 index 00000000..5771f2c1 --- /dev/null +++ b/src/app/molecules/confirm-dialog/ConfirmDialog.jsx @@ -0,0 +1,58 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import './ConfirmDialog.scss'; + +import { openReusableDialog } from '../../../client/action/navigation'; + +import Text from '../../atoms/text/Text'; +import Button from '../../atoms/button/Button'; + +function ConfirmDialog({ + desc, actionTitle, actionType, onComplete, +}) { + return ( +
+ {desc} +
+ + +
+
+ ); +} +ConfirmDialog.propTypes = { + desc: PropTypes.string.isRequired, + actionTitle: PropTypes.string.isRequired, + actionType: PropTypes.oneOf(['primary', 'positive', 'danger', 'caution']).isRequired, + onComplete: PropTypes.func.isRequired, +}; + +/** + * @param {string} title title of confirm dialog + * @param {string} desc description of confirm dialog + * @param {string} actionTitle title of main action to take + * @param {'primary' | 'positive' | 'danger' | 'caution'} actionType type of action. default=primary + * @return {Promise} does it get's confirmed or not + */ +// eslint-disable-next-line import/prefer-default-export +export const confirmDialog = (title, desc, actionTitle, actionType = 'primary') => new Promise((resolve) => { + let isCompleted = false; + openReusableDialog( + {title}, + (requestClose) => ( + { + isCompleted = true; + resolve(isConfirmed); + requestClose(); + }} + /> + ), + () => { + if (!isCompleted) resolve(false); + }, + ); +}); diff --git a/src/app/molecules/confirm-dialog/ConfirmDialog.scss b/src/app/molecules/confirm-dialog/ConfirmDialog.scss new file mode 100644 index 00000000..05f88be1 --- /dev/null +++ b/src/app/molecules/confirm-dialog/ConfirmDialog.scss @@ -0,0 +1,11 @@ +.confirm-dialog { + padding: var(--sp-normal); + + & > .text { + padding-bottom: var(--sp-normal); + } + &__btn { + display: flex; + gap: var(--sp-normal); + } +} \ No newline at end of file diff --git a/src/app/molecules/message/Message.jsx b/src/app/molecules/message/Message.jsx index 70ca87e3..be4dea55 100644 --- a/src/app/molecules/message/Message.jsx +++ b/src/app/molecules/message/Message.jsx @@ -36,6 +36,8 @@ import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg'; import CmdIC from '../../../../public/res/ic/outlined/cmd.svg'; import BinIC from '../../../../public/res/ic/outlined/bin.svg'; +import { confirmDialog } from '../confirm-dialog/ConfirmDialog'; + function PlaceholderMessage() { return (
@@ -546,10 +548,15 @@ const MessageOptions = React.memo(({ { - if (window.confirm('Are you sure that you want to delete this event?')) { - redactEvent(roomId, mEvent.getId()); - } + onClick={async () => { + const isConfirmed = await confirmDialog( + 'Delete message', + 'Are you sure that you want to delete this message?', + 'Delete', + 'danger', + ); + if (!isConfirmed) return; + redactEvent(roomId, mEvent.getId()); }} > Delete diff --git a/src/app/molecules/room-encryption/RoomEncryption.jsx b/src/app/molecules/room-encryption/RoomEncryption.jsx index b9ba706b..1657f363 100644 --- a/src/app/molecules/room-encryption/RoomEncryption.jsx +++ b/src/app/molecules/room-encryption/RoomEncryption.jsx @@ -8,6 +8,8 @@ import Text from '../../atoms/text/Text'; import Toggle from '../../atoms/button/Toggle'; import SettingTile from '../setting-tile/SettingTile'; +import { confirmDialog } from '../confirm-dialog/ConfirmDialog'; + function RoomEncryption({ roomId }) { const mx = initMatrix.matrixClient; const room = mx.getRoom(roomId); @@ -15,17 +17,20 @@ function RoomEncryption({ roomId }) { const [isEncrypted, setIsEncrypted] = useState(encryptionEvents.length > 0); const canEnableEncryption = room.currentState.maySendStateEvent('m.room.encryption', mx.getUserId()); - const handleEncryptionEnable = () => { + const handleEncryptionEnable = async () => { const joinRule = room.getJoinRule(); const confirmMsg1 = 'It is not recommended to add encryption in public room. Anyone can find and join public rooms, so anyone can read messages in them.'; const confirmMsg2 = 'Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly'; - if (joinRule === 'public' ? confirm(confirmMsg1) : true) { - if (confirm(confirmMsg2)) { - setIsEncrypted(true); - mx.sendStateEvent(roomId, 'm.room.encryption', { - algorithm: 'm.megolm.v1.aes-sha2', - }); - } + + const isConfirmed1 = (joinRule === 'public') + ? await confirmDialog('Enable encryption', confirmMsg1, 'Continue', 'caution') + : true; + if (!isConfirmed1) return; + if (await confirmDialog('Enable encryption', confirmMsg2, 'Enable', 'caution')) { + setIsEncrypted(true); + mx.sendStateEvent(roomId, 'm.room.encryption', { + algorithm: 'm.megolm.v1.aes-sha2', + }); } }; diff --git a/src/app/molecules/room-options/RoomOptions.jsx b/src/app/molecules/room-options/RoomOptions.jsx index ef824785..af18d712 100644 --- a/src/app/molecules/room-options/RoomOptions.jsx +++ b/src/app/molecules/room-options/RoomOptions.jsx @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import { twemojify } from '../../../util/twemojify'; import initMatrix from '../../../client/initMatrix'; -import { openInviteUser, openNavigation } from '../../../client/action/navigation'; +import { openInviteUser } from '../../../client/action/navigation'; import * as roomActions from '../../../client/action/room'; import { markAsRead } from '../../../client/action/notifications'; @@ -15,6 +15,8 @@ import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg'; import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg'; import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg'; +import { confirmDialog } from '../confirm-dialog/ConfirmDialog'; + function RoomOptions({ roomId, afterOptionSelect }) { const mx = initMatrix.matrixClient; const room = mx.getRoom(roomId); @@ -29,12 +31,16 @@ function RoomOptions({ roomId, afterOptionSelect }) { openInviteUser(roomId); afterOptionSelect(); }; - const handleLeaveClick = () => { - if (confirm('Are you sure that you want to leave this room?')) { - roomActions.leave(roomId); - afterOptionSelect(); - openNavigation(); - } + const handleLeaveClick = async () => { + afterOptionSelect(); + const isConfirmed = await confirmDialog( + 'Leave room', + `Are you sure that you want to leave "${room.name}" room?`, + 'Leave', + 'danger', + ); + if (!isConfirmed) return; + roomActions.leave(roomId); }; return ( diff --git a/src/app/molecules/room-profile/RoomProfile.jsx b/src/app/molecules/room-profile/RoomProfile.jsx index 4043cbf8..96e84076 100644 --- a/src/app/molecules/room-profile/RoomProfile.jsx +++ b/src/app/molecules/room-profile/RoomProfile.jsx @@ -19,6 +19,7 @@ import PencilIC from '../../../../public/res/ic/outlined/pencil.svg'; import { useStore } from '../../hooks/useStore'; import { useForceUpdate } from '../../hooks/useForceUpdate'; +import { confirmDialog } from '../confirm-dialog/ConfirmDialog'; function RoomProfile({ roomId }) { const isMountStore = useStore(); @@ -117,7 +118,13 @@ function RoomProfile({ roomId }) { const handleAvatarUpload = async (url) => { if (url === null) { - if (confirm('Are you sure that you want to remove room avatar?')) { + const isConfirmed = await confirmDialog( + 'Remove avatar', + 'Are you sure that you want to remove room avatar?', + 'Remove', + 'caution', + ); + if (isConfirmed) { await mx.sendStateEvent(roomId, 'm.room.avatar', { url }, ''); } } else await mx.sendStateEvent(roomId, 'm.room.avatar', { url }, ''); diff --git a/src/app/molecules/space-options/SpaceOptions.jsx b/src/app/molecules/space-options/SpaceOptions.jsx index e9b72cb6..e6b78d4c 100644 --- a/src/app/molecules/space-options/SpaceOptions.jsx +++ b/src/app/molecules/space-options/SpaceOptions.jsx @@ -24,6 +24,8 @@ import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg'; import PinIC from '../../../../public/res/ic/outlined/pin.svg'; import PinFilledIC from '../../../../public/res/ic/filled/pin.svg'; +import { confirmDialog } from '../confirm-dialog/ConfirmDialog'; + function SpaceOptions({ roomId, afterOptionSelect }) { const mx = initMatrix.matrixClient; const room = mx.getRoom(roomId); @@ -54,11 +56,16 @@ function SpaceOptions({ roomId, afterOptionSelect }) { afterOptionSelect(); }; - const handleLeaveClick = () => { - if (confirm('Are you sure that you want to leave this space?')) { - leave(roomId); - afterOptionSelect(); - } + const handleLeaveClick = async () => { + afterOptionSelect(); + const isConfirmed = await confirmDialog( + 'Leave space', + `Are you sure that you want to leave "${room.name}" space?`, + 'Leave', + 'danger', + ); + if (!isConfirmed) return; + leave(roomId); }; return ( diff --git a/src/app/organisms/profile-editor/ProfileEditor.jsx b/src/app/organisms/profile-editor/ProfileEditor.jsx index 677985f4..972192ef 100644 --- a/src/app/organisms/profile-editor/ProfileEditor.jsx +++ b/src/app/organisms/profile-editor/ProfileEditor.jsx @@ -12,6 +12,7 @@ import ImageUpload from '../../molecules/image-upload/ImageUpload'; import Input from '../../atoms/input/Input'; import PencilIC from '../../../../public/res/ic/outlined/pencil.svg'; +import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog'; import './ProfileEditor.scss'; @@ -38,9 +39,15 @@ function ProfileEditor({ userId }) { }; }, [userId]); - const handleAvatarUpload = (url) => { + const handleAvatarUpload = async (url) => { if (url === null) { - if (confirm('Are you sure that you want to remove avatar?')) { + const isConfirmed = await confirmDialog( + 'Remove avatar', + 'Are you sure that you want to remove avatar?', + 'Remove', + 'caution', + ); + if (isConfirmed) { mx.setAvatarUrl(''); setAvatarSrc(null); } diff --git a/src/app/organisms/profile-viewer/ProfileViewer.jsx b/src/app/organisms/profile-viewer/ProfileViewer.jsx index 343d813c..d74629f2 100644 --- a/src/app/organisms/profile-viewer/ProfileViewer.jsx +++ b/src/app/organisms/profile-viewer/ProfileViewer.jsx @@ -32,6 +32,7 @@ import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.s import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; import { useForceUpdate } from '../../hooks/useForceUpdate'; +import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog'; function ModerationTools({ roomId, userId, @@ -362,7 +363,7 @@ function ProfileViewer() { && (powerLevel < myPowerLevel || userId === mx.getUserId()) ); - const handleChangePowerLevel = (newPowerLevel) => { + const handleChangePowerLevel = async (newPowerLevel) => { if (newPowerLevel === powerLevel) return; const SHARED_POWER_MSG = 'You will not be able to undo this change as you are promoting the user to have the same power level as yourself. Are you sure?'; const DEMOTING_MYSELF_MSG = 'You will not be able to undo this change as you are demoting yourself. Are you sure?'; @@ -370,9 +371,14 @@ function ProfileViewer() { const isSharedPower = newPowerLevel === myPowerLevel; const isDemotingMyself = userId === mx.getUserId(); if (isSharedPower || isDemotingMyself) { - if (confirm(isSharedPower ? SHARED_POWER_MSG : DEMOTING_MYSELF_MSG)) { - roomActions.setPowerLevel(roomId, userId, newPowerLevel); - } + const isConfirmed = await confirmDialog( + 'Change power level', + isSharedPower ? SHARED_POWER_MSG : DEMOTING_MYSELF_MSG, + 'Change', + 'caution', + ); + if (!isConfirmed) return; + roomActions.setPowerLevel(roomId, userId, newPowerLevel); } else { roomActions.setPowerLevel(roomId, userId, newPowerLevel); } diff --git a/src/app/organisms/room/RoomSettings.jsx b/src/app/organisms/room/RoomSettings.jsx index 8d14c18d..50c5e512 100644 --- a/src/app/organisms/room/RoomSettings.jsx +++ b/src/app/organisms/room/RoomSettings.jsx @@ -36,6 +36,7 @@ import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg'; import ChevronTopIC from '../../../../public/res/ic/outlined/chevron-top.svg'; import { useForceUpdate } from '../../hooks/useForceUpdate'; +import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog'; const tabText = { GENERAL: 'General', @@ -85,10 +86,15 @@ function GeneralSettings({ roomId }) { { - if (confirm('Are you sure that you want to leave this room?')) { - roomActions.leave(roomId); - } + onClick={async () => { + const isConfirmed = await confirmDialog( + 'Leave room', + `Are you sure that you want to leave "${room.name}" room?`, + 'Leave', + 'danger', + ); + if (!isConfirmed) return; + roomActions.leave(roomId); }} iconSrc={LeaveArrowIC} > diff --git a/src/app/organisms/settings/DeviceManage.jsx b/src/app/organisms/settings/DeviceManage.jsx index d7efd362..b6ce307b 100644 --- a/src/app/organisms/settings/DeviceManage.jsx +++ b/src/app/organisms/settings/DeviceManage.jsx @@ -4,10 +4,12 @@ import dateFormat from 'dateformat'; import initMatrix from '../../../client/initMatrix'; import { isCrossVerified } from '../../../util/matrixUtil'; +import { openReusableDialog } from '../../../client/action/navigation'; import Text from '../../atoms/text/Text'; import Button from '../../atoms/button/Button'; import IconButton from '../../atoms/button/IconButton'; +import Input from '../../atoms/input/Input'; import { MenuHeader } from '../../atoms/context-menu/ContextMenu'; import InfoCard from '../../atoms/card/InfoCard'; import Spinner from '../../atoms/spinner/Spinner'; @@ -18,11 +20,46 @@ import BinIC from '../../../../public/res/ic/outlined/bin.svg'; import InfoIC from '../../../../public/res/ic/outlined/info.svg'; import { authRequest } from './AuthRequest'; +import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog'; import { useStore } from '../../hooks/useStore'; import { useDeviceList } from '../../hooks/useDeviceList'; import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus'; +const promptDeviceName = async (deviceName) => new Promise((resolve) => { + let isCompleted = false; + + const renderContent = (onComplete) => { + const handleSubmit = (e) => { + e.preventDefault(); + const name = e.target.session.value; + if (typeof name !== 'string') onComplete(null); + onComplete(name); + }; + return ( +
+ +
+ + +
+
+ ); + }; + + openReusableDialog( + Edit session name, + (requestClose) => renderContent((name) => { + isCompleted = true; + resolve(name); + requestClose(); + }), + () => { + if (!isCompleted) resolve(null); + }, + ); +}); + function DeviceManage() { const TRUNCATED_COUNT = 4; const mx = initMatrix.matrixClient; @@ -59,7 +96,7 @@ function DeviceManage() { } const handleRename = async (device) => { - const newName = window.prompt('Edit session name', device.display_name); + const newName = await promptDeviceName(device.display_name); if (newName === null || newName.trim() === '') return; if (newName.trim() === device.display_name) return; addToProcessing(device); @@ -74,15 +111,20 @@ function DeviceManage() { }; const handleRemove = async (device) => { - if (window.confirm(`You are about to logout "${device.display_name}" session.`)) { - addToProcessing(device); - await authRequest(`Logout "${device.display_name}"`, async (auth) => { - await mx.deleteDevice(device.device_id, auth); - }); + const isConfirmed = await confirmDialog( + `Logout ${device.display_name}`, + `You are about to logout "${device.display_name}" session.`, + 'Logout', + 'danger', + ); + if (!isConfirmed) return; + addToProcessing(device); + await authRequest(`Logout "${device.display_name}"`, async (auth) => { + await mx.deleteDevice(device.device_id, auth); + }); - if (!mountStore.getItem()) return; - removeFromProcessing(device); - } + if (!mountStore.getItem()) return; + removeFromProcessing(device); }; const renderDevice = (device, isVerified) => { diff --git a/src/app/organisms/settings/DeviceManage.scss b/src/app/organisms/settings/DeviceManage.scss index 0daf2e61..0a8fc4a9 100644 --- a/src/app/organisms/settings/DeviceManage.scss +++ b/src/app/organisms/settings/DeviceManage.scss @@ -15,4 +15,15 @@ & .setting-tile:last-of-type { border-bottom: none; } + + &__rename { + padding: var(--sp-normal); + & > *:not(:last-child) { + margin-bottom: var(--sp-normal); + } + &-btn { + display: flex; + gap: var(--sp-normal); + } + } } \ No newline at end of file diff --git a/src/app/organisms/settings/Settings.jsx b/src/app/organisms/settings/Settings.jsx index 6dbbffb2..82b948ad 100644 --- a/src/app/organisms/settings/Settings.jsx +++ b/src/app/organisms/settings/Settings.jsx @@ -38,6 +38,7 @@ import PowerIC from '../../../../public/res/ic/outlined/power.svg'; import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; import CinnySVG from '../../../../public/res/svg/cinny.svg'; +import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog'; function AppearanceSection() { const [, updateState] = useState({}); @@ -297,8 +298,10 @@ function Settings() { const [isOpen, requestClose] = useWindowToggle(setSelectedTab); const handleTabChange = (tabItem) => setSelectedTab(tabItem); - const handleLogout = () => { - if (confirm('Confirm logout')) logout(); + const handleLogout = async () => { + if (await confirmDialog('Logout', 'Are you sure that you want to logout your session?', 'Logout', 'danger')) { + logout(); + } }; return ( diff --git a/src/app/organisms/space-settings/SpaceSettings.jsx b/src/app/organisms/space-settings/SpaceSettings.jsx index 18e21cf3..43735993 100644 --- a/src/app/organisms/space-settings/SpaceSettings.jsx +++ b/src/app/organisms/space-settings/SpaceSettings.jsx @@ -36,6 +36,7 @@ import PinFilledIC from '../../../../public/res/ic/filled/pin.svg'; import CategoryIC from '../../../../public/res/ic/outlined/category.svg'; import CategoryFilledIC from '../../../../public/res/ic/filled/category.svg'; +import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog'; import { useForceUpdate } from '../../hooks/useForceUpdate'; const tabText = { @@ -61,6 +62,7 @@ const tabItems = [{ function GeneralSettings({ roomId }) { const isPinned = initMatrix.accountData.spaceShortcut.has(roomId); const isCategorized = initMatrix.accountData.categorizedSpaces.has(roomId); + const roomName = initMatrix.matrixClient.getRoom(roomId)?.name; const [, forceUpdate] = useForceUpdate(); return ( @@ -89,10 +91,14 @@ function GeneralSettings({ roomId }) {
{ - if (confirm('Are you sure that you want to leave this space?')) { - leave(roomId); - } + onClick={async () => { + const isConfirmed = await confirmDialog( + 'Leave space', + `Are you sure that you want to leave "${roomName}" space?`, + 'Leave', + 'danger', + ); + if (isConfirmed) leave(roomId); }} iconSrc={LeaveArrowIC} >