diff --git a/package-lock.json b/package-lock.json index 3e8d258..4e6e4df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", @@ -2422,6 +2424,21 @@ "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", @@ -2605,7 +2622,7 @@ "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", @@ -5267,6 +5284,16 @@ "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", @@ -11324,6 +11351,43 @@ "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", @@ -11434,6 +11498,14 @@ "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", @@ -15715,6 +15787,21 @@ "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", @@ -15891,7 +15978,7 @@ "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", @@ -18041,6 +18128,16 @@ "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", @@ -22591,6 +22688,26 @@ "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", @@ -22676,6 +22793,14 @@ "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", diff --git a/package.json b/package.json index 898557a..33f7e1c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/organisms/navigation/SideBar.jsx b/src/app/organisms/navigation/SideBar.jsx index d811f61..4a30522 100644 --- a/src/app/organisms/navigation/SideBar.jsx +++ b/src/app/organisms/navigation/SideBar.jsx @@ -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) => , - ); - }; + 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 ( + <> + selectTab(cons.tabs.HOME)} + avatar={} + notificationBadge={homeNoti ? ( + 0} + content={abbreviateNumber(homeNoti.total) || null} + /> + ) : null} + /> + selectTab(cons.tabs.DIRECTS)} + avatar={} + notificationBadge={dmsNoti ? ( + 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) => , + ); + }; + + 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 ( + selectTab(spaceId)} + onContextMenu={(e) => openSpaceOptions(e, spaceId)} + avatar={( + + )} + notificationBadge={notifications.hasNoti(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 ( + + { + spaceShortcut.map((shortcut, index) => ( + + )) + } + + ); +} + +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 (
- selectTab(cons.tabs.HOME)} - avatar={} - notificationBadge={homeNoti ? ( - 0} - content={abbreviateNumber(homeNoti.total) || null} - /> - ) : null} - /> - selectTab(cons.tabs.DIRECTS)} - avatar={} - notificationBadge={dmsNoti ? ( - 0} - content={abbreviateNumber(dmsNoti.total) || null} - /> - ) : null} - /> +
- { - spaceShortcut.map((shortcut) => { - const sRoomId = shortcut; - const room = mx.getRoom(sRoomId); - return ( - selectTab(shortcut)} - onContextMenu={(e) => openSpaceOptions(e, sRoomId)} - avatar={( - - )} - notificationBadge={notifications.hasNoti(sRoomId) ? ( - 0} - content={abbreviateNumber(notifications.getTotalNoti(sRoomId)) || null} - /> - ) : null} - /> - ); - }) - } + openShortcutSpaces()}