Replace confirm and prompt with custom dialogs (#500)
authorAjay Bura <32841439+ajbura@users.noreply.github.com>
Mon, 25 Apr 2022 14:51:21 +0000 (20:21 +0530)
committerGitHub <noreply@github.com>
Mon, 25 Apr 2022 14:51:21 +0000 (20:21 +0530)
14 files changed:
src/app/molecules/confirm-dialog/ConfirmDialog.jsx [new file with mode: 0644]
src/app/molecules/confirm-dialog/ConfirmDialog.scss [new file with mode: 0644]
src/app/molecules/message/Message.jsx
src/app/molecules/room-encryption/RoomEncryption.jsx
src/app/molecules/room-options/RoomOptions.jsx
src/app/molecules/room-profile/RoomProfile.jsx
src/app/molecules/space-options/SpaceOptions.jsx
src/app/organisms/profile-editor/ProfileEditor.jsx
src/app/organisms/profile-viewer/ProfileViewer.jsx
src/app/organisms/room/RoomSettings.jsx
src/app/organisms/settings/DeviceManage.jsx
src/app/organisms/settings/DeviceManage.scss
src/app/organisms/settings/Settings.jsx
src/app/organisms/space-settings/SpaceSettings.jsx

diff --git a/src/app/molecules/confirm-dialog/ConfirmDialog.jsx b/src/app/molecules/confirm-dialog/ConfirmDialog.jsx
new file mode 100644 (file)
index 0000000..5771f2c
--- /dev/null
@@ -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 (
+    <div className="confirm-dialog">
+      <Text>{desc}</Text>
+      <div className="confirm-dialog__btn">
+        <Button variant={actionType} onClick={() => onComplete(true)}>{actionTitle}</Button>
+        <Button onClick={() => onComplete(false)}>Cancel</Button>
+      </div>
+    </div>
+  );
+}
+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<boolean>} 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(
+    <Text variant="s1" weight="medium">{title}</Text>,
+    (requestClose) => (
+      <ConfirmDialog
+        desc={desc}
+        actionTitle={actionTitle}
+        actionType={actionType}
+        onComplete={(isConfirmed) => {
+          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 (file)
index 0000000..05f88be
--- /dev/null
@@ -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
index 70ca87e3f29900afd8fec8feb4d504e1736fac42..be4dea55926763939e8f9b47f22d854456bce6a3 100644 (file)
@@ -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 (
     <div className="ph-msg">
@@ -546,10 +548,15 @@ const MessageOptions = React.memo(({
                 <MenuItem
                   variant="danger"
                   iconSrc={BinIC}
-                  onClick={() => {
-                    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
index b9ba706b93bc919d047fcc6b83666e664f127898..1657f3631d020481d09acbc5597228f0493364b3 100644 (file)
@@ -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',
+      });
     }
   };
 
index ef824785f48f88d4b92605167639ed9ee06b93db..af18d71206f7a91bb98653e64af240e7b935737a 100644 (file)
@@ -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 (
index 4043cbf8c902394184da81493b77d5aebe47930a..96e840766400d66c482fa8ac8afaf01e29ab96d8 100644 (file)
@@ -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 }, '');
index e9b72cb62c4d27563a197b5c71696bce435bcf33..e6b78d4c607c8fb609e080f7d4a13e6334fcfa6e 100644 (file)
@@ -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 (
index 677985f487b318dad603f046311bccee1317d183..972192efdf21218e9ee413014af17946df360aa0 100644 (file)
@@ -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);
       }
index 343d813c97a49e8c89ec8f9459f8b7f9c43ed6e2..d74629f26537baecda23281bc5dc85edc92f25a9 100644 (file)
@@ -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);
       }
index 8d14c18dbdd38ea9736ac4b36a0af4fb03a2417b..50c5e5127c780b58b4df2e4346776c9d2c89b1e3 100644 (file)
@@ -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 }) {
         </MenuItem>
         <MenuItem
           variant="danger"
-          onClick={() => {
-            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}
         >
index d7efd362135ad351dc264d18c5c3b4617e8efd26..b6ce307b151f1437355589e7cf0f30bc52968c78 100644 (file)
@@ -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 (
+      <form className="device-manage__rename" onSubmit={handleSubmit}>
+        <Input value={deviceName} label="Session name" name="session" />
+        <div className="device-manage__rename-btn">
+          <Button variant="primary" type="submit">Save</Button>
+          <Button onClick={() => onComplete(null)}>Cancel</Button>
+        </div>
+      </form>
+    );
+  };
+
+  openReusableDialog(
+    <Text variant="s1" weight="medium">Edit session name</Text>,
+    (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) => {
index 0daf2e6128c50019cc0221a8930d26048156add9..0a8fc4a986196316ac67882698c84dc36e7cc030 100644 (file)
   & .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
index 6dbbffb2c515c3e1e29cf61dda658ec5c2a8ae36..82b948adf7176c740f17098bd0dacdb6f71917e3 100644 (file)
@@ -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 (
index 18e21cf30be3d25d5c31c1d77cabf5cbba0a6d17..437359930efd48f8bbfa160c5d84c8ca969228e3 100644 (file)
@@ -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 }) {
         </MenuItem>
         <MenuItem
           variant="danger"
-          onClick={() => {
-            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}
         >