Add dnd space shortcut (#153)

Signed-off-by: Ajay Bura <ajbura@gmail.com>
This commit is contained in:
Ajay Bura 2022-03-08 16:34:55 +05:30
parent a7a5b08ad8
commit b8fe4c937e
3 changed files with 353 additions and 105 deletions

129
package-lock.json generated
View 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",
@ -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",

View 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",

View 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()}