2022-03-08 12:04:55 +01:00
|
|
|
import React, { useState, useEffect, useRef } from 'react';
|
|
|
|
import PropTypes from 'prop-types';
|
2021-07-28 15:15:52 +02:00
|
|
|
import './SideBar.scss';
|
|
|
|
|
2022-03-08 12:04:55 +01:00
|
|
|
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
|
|
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
|
|
|
|
2021-07-28 15:15:52 +02:00
|
|
|
import initMatrix from '../../../client/initMatrix';
|
|
|
|
import cons from '../../../client/state/cons';
|
|
|
|
import colorMXID from '../../../util/colorMXID';
|
2021-08-30 05:01:13 +02:00
|
|
|
import {
|
2022-03-06 13:22:04 +01:00
|
|
|
selectTab, openShortcutSpaces, openInviteList,
|
|
|
|
openSearch, openSettings, openReusableContextMenu,
|
2021-08-30 05:01:13 +02:00
|
|
|
} from '../../../client/action/navigation';
|
2022-03-08 12:04:55 +01:00
|
|
|
import { moveSpaceShortcut } from '../../../client/action/accountData';
|
2022-01-29 10:00:42 +01:00
|
|
|
import { abbreviateNumber, getEventCords } from '../../../util/common';
|
2022-04-24 12:12:24 +02:00
|
|
|
import { isCrossVerified } from '../../../util/matrixUtil';
|
2021-07-28 15:15:52 +02:00
|
|
|
|
2022-03-07 16:35:47 +01:00
|
|
|
import Avatar from '../../atoms/avatar/Avatar';
|
|
|
|
import NotificationBadge from '../../atoms/badge/NotificationBadge';
|
2021-07-28 15:15:52 +02:00
|
|
|
import ScrollView from '../../atoms/scroll/ScrollView';
|
|
|
|
import SidebarAvatar from '../../molecules/sidebar-avatar/SidebarAvatar';
|
2022-01-29 10:00:42 +01:00
|
|
|
import SpaceOptions from '../../molecules/space-options/SpaceOptions';
|
2021-07-28 15:15:52 +02:00
|
|
|
|
|
|
|
import HomeIC from '../../../../public/res/ic/outlined/home.svg';
|
|
|
|
import UserIC from '../../../../public/res/ic/outlined/user.svg';
|
2022-03-06 13:22:04 +01:00
|
|
|
import AddPinIC from '../../../../public/res/ic/outlined/add-pin.svg';
|
2021-12-10 12:52:53 +01:00
|
|
|
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
|
2021-07-28 15:15:52 +02:00
|
|
|
import InviteIC from '../../../../public/res/ic/outlined/invite.svg';
|
2022-04-24 12:12:24 +02:00
|
|
|
import ShieldUserIC from '../../../../public/res/ic/outlined/shield-user.svg';
|
2021-07-28 15:15:52 +02:00
|
|
|
|
2022-01-26 12:33:26 +01:00
|
|
|
import { useSelectedTab } from '../../hooks/useSelectedTab';
|
2022-04-24 12:12:24 +02:00
|
|
|
import { useDeviceList } from '../../hooks/useDeviceList';
|
|
|
|
|
|
|
|
import { tabText as settingTabText } from '../settings/Settings';
|
2022-03-08 12:04:55 +01:00
|
|
|
|
|
|
|
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);
|
|
|
|
};
|
|
|
|
}, []);
|
|
|
|
}
|
2022-01-26 12:33:26 +01:00
|
|
|
|
2021-07-28 15:15:52 +02:00
|
|
|
function ProfileAvatarMenu() {
|
|
|
|
const mx = initMatrix.matrixClient;
|
2021-10-25 10:55:06 +02:00
|
|
|
const [profile, setProfile] = useState({
|
|
|
|
avatarUrl: null,
|
|
|
|
displayName: mx.getUser(mx.getUserId()).displayName,
|
|
|
|
});
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
const user = mx.getUser(mx.getUserId());
|
|
|
|
const setNewProfile = (avatarUrl, displayName) => setProfile({
|
|
|
|
avatarUrl: avatarUrl || null,
|
|
|
|
displayName: displayName || profile.displayName,
|
|
|
|
});
|
|
|
|
const onAvatarChange = (event, myUser) => {
|
|
|
|
setNewProfile(myUser.avatarUrl, myUser.displayName);
|
|
|
|
};
|
|
|
|
mx.getProfileInfo(mx.getUserId()).then((info) => {
|
|
|
|
setNewProfile(info.avatar_url, info.displayname);
|
|
|
|
});
|
|
|
|
user.on('User.avatarUrl', onAvatarChange);
|
|
|
|
return () => {
|
|
|
|
user.removeListener('User.avatarUrl', onAvatarChange);
|
|
|
|
};
|
|
|
|
}, []);
|
2021-07-28 15:15:52 +02:00
|
|
|
|
|
|
|
return (
|
2021-12-19 15:35:13 +01:00
|
|
|
<SidebarAvatar
|
|
|
|
onClick={openSettings}
|
2022-03-21 05:16:11 +01:00
|
|
|
tooltip="Settings"
|
2022-03-07 16:35:47 +01:00
|
|
|
avatar={(
|
|
|
|
<Avatar
|
|
|
|
text={profile.displayName}
|
|
|
|
bgColor={colorMXID(mx.getUserId())}
|
|
|
|
size="normal"
|
|
|
|
imageSrc={profile.avatarUrl !== null ? mx.mxcUrlToHttp(profile.avatarUrl, 42, 42, 'crop') : null}
|
|
|
|
/>
|
|
|
|
)}
|
2021-07-28 15:15:52 +02:00
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-04-24 12:12:24 +02:00
|
|
|
function CrossSigninAlert() {
|
|
|
|
const deviceList = useDeviceList();
|
|
|
|
const unverified = deviceList?.filter((device) => !isCrossVerified(device.device_id));
|
|
|
|
|
|
|
|
if (!unverified?.length) return null;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<SidebarAvatar
|
|
|
|
className="sidebar__cross-signin-alert"
|
|
|
|
tooltip={`${unverified.length} unverified sessions`}
|
|
|
|
onClick={() => openSettings(settingTabText.SECURITY)}
|
|
|
|
avatar={<Avatar iconSrc={ShieldUserIC} iconColor="var(--ic-danger-normal)" size="normal" />}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-03-08 12:04:55 +01:00
|
|
|
function FeaturedTab() {
|
2022-02-27 12:32:03 +01:00
|
|
|
const { roomList, accountData, notifications } = initMatrix;
|
2022-01-26 12:33:26 +01:00
|
|
|
const [selectedTab] = useSelectedTab();
|
2022-03-08 12:04:55 +01:00
|
|
|
useNotificationUpdate();
|
2022-01-29 10:00:42 +01:00
|
|
|
|
2021-09-12 17:14:13 +02:00
|
|
|
function getHomeNoti() {
|
|
|
|
const orphans = roomList.getOrphans();
|
|
|
|
let noti = null;
|
|
|
|
|
|
|
|
orphans.forEach((roomId) => {
|
2022-02-27 12:32:03 +01:00
|
|
|
if (accountData.spaceShortcut.has(roomId)) return;
|
2021-09-12 17:14:13 +02:00
|
|
|
if (!notifications.hasNoti(roomId)) return;
|
|
|
|
if (noti === null) noti = { total: 0, highlight: 0 };
|
|
|
|
const childNoti = notifications.getNoti(roomId);
|
|
|
|
noti.total += childNoti.total;
|
|
|
|
noti.highlight += childNoti.highlight;
|
|
|
|
});
|
|
|
|
|
|
|
|
return noti;
|
|
|
|
}
|
|
|
|
function getDMsNoti() {
|
|
|
|
if (roomList.directs.size === 0) return null;
|
|
|
|
let noti = null;
|
|
|
|
|
|
|
|
[...roomList.directs].forEach((roomId) => {
|
|
|
|
if (!notifications.hasNoti(roomId)) return;
|
|
|
|
if (noti === null) noti = { total: 0, highlight: 0 };
|
|
|
|
const childNoti = notifications.getNoti(roomId);
|
|
|
|
noti.total += childNoti.total;
|
|
|
|
noti.highlight += childNoti.highlight;
|
|
|
|
});
|
|
|
|
|
|
|
|
return noti;
|
|
|
|
}
|
|
|
|
|
|
|
|
const dmsNoti = getDMsNoti();
|
|
|
|
const homeNoti = getHomeNoti();
|
|
|
|
|
2022-03-08 12:04:55 +01:00
|
|
|
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();
|
|
|
|
|
2021-07-28 15:15:52 +02:00
|
|
|
return (
|
|
|
|
<div className="sidebar">
|
|
|
|
<div className="sidebar__scrollable">
|
|
|
|
<ScrollView invisible>
|
|
|
|
<div className="scrollable-content">
|
|
|
|
<div className="featured-container">
|
2022-03-08 12:04:55 +01:00
|
|
|
<FeaturedTab />
|
2021-07-28 15:15:52 +02:00
|
|
|
</div>
|
|
|
|
<div className="sidebar-divider" />
|
2021-09-05 15:26:34 +02:00
|
|
|
<div className="space-container">
|
2022-03-08 12:04:55 +01:00
|
|
|
<SpaceShortcut />
|
2022-03-06 13:22:04 +01:00
|
|
|
<SidebarAvatar
|
|
|
|
tooltip="Pin spaces"
|
2022-03-07 16:35:47 +01:00
|
|
|
onClick={() => openShortcutSpaces()}
|
|
|
|
avatar={<Avatar iconSrc={AddPinIC} size="normal" />}
|
2022-03-06 13:22:04 +01:00
|
|
|
/>
|
2021-09-05 15:26:34 +02:00
|
|
|
</div>
|
2021-07-28 15:15:52 +02:00
|
|
|
</div>
|
|
|
|
</ScrollView>
|
|
|
|
</div>
|
|
|
|
<div className="sidebar__sticky">
|
|
|
|
<div className="sidebar-divider" />
|
|
|
|
<div className="sticky-container">
|
2021-12-10 12:52:53 +01:00
|
|
|
<SidebarAvatar
|
|
|
|
tooltip="Search"
|
2022-03-07 16:35:47 +01:00
|
|
|
onClick={() => openSearch()}
|
|
|
|
avatar={<Avatar iconSrc={SearchIC} size="normal" />}
|
2021-12-10 12:52:53 +01:00
|
|
|
/>
|
2021-07-28 15:15:52 +02:00
|
|
|
{ totalInvites !== 0 && (
|
|
|
|
<SidebarAvatar
|
|
|
|
tooltip="Invites"
|
2022-03-07 16:35:47 +01:00
|
|
|
onClick={() => openInviteList()}
|
|
|
|
avatar={<Avatar iconSrc={InviteIC} size="normal" />}
|
|
|
|
notificationBadge={<NotificationBadge alert content={totalInvites} />}
|
2021-07-28 15:15:52 +02:00
|
|
|
/>
|
|
|
|
)}
|
2022-04-24 12:12:24 +02:00
|
|
|
<CrossSigninAlert />
|
2021-07-28 15:15:52 +02:00
|
|
|
<ProfileAvatarMenu />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export default SideBar;
|