mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-17 04:30:29 +03:00
initial commit
This commit is contained in:
commit
026f835a87
176 changed files with 10613 additions and 0 deletions
223
src/app/organisms/navigation/Drawer.jsx
Normal file
223
src/app/organisms/navigation/Drawer.jsx
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './Drawer.scss';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import cons from '../../../client/state/cons';
|
||||
import { doesRoomHaveUnread } from '../../../util/matrixUtil';
|
||||
import {
|
||||
selectRoom, openPublicChannels, openCreateChannel, openInviteUser,
|
||||
} from '../../../client/action/navigation';
|
||||
import navigation from '../../../client/state/navigation';
|
||||
|
||||
import Header, { TitleWrapper } from '../../atoms/header/Header';
|
||||
import Text from '../../atoms/text/Text';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
import ScrollView from '../../atoms/scroll/ScrollView';
|
||||
import ContextMenu, { MenuItem, MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
||||
import ChannelSelector from '../../molecules/channel-selector/ChannelSelector';
|
||||
|
||||
import PlusIC from '../../../../public/res/ic/outlined/plus.svg';
|
||||
// import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg';
|
||||
import HashIC from '../../../../public/res/ic/outlined/hash.svg';
|
||||
import HashLockIC from '../../../../public/res/ic/outlined/hash-lock.svg';
|
||||
import HashPlusIC from '../../../../public/res/ic/outlined/hash-plus.svg';
|
||||
import HashSearchIC from '../../../../public/res/ic/outlined/hash-search.svg';
|
||||
import SpaceIC from '../../../../public/res/ic/outlined/space.svg';
|
||||
import SpaceLockIC from '../../../../public/res/ic/outlined/space-lock.svg';
|
||||
|
||||
function AtoZ(aId, bId) {
|
||||
let aName = initMatrix.matrixClient.getRoom(aId).name;
|
||||
let bName = initMatrix.matrixClient.getRoom(bId).name;
|
||||
|
||||
// remove "#" from the room name
|
||||
// To ignore it in sorting
|
||||
aName = aName.replaceAll('#', '');
|
||||
bName = bName.replaceAll('#', '');
|
||||
|
||||
if (aName.toLowerCase() < bName.toLowerCase()) {
|
||||
return -1;
|
||||
}
|
||||
if (aName.toLowerCase() > bName.toLowerCase()) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function DrawerHeader({ tabId }) {
|
||||
return (
|
||||
<Header>
|
||||
<TitleWrapper>
|
||||
<Text variant="s1">{(tabId === 'channels' ? 'Home' : 'Direct messages')}</Text>
|
||||
</TitleWrapper>
|
||||
{(tabId === 'dm')
|
||||
? <IconButton onClick={() => openInviteUser()} tooltip="Start DM" src={PlusIC} size="normal" />
|
||||
: (
|
||||
<ContextMenu
|
||||
content={(hideMenu) => (
|
||||
<>
|
||||
<MenuHeader>Add channel</MenuHeader>
|
||||
<MenuItem
|
||||
iconSrc={HashPlusIC}
|
||||
onClick={() => { hideMenu(); openCreateChannel(); }}
|
||||
>
|
||||
Create new channel
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
iconSrc={HashSearchIC}
|
||||
onClick={() => { hideMenu(); openPublicChannels(); }}
|
||||
>
|
||||
Add Public channel
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
render={(toggleMenu) => (<IconButton onClick={toggleMenu} tooltip="Add channel" src={PlusIC} size="normal" />)}
|
||||
/>
|
||||
)}
|
||||
{/* <IconButton onClick={() => ''} tooltip="Menu" src={VerticalMenuIC} size="normal" /> */}
|
||||
</Header>
|
||||
);
|
||||
}
|
||||
DrawerHeader.propTypes = {
|
||||
tabId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
function DrawerBradcrumb() {
|
||||
return (
|
||||
<div className="breadcrumb__wrapper">
|
||||
<ScrollView horizontal vertical={false}>
|
||||
<div>
|
||||
{/* TODO: bradcrumb space paths when spaces become a thing */}
|
||||
</div>
|
||||
</ScrollView>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderSelector(room, roomId, isSelected, isDM) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
let imageSrc = room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop');
|
||||
if (typeof imageSrc === 'undefined') imageSrc = null;
|
||||
|
||||
return (
|
||||
<ChannelSelector
|
||||
key={roomId}
|
||||
iconSrc={
|
||||
isDM
|
||||
? null
|
||||
: (() => {
|
||||
if (room.isSpaceRoom()) {
|
||||
return (room.getJoinRule() === 'invite' ? SpaceLockIC : SpaceIC);
|
||||
}
|
||||
return (room.getJoinRule() === 'invite' ? HashLockIC : HashIC);
|
||||
})()
|
||||
}
|
||||
imageSrc={isDM ? imageSrc : null}
|
||||
roomId={roomId}
|
||||
unread={doesRoomHaveUnread(room)}
|
||||
onClick={() => selectRoom(roomId)}
|
||||
notificationCount={room.getUnreadNotificationCount('total')}
|
||||
alert={room.getUnreadNotificationCount('highlight') !== 0}
|
||||
selected={isSelected}
|
||||
>
|
||||
{room.name}
|
||||
</ChannelSelector>
|
||||
);
|
||||
}
|
||||
|
||||
function Directs({ selectedRoomId }) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const directIds = [...initMatrix.roomList.directs].sort(AtoZ);
|
||||
|
||||
return directIds.map((id) => renderSelector(mx.getRoom(id), id, selectedRoomId === id, true));
|
||||
}
|
||||
Directs.defaultProps = { selectedRoomId: null };
|
||||
Directs.propTypes = { selectedRoomId: PropTypes.string };
|
||||
|
||||
function Home({ selectedRoomId }) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const spaceIds = [...initMatrix.roomList.spaces].sort(AtoZ);
|
||||
const roomIds = [...initMatrix.roomList.rooms].sort(AtoZ);
|
||||
|
||||
return (
|
||||
<>
|
||||
{ spaceIds.length !== 0 && <Text className="cat-header" variant="b3">Spaces</Text> }
|
||||
{ spaceIds.map((id) => renderSelector(mx.getRoom(id), id, selectedRoomId === id, false)) }
|
||||
{ roomIds.length !== 0 && <Text className="cat-header" variant="b3">Channels</Text> }
|
||||
{ roomIds.map((id) => renderSelector(mx.getRoom(id), id, selectedRoomId === id, false)) }
|
||||
</>
|
||||
);
|
||||
}
|
||||
Home.defaultProps = { selectedRoomId: null };
|
||||
Home.propTypes = { selectedRoomId: PropTypes.string };
|
||||
|
||||
function Channels({ tabId }) {
|
||||
const [selectedRoomId, changeSelectedRoomId] = useState(null);
|
||||
const [, updateState] = useState();
|
||||
|
||||
const selectHandler = (roomId) => changeSelectedRoomId(roomId);
|
||||
const handleDataChanges = () => updateState({});
|
||||
|
||||
const onRoomListChange = () => {
|
||||
const { spaces, rooms, directs } = initMatrix.roomList;
|
||||
if (!(
|
||||
spaces.has(selectedRoomId)
|
||||
|| rooms.has(selectedRoomId)
|
||||
|| directs.has(selectedRoomId))
|
||||
) {
|
||||
selectRoom(null);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
navigation.on(cons.events.navigation.ROOM_SELECTED, selectHandler);
|
||||
initMatrix.roomList.on(cons.events.roomList.ROOMLIST_UPDATED, handleDataChanges);
|
||||
|
||||
return () => {
|
||||
navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectHandler);
|
||||
initMatrix.roomList.removeListener(cons.events.roomList.ROOMLIST_UPDATED, handleDataChanges);
|
||||
};
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
initMatrix.roomList.on(cons.events.roomList.ROOMLIST_UPDATED, onRoomListChange);
|
||||
|
||||
return () => {
|
||||
initMatrix.roomList.removeListener(cons.events.roomList.ROOMLIST_UPDATED, onRoomListChange);
|
||||
};
|
||||
}, [selectedRoomId]);
|
||||
|
||||
return (
|
||||
<div className="channels-container">
|
||||
{
|
||||
tabId === 'channels'
|
||||
? <Home selectedRoomId={selectedRoomId} />
|
||||
: <Directs selectedRoomId={selectedRoomId} />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Channels.propTypes = {
|
||||
tabId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
function Drawer({ tabId }) {
|
||||
return (
|
||||
<div className="drawer">
|
||||
<DrawerHeader tabId={tabId} />
|
||||
<div className="drawer__content-wrapper">
|
||||
<DrawerBradcrumb />
|
||||
<div className="channels__wrapper">
|
||||
<ScrollView autoHide>
|
||||
<Channels tabId={tabId} />
|
||||
</ScrollView>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Drawer.propTypes = {
|
||||
tabId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default Drawer;
|
||||
48
src/app/organisms/navigation/Drawer.scss
Normal file
48
src/app/organisms/navigation/Drawer.scss
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
.drawer-flexBox {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.drawer-flexItem {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.drawer {
|
||||
@extend .drawer-flexItem;
|
||||
@extend .drawer-flexBox;
|
||||
min-width: 0;
|
||||
border-right: 1px solid var(--bg-surface-border);
|
||||
|
||||
[dir=rtl] & {
|
||||
border-right: none;
|
||||
border-left: 1px solid var(--bg-surface-border);
|
||||
}
|
||||
|
||||
&__content-wrapper {
|
||||
@extend .drawer-flexItem;
|
||||
@extend .drawer-flexBox;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb__wrapper {
|
||||
display: none;
|
||||
height: var(--header-height);
|
||||
}
|
||||
.channels__wrapper {
|
||||
@extend .drawer-flexItem;
|
||||
}
|
||||
|
||||
.channels-container {
|
||||
padding-bottom: var(--sp-extra-loose);
|
||||
|
||||
& > .channel-selector__button-wrapper:first-child {
|
||||
margin-top: var(--sp-extra-tight);
|
||||
}
|
||||
|
||||
& .cat-header {
|
||||
margin: var(--sp-normal);
|
||||
margin-bottom: var(--sp-extra-tight);
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
36
src/app/organisms/navigation/Navigation.jsx
Normal file
36
src/app/organisms/navigation/Navigation.jsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import './Navigation.scss';
|
||||
|
||||
import cons from '../../../client/state/cons';
|
||||
import navigation from '../../../client/state/navigation';
|
||||
import { handleTabChange } from '../../../client/action/navigation';
|
||||
|
||||
import SideBar from './SideBar';
|
||||
import Drawer from './Drawer';
|
||||
|
||||
function Navigation() {
|
||||
const [activeTab, changeActiveTab] = useState(navigation.getActiveTab());
|
||||
|
||||
function changeTab(tabId) {
|
||||
handleTabChange(tabId);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handleTab = () => {
|
||||
changeActiveTab(navigation.getActiveTab());
|
||||
};
|
||||
navigation.on(cons.events.navigation.TAB_CHANGED, handleTab);
|
||||
|
||||
return () => {
|
||||
navigation.removeListener(cons.events.navigation.TAB_CHANGED, handleTab);
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<div className="navigation">
|
||||
<SideBar tabId={activeTab} changeTab={changeTab} />
|
||||
<Drawer tabId={activeTab} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Navigation;
|
||||
7
src/app/organisms/navigation/Navigation.scss
Normal file
7
src/app/organisms/navigation/Navigation.scss
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
.navigation {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--bg-surface-low);
|
||||
|
||||
display: flex;
|
||||
}
|
||||
118
src/app/organisms/navigation/SideBar.jsx
Normal file
118
src/app/organisms/navigation/SideBar.jsx
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './SideBar.scss';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import cons from '../../../client/state/cons';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
import logout from '../../../client/action/logout';
|
||||
import { openInviteList, openPublicChannels, openSettings } from '../../../client/action/navigation';
|
||||
|
||||
import ScrollView from '../../atoms/scroll/ScrollView';
|
||||
import SidebarAvatar from '../../molecules/sidebar-avatar/SidebarAvatar';
|
||||
import ContextMenu, { MenuItem, MenuHeader, MenuBorder } from '../../atoms/context-menu/ContextMenu';
|
||||
|
||||
import HomeIC from '../../../../public/res/ic/outlined/home.svg';
|
||||
import UserIC from '../../../../public/res/ic/outlined/user.svg';
|
||||
import HashSearchIC from '../../../../public/res/ic/outlined/hash-search.svg';
|
||||
import InviteIC from '../../../../public/res/ic/outlined/invite.svg';
|
||||
import SettingsIC from '../../../../public/res/ic/outlined/settings.svg';
|
||||
import PowerIC from '../../../../public/res/ic/outlined/power.svg';
|
||||
|
||||
function ProfileAvatarMenu() {
|
||||
const mx = initMatrix.matrixClient;
|
||||
|
||||
return (
|
||||
<ContextMenu
|
||||
content={(hideMenu) => (
|
||||
<>
|
||||
<MenuHeader>{mx.getUserId()}</MenuHeader>
|
||||
{/* <MenuItem iconSrc={UserIC} onClick={() => ''}>Profile</MenuItem> */}
|
||||
{/* <MenuItem iconSrc={BellIC} onClick={() => ''}>Notification settings</MenuItem> */}
|
||||
<MenuItem
|
||||
iconSrc={SettingsIC}
|
||||
onClick={() => { hideMenu(); openSettings(); }}
|
||||
>
|
||||
Settings
|
||||
</MenuItem>
|
||||
<MenuBorder />
|
||||
<MenuItem iconSrc={PowerIC} variant="danger" onClick={logout}>Logout</MenuItem>
|
||||
</>
|
||||
)}
|
||||
render={(toggleMenu) => (
|
||||
<SidebarAvatar
|
||||
onClick={toggleMenu}
|
||||
tooltip={mx.getUser(mx.getUserId()).displayName}
|
||||
imageSrc={mx.getUser(mx.getUserId()).avatarUrl !== null ? mx.mxcUrlToHttp(mx.getUser(mx.getUserId()).avatarUrl, 42, 42, 'crop') : null}
|
||||
bgColor={colorMXID(mx.getUserId())}
|
||||
text={mx.getUser(mx.getUserId()).displayName.slice(0, 1)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SideBar({ tabId, changeTab }) {
|
||||
const totalInviteCount = () => initMatrix.roomList.inviteRooms.size
|
||||
+ initMatrix.roomList.inviteSpaces.size
|
||||
+ initMatrix.roomList.inviteDirects.size;
|
||||
|
||||
const [totalInvites, updateTotalInvites] = useState(totalInviteCount());
|
||||
|
||||
function onInviteListChange() {
|
||||
updateTotalInvites(totalInviteCount());
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
initMatrix.roomList.on(
|
||||
cons.events.roomList.INVITELIST_UPDATED,
|
||||
onInviteListChange,
|
||||
);
|
||||
|
||||
return () => {
|
||||
initMatrix.roomList.removeListener(
|
||||
cons.events.roomList.INVITELIST_UPDATED,
|
||||
onInviteListChange,
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="sidebar">
|
||||
<div className="sidebar__scrollable">
|
||||
<ScrollView invisible>
|
||||
<div className="scrollable-content">
|
||||
<div className="featured-container">
|
||||
<SidebarAvatar active={tabId === 'channels'} onClick={() => changeTab('channels')} tooltip="Home" iconSrc={HomeIC} />
|
||||
<SidebarAvatar active={tabId === 'dm'} onClick={() => changeTab('dm')} tooltip="People" iconSrc={UserIC} />
|
||||
<SidebarAvatar onClick={() => openPublicChannels()} tooltip="Public channels" iconSrc={HashSearchIC} />
|
||||
</div>
|
||||
<div className="sidebar-divider" />
|
||||
<div className="space-container" />
|
||||
</div>
|
||||
</ScrollView>
|
||||
</div>
|
||||
<div className="sidebar__sticky">
|
||||
<div className="sidebar-divider" />
|
||||
<div className="sticky-container">
|
||||
{ totalInvites !== 0 && (
|
||||
<SidebarAvatar
|
||||
notifyCount={totalInvites}
|
||||
onClick={() => openInviteList()}
|
||||
tooltip="Invites"
|
||||
iconSrc={InviteIC}
|
||||
/>
|
||||
)}
|
||||
<ProfileAvatarMenu />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SideBar.propTypes = {
|
||||
tabId: PropTypes.string.isRequired,
|
||||
changeTab: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default SideBar;
|
||||
70
src/app/organisms/navigation/SideBar.scss
Normal file
70
src/app/organisms/navigation/SideBar.scss
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
.sidebar__flexBox {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
@extend .sidebar__flexBox;
|
||||
width: var(--navigation-sidebar-width);
|
||||
height: 100%;
|
||||
border-right: 1px solid var(--bg-surface-border);
|
||||
|
||||
[dir=rtl] & {
|
||||
border-right: none;
|
||||
border-left: 1px solid var(--bg-surface-border);
|
||||
}
|
||||
|
||||
&__scrollable,
|
||||
&__sticky {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__scrollable {
|
||||
flex: 1;
|
||||
min-height: 0px;
|
||||
}
|
||||
|
||||
&__sticky {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.scrollable-content {
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
|
||||
background: transparent;
|
||||
// background-image: linear-gradient(to top, var(--bg-surface-low), transparent);
|
||||
// It produce bug in safari
|
||||
// To fix it, we have to set the color as a fully transparent version of that exact color. like:
|
||||
// background-image: linear-gradient(to top, rgb(255, 255, 255), rgba(255, 255, 255, 0));
|
||||
// TODO: fix this bug while implementing spaces
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.featured-container,
|
||||
.space-container,
|
||||
.sticky-container {
|
||||
@extend .sidebar__flexBox;
|
||||
|
||||
padding: var(--sp-ultra-tight) 0;
|
||||
|
||||
& > .sidebar-avatar,
|
||||
& > .avatar-container {
|
||||
margin: calc(var(--sp-tight) / 2) 0;
|
||||
}
|
||||
}
|
||||
.sidebar-divider {
|
||||
margin: auto;
|
||||
width: 24px;
|
||||
height: 1px;
|
||||
background-color: var(--bg-surface-border);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue