Add dnd space shortcut (#153)
authorAjay Bura <ajbura@gmail.com>
Tue, 8 Mar 2022 11:04:55 +0000 (16:34 +0530)
committerAjay Bura <ajbura@gmail.com>
Tue, 8 Mar 2022 11:04:55 +0000 (16:34 +0530)
Signed-off-by: Ajay Bura <ajbura@gmail.com>
package-lock.json
package.json
src/app/organisms/navigation/SideBar.jsx

index 3e8d258ac8e873bd97351c497bc75ff36876f013..4e6e4dfdb602afb7d5ba304af087f09be0bb8cf0 100644 (file)
@@ -31,6 +31,8 @@
         "prop-types": "^15.8.1",
         "react": "^17.0.2",
         "react-autosize-textarea": "^7.1.0",
+        "react-dnd": "^15.1.1",
+        "react-dnd-html5-backend": "^15.1.2",
         "react-dom": "^17.0.2",
         "react-google-recaptcha": "^2.1.0",
         "react-modal": "^3.14.4",
         "url": "https://opencollective.com/popperjs"
       }
     },
+    "node_modules/@react-dnd/asap": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.0.tgz",
+      "integrity": "sha512-0XhqJSc6pPoNnf8DhdsPHtUhRzZALVzYMTzRwV4VI6DJNJ/5xxfL9OQUwb8IH5/2x7lSf7nAZrnzUD+16VyOVQ=="
+    },
+    "node_modules/@react-dnd/invariant": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-3.0.0.tgz",
+      "integrity": "sha512-keberJRIqPX15IK3SWS/iO1t/kGETiL1oczKrDitAaMnQ+kpHf81l3MrRmFjvfqcnApE+izEvwM6GsyoIcpsVA=="
+    },
+    "node_modules/@react-dnd/shallowequal": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-3.0.0.tgz",
+      "integrity": "sha512-1ELWQdJB2UrCXTKK5cCD9uGLLIwECLIEdttKA255owdpchtXohIjZBTlFJszwYi2ZKe2Do+QvUzsGyGCMNwbdw=="
+    },
     "node_modules/@tippyjs/react": {
       "version": "4.2.6",
       "resolved": "https://registry.npmjs.org/@tippyjs/react/-/react-4.2.6.tgz",
       "version": "16.11.6",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.6.tgz",
       "integrity": "sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w==",
-      "dev": true
+      "devOptional": true
     },
     "node_modules/@types/qs": {
       "version": "6.9.7",
         "node": ">=8"
       }
     },
+    "node_modules/dnd-core": {
+      "version": "15.1.1",
+      "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-15.1.1.tgz",
+      "integrity": "sha512-Mtj/Sltcx7stVXzeDg4g7roTe/AmzRuIf/FYOxX6F8gULbY54w066BlErBOzQfn9RIJ3gAYLGX7wvVvoBSq7ig==",
+      "dependencies": {
+        "@react-dnd/asap": "4.0.0",
+        "@react-dnd/invariant": "3.0.0",
+        "redux": "^4.1.1"
+      }
+    },
     "node_modules/dns-equal": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz",
         "react-dom": "^0.14.0 || ^15.0.0 || ^16.0.0"
       }
     },
+    "node_modules/react-dnd": {
+      "version": "15.1.1",
+      "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-15.1.1.tgz",
+      "integrity": "sha512-QLrHtPU08U4c5zop0ANeqrHXaQw2EWLMn8DQoN6/e4eSN/UbB84P49/80Qg0MEF29VLB5vikSoiFh9N8ASNmpQ==",
+      "dependencies": {
+        "@react-dnd/invariant": "3.0.0",
+        "@react-dnd/shallowequal": "3.0.0",
+        "dnd-core": "15.1.1",
+        "fast-deep-equal": "^3.1.3",
+        "hoist-non-react-statics": "^3.3.2"
+      },
+      "peerDependencies": {
+        "@types/hoist-non-react-statics": ">= 3.3.1",
+        "@types/node": ">= 12",
+        "@types/react": ">= 16",
+        "react": ">= 16.14"
+      },
+      "peerDependenciesMeta": {
+        "@types/hoist-non-react-statics": {
+          "optional": true
+        },
+        "@types/node": {
+          "optional": true
+        },
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/react-dnd-html5-backend": {
+      "version": "15.1.2",
+      "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-15.1.2.tgz",
+      "integrity": "sha512-mem9QbutUF+aA2YC1y47G3ECjnYV/sCYKSnu5Jd7cbg3fLMPAwbnTf/JayYdnCH5l3eg9akD9dQt+cD0UdF8QQ==",
+      "dependencies": {
+        "dnd-core": "15.1.1"
+      }
+    },
     "node_modules/react-dom": {
       "version": "17.0.2",
       "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
         "node": ">= 0.10"
       }
     },
+    "node_modules/redux": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.2.tgz",
+      "integrity": "sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==",
+      "dependencies": {
+        "@babel/runtime": "^7.9.2"
+      }
+    },
     "node_modules/regenerate": {
       "version": "1.4.2",
       "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
       "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.10.2.tgz",
       "integrity": "sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ=="
     },
+    "@react-dnd/asap": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.0.tgz",
+      "integrity": "sha512-0XhqJSc6pPoNnf8DhdsPHtUhRzZALVzYMTzRwV4VI6DJNJ/5xxfL9OQUwb8IH5/2x7lSf7nAZrnzUD+16VyOVQ=="
+    },
+    "@react-dnd/invariant": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-3.0.0.tgz",
+      "integrity": "sha512-keberJRIqPX15IK3SWS/iO1t/kGETiL1oczKrDitAaMnQ+kpHf81l3MrRmFjvfqcnApE+izEvwM6GsyoIcpsVA=="
+    },
+    "@react-dnd/shallowequal": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-3.0.0.tgz",
+      "integrity": "sha512-1ELWQdJB2UrCXTKK5cCD9uGLLIwECLIEdttKA255owdpchtXohIjZBTlFJszwYi2ZKe2Do+QvUzsGyGCMNwbdw=="
+    },
     "@tippyjs/react": {
       "version": "4.2.6",
       "resolved": "https://registry.npmjs.org/@tippyjs/react/-/react-4.2.6.tgz",
       "version": "16.11.6",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.6.tgz",
       "integrity": "sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w==",
-      "dev": true
+      "devOptional": true
     },
     "@types/qs": {
       "version": "6.9.7",
         "path-type": "^4.0.0"
       }
     },
+    "dnd-core": {
+      "version": "15.1.1",
+      "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-15.1.1.tgz",
+      "integrity": "sha512-Mtj/Sltcx7stVXzeDg4g7roTe/AmzRuIf/FYOxX6F8gULbY54w066BlErBOzQfn9RIJ3gAYLGX7wvVvoBSq7ig==",
+      "requires": {
+        "@react-dnd/asap": "4.0.0",
+        "@react-dnd/invariant": "3.0.0",
+        "redux": "^4.1.1"
+      }
+    },
     "dns-equal": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz",
         "prop-types": "^15.5.6"
       }
     },
+    "react-dnd": {
+      "version": "15.1.1",
+      "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-15.1.1.tgz",
+      "integrity": "sha512-QLrHtPU08U4c5zop0ANeqrHXaQw2EWLMn8DQoN6/e4eSN/UbB84P49/80Qg0MEF29VLB5vikSoiFh9N8ASNmpQ==",
+      "requires": {
+        "@react-dnd/invariant": "3.0.0",
+        "@react-dnd/shallowequal": "3.0.0",
+        "dnd-core": "15.1.1",
+        "fast-deep-equal": "^3.1.3",
+        "hoist-non-react-statics": "^3.3.2"
+      }
+    },
+    "react-dnd-html5-backend": {
+      "version": "15.1.2",
+      "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-15.1.2.tgz",
+      "integrity": "sha512-mem9QbutUF+aA2YC1y47G3ECjnYV/sCYKSnu5Jd7cbg3fLMPAwbnTf/JayYdnCH5l3eg9akD9dQt+cD0UdF8QQ==",
+      "requires": {
+        "dnd-core": "15.1.1"
+      }
+    },
     "react-dom": {
       "version": "17.0.2",
       "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
         "resolve": "^1.9.0"
       }
     },
+    "redux": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.2.tgz",
+      "integrity": "sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==",
+      "requires": {
+        "@babel/runtime": "^7.9.2"
+      }
+    },
     "regenerate": {
       "version": "1.4.2",
       "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
index 898557ad5531e3f60184e331f146a0673d09bc26..33f7e1c135234abbb7382bf42eb6e0cb7b3f95ff 100644 (file)
@@ -37,6 +37,8 @@
     "prop-types": "^15.8.1",
     "react": "^17.0.2",
     "react-autosize-textarea": "^7.1.0",
+    "react-dnd": "^15.1.1",
+    "react-dnd-html5-backend": "^15.1.2",
     "react-dom": "^17.0.2",
     "react-google-recaptcha": "^2.1.0",
     "react-modal": "^3.14.4",
index d811f610c563f9bf2e6816e954e879cf4639cc08..4a305227488ff3d5545ab196957c1b418813a3b5 100644 (file)
@@ -1,6 +1,10 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useRef } from 'react';
+import PropTypes from 'prop-types';
 import './SideBar.scss';
 
+import { DndProvider, useDrag, useDrop } from 'react-dnd';
+import { HTML5Backend } from 'react-dnd-html5-backend';
+
 import initMatrix from '../../../client/initMatrix';
 import cons from '../../../client/state/cons';
 import colorMXID from '../../../util/colorMXID';
@@ -8,6 +12,7 @@ import {
   selectTab, openShortcutSpaces, openInviteList,
   openSearch, openSettings, openReusableContextMenu,
 } from '../../../client/action/navigation';
+import { moveSpaceShortcut } from '../../../client/action/accountData';
 import { abbreviateNumber, getEventCords } from '../../../util/common';
 
 import Avatar from '../../atoms/avatar/Avatar';
@@ -23,7 +28,21 @@ import SearchIC from '../../../../public/res/ic/outlined/search.svg';
 import InviteIC from '../../../../public/res/ic/outlined/invite.svg';
 
 import { useSelectedTab } from '../../hooks/useSelectedTab';
-import { useSpaceShortcut } from '../../hooks/useSpaceShortcut';
+
+function useNotificationUpdate() {
+  const { notifications } = initMatrix;
+  const [, forceUpdate] = useState({});
+  useEffect(() => {
+    function onNotificationChanged(roomId, total, prevTotal) {
+      if (total === prevTotal) return;
+      forceUpdate({});
+    }
+    notifications.on(cons.events.notifications.NOTI_CHANGED, onNotificationChanged);
+    return () => {
+      notifications.removeListener(cons.events.notifications.NOTI_CHANGED, onNotificationChanged);
+    };
+  }, []);
+}
 
 function ProfileAvatarMenu() {
   const mx = initMatrix.matrixClient;
@@ -66,54 +85,10 @@ function ProfileAvatarMenu() {
   );
 }
 
-function useTotalInvites() {
-  const { roomList } = initMatrix;
-  const totalInviteCount = () => roomList.inviteRooms.size
-    + roomList.inviteSpaces.size
-    + roomList.inviteDirects.size;
-  const [totalInvites, updateTotalInvites] = useState(totalInviteCount());
-
-  useEffect(() => {
-    const onInviteListChange = () => {
-      updateTotalInvites(totalInviteCount());
-    };
-    roomList.on(cons.events.roomList.INVITELIST_UPDATED, onInviteListChange);
-    return () => {
-      roomList.removeListener(cons.events.roomList.INVITELIST_UPDATED, onInviteListChange);
-    };
-  }, []);
-
-  return [totalInvites];
-}
-
-function SideBar() {
+function FeaturedTab() {
   const { roomList, accountData, notifications } = initMatrix;
-  const mx = initMatrix.matrixClient;
-
   const [selectedTab] = useSelectedTab();
-  const [spaceShortcut] = useSpaceShortcut();
-  const [totalInvites] = useTotalInvites();
-  const [, forceUpdate] = useState({});
-
-  useEffect(() => {
-    function onNotificationChanged(roomId, total, prevTotal) {
-      if (total === prevTotal) return;
-      forceUpdate({});
-    }
-    notifications.on(cons.events.notifications.NOTI_CHANGED, onNotificationChanged);
-    return () => {
-      notifications.removeListener(cons.events.notifications.NOTI_CHANGED, onNotificationChanged);
-    };
-  }, []);
-
-  const openSpaceOptions = (e, spaceId) => {
-    e.preventDefault();
-    openReusableContextMenu(
-      'right',
-      getEventCords(e, '.sidebar-avatar'),
-      (closeMenu) => <SpaceOptions roomId={spaceId} afterOptionSelect={closeMenu} />,
-    );
-  };
+  useNotificationUpdate();
 
   function getHomeNoti() {
     const orphans = roomList.getOrphans();
@@ -145,73 +120,219 @@ function SideBar() {
     return noti;
   }
 
-  // TODO: bellow operations are heavy.
-  // refactor this component into more smaller components.
   const dmsNoti = getDMsNoti();
   const homeNoti = getHomeNoti();
 
+  return (
+    <>
+      <SidebarAvatar
+        tooltip="Home"
+        active={selectedTab === cons.tabs.HOME}
+        onClick={() => selectTab(cons.tabs.HOME)}
+        avatar={<Avatar iconSrc={HomeIC} size="normal" />}
+        notificationBadge={homeNoti ? (
+          <NotificationBadge
+            alert={homeNoti?.highlight > 0}
+            content={abbreviateNumber(homeNoti.total) || null}
+          />
+        ) : null}
+      />
+      <SidebarAvatar
+        tooltip="People"
+        active={selectedTab === cons.tabs.DIRECTS}
+        onClick={() => selectTab(cons.tabs.DIRECTS)}
+        avatar={<Avatar iconSrc={UserIC} size="normal" />}
+        notificationBadge={dmsNoti ? (
+          <NotificationBadge
+            alert={dmsNoti?.highlight > 0}
+            content={abbreviateNumber(dmsNoti.total) || null}
+          />
+        ) : null}
+      />
+    </>
+  );
+}
+
+function DraggableSpaceShortcut({
+  isActive, spaceId, index, moveShortcut, onDrop,
+}) {
+  const mx = initMatrix.matrixClient;
+  const { notifications } = initMatrix;
+  const room = mx.getRoom(spaceId);
+  const shortcutRef = useRef(null);
+  const avatarRef = useRef(null);
+
+  const openSpaceOptions = (e, sId) => {
+    e.preventDefault();
+    openReusableContextMenu(
+      'right',
+      getEventCords(e, '.sidebar-avatar'),
+      (closeMenu) => <SpaceOptions roomId={sId} afterOptionSelect={closeMenu} />,
+    );
+  };
+
+  const [, drop] = useDrop({
+    accept: 'SPACE_SHORTCUT',
+    collect(monitor) {
+      return {
+        handlerId: monitor.getHandlerId(),
+      };
+    },
+    drop(item) {
+      onDrop(item.index, item.spaceId);
+    },
+    hover(item, monitor) {
+      if (!shortcutRef.current) return;
+
+      const dragIndex = item.index;
+      const hoverIndex = index;
+      if (dragIndex === hoverIndex) return;
+
+      const hoverBoundingRect = shortcutRef.current?.getBoundingClientRect();
+      const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
+      const clientOffset = monitor.getClientOffset();
+      const hoverClientY = clientOffset.y - hoverBoundingRect.top;
+
+      if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
+        return;
+      }
+      if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
+        return;
+      }
+      moveShortcut(dragIndex, hoverIndex);
+      // eslint-disable-next-line no-param-reassign
+      item.index = hoverIndex;
+    },
+  });
+  const [{ isDragging }, drag] = useDrag({
+    type: 'SPACE_SHORTCUT',
+    item: () => ({ spaceId, index }),
+    collect: (monitor) => ({
+      isDragging: monitor.isDragging(),
+    }),
+  });
+
+  drag(avatarRef);
+  drop(shortcutRef);
+
+  if (shortcutRef.current) {
+    if (isDragging) shortcutRef.current.style.opacity = 0;
+    else shortcutRef.current.style.opacity = 1;
+  }
+
+  return (
+    <SidebarAvatar
+      ref={shortcutRef}
+      active={isActive}
+      tooltip={room.name}
+      onClick={() => selectTab(spaceId)}
+      onContextMenu={(e) => openSpaceOptions(e, spaceId)}
+      avatar={(
+        <Avatar
+          ref={avatarRef}
+          text={room.name}
+          bgColor={colorMXID(room.roomId)}
+          size="normal"
+          imageSrc={room.getAvatarUrl(initMatrix.matrixClient.baseUrl, 42, 42, 'crop') || null}
+        />
+      )}
+      notificationBadge={notifications.hasNoti(spaceId) ? (
+        <NotificationBadge
+          alert={notifications.getHighlightNoti(spaceId) > 0}
+          content={abbreviateNumber(notifications.getTotalNoti(spaceId)) || null}
+        />
+      ) : null}
+    />
+  );
+}
+
+DraggableSpaceShortcut.propTypes = {
+  spaceId: PropTypes.string.isRequired,
+  isActive: PropTypes.bool.isRequired,
+  index: PropTypes.number.isRequired,
+  moveShortcut: PropTypes.func.isRequired,
+  onDrop: PropTypes.func.isRequired,
+};
+
+function SpaceShortcut() {
+  const { accountData } = initMatrix;
+  const [selectedTab] = useSelectedTab();
+  useNotificationUpdate();
+  const [spaceShortcut, setSpaceShortcut] = useState([...accountData.spaceShortcut]);
+
+  useEffect(() => {
+    const handleShortcut = () => setSpaceShortcut([...accountData.spaceShortcut]);
+    accountData.on(cons.events.accountData.SPACE_SHORTCUT_UPDATED, handleShortcut);
+    return () => {
+      accountData.removeListener(cons.events.accountData.SPACE_SHORTCUT_UPDATED, handleShortcut);
+    };
+  }, []);
+
+  const moveShortcut = (dragIndex, hoverIndex) => {
+    const dragSpaceId = spaceShortcut[dragIndex];
+    const newShortcuts = [...spaceShortcut];
+    newShortcuts.splice(dragIndex, 1);
+    newShortcuts.splice(hoverIndex, 0, dragSpaceId);
+    setSpaceShortcut(newShortcuts);
+  };
+
+  const handleDrop = (dragIndex, dragSpaceId) => {
+    if ([...accountData.spaceShortcut][dragIndex] === dragSpaceId) return;
+    moveSpaceShortcut(dragSpaceId, dragIndex);
+  };
+
+  return (
+    <DndProvider backend={HTML5Backend}>
+      {
+        spaceShortcut.map((shortcut, index) => (
+          <DraggableSpaceShortcut
+            key={shortcut}
+            index={index}
+            spaceId={shortcut}
+            isActive={selectedTab === shortcut}
+            moveShortcut={moveShortcut}
+            onDrop={handleDrop}
+          />
+        ))
+      }
+    </DndProvider>
+  );
+}
+
+function useTotalInvites() {
+  const { roomList } = initMatrix;
+  const totalInviteCount = () => roomList.inviteRooms.size
+    + roomList.inviteSpaces.size
+    + roomList.inviteDirects.size;
+  const [totalInvites, updateTotalInvites] = useState(totalInviteCount());
+
+  useEffect(() => {
+    const onInviteListChange = () => {
+      updateTotalInvites(totalInviteCount());
+    };
+    roomList.on(cons.events.roomList.INVITELIST_UPDATED, onInviteListChange);
+    return () => {
+      roomList.removeListener(cons.events.roomList.INVITELIST_UPDATED, onInviteListChange);
+    };
+  }, []);
+
+  return [totalInvites];
+}
+
+function SideBar() {
+  const [totalInvites] = useTotalInvites();
+
   return (
     <div className="sidebar">
       <div className="sidebar__scrollable">
         <ScrollView invisible>
           <div className="scrollable-content">
             <div className="featured-container">
-              <SidebarAvatar
-                tooltip="Home"
-                active={selectedTab === cons.tabs.HOME}
-                onClick={() => selectTab(cons.tabs.HOME)}
-                avatar={<Avatar iconSrc={HomeIC} size="normal" />}
-                notificationBadge={homeNoti ? (
-                  <NotificationBadge
-                    alert={homeNoti?.highlight > 0}
-                    content={abbreviateNumber(homeNoti.total) || null}
-                  />
-                ) : null}
-              />
-              <SidebarAvatar
-                tooltip="People"
-                active={selectedTab === cons.tabs.DIRECTS}
-                onClick={() => selectTab(cons.tabs.DIRECTS)}
-                avatar={<Avatar iconSrc={UserIC} size="normal" />}
-                notificationBadge={dmsNoti ? (
-                  <NotificationBadge
-                    alert={dmsNoti?.highlight > 0}
-                    content={abbreviateNumber(dmsNoti.total) || null}
-                  />
-                ) : null}
-              />
+              <FeaturedTab />
             </div>
             <div className="sidebar-divider" />
             <div className="space-container">
-              {
-                spaceShortcut.map((shortcut) => {
-                  const sRoomId = shortcut;
-                  const room = mx.getRoom(sRoomId);
-                  return (
-                    <SidebarAvatar
-                      active={selectedTab === sRoomId}
-                      key={sRoomId}
-                      tooltip={room.name}
-                      onClick={() => selectTab(shortcut)}
-                      onContextMenu={(e) => openSpaceOptions(e, sRoomId)}
-                      avatar={(
-                        <Avatar
-                          text={room.name}
-                          bgColor={colorMXID(room.roomId)}
-                          size="normal"
-                          imageSrc={room.getAvatarUrl(initMatrix.matrixClient.baseUrl, 42, 42, 'crop') || null}
-                        />
-                      )}
-                      notificationBadge={notifications.hasNoti(sRoomId) ? (
-                        <NotificationBadge
-                          alert={notifications.getHighlightNoti(sRoomId) > 0}
-                          content={abbreviateNumber(notifications.getTotalNoti(sRoomId)) || null}
-                        />
-                      ) : null}
-                    />
-                  );
-                })
-              }
+              <SpaceShortcut />
               <SidebarAvatar
                 tooltip="Pin spaces"
                 onClick={() => openShortcutSpaces()}