"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",
-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';
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';
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;
);
}
-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();
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()}