Skip to content

Commit

Permalink
[IMPROVE] Rewrite User Dropdown and Kebab menu. (RocketChat#20070)
Browse files Browse the repository at this point in the history
Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com>
Co-authored-by: Guilherme Gazzo <guilherme@gazzo.xyz>
  • Loading branch information
3 people committed Jan 12, 2021
1 parent 3483c8b commit c444e5d
Show file tree
Hide file tree
Showing 11 changed files with 239 additions and 221 deletions.
2 changes: 1 addition & 1 deletion app/livechat/client/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Tracker.autorun((c) => {

AccountBox.addItem({
name: 'Omnichannel',
icon: 'omnichannel',
icon: 'headset',
href: '/omnichannel/current',
sideNav: 'omnichannelFlex',
condition: () => settings.get('Livechat_enabled') && hasAllPermission('view-livechat-manager'),
Expand Down
125 changes: 14 additions & 111 deletions client/sidebar/header/UserAvatarButton.js
Original file line number Diff line number Diff line change
@@ -1,131 +1,32 @@
import React from 'react';
import { Meteor } from 'meteor/meteor';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Box } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { css } from '@rocket.chat/css-in-js';

import { popover, modal, AccountBox } from '../../../app/ui-utils';
import { popover } from '../../../app/ui-utils';
import { useSetting } from '../../contexts/SettingsContext';
import { useTranslation } from '../../contexts/TranslationContext';
import { UserStatus } from '../../components/UserStatus';
import { userStatus } from '../../../app/user-status';
import { callbacks } from '../../../app/callbacks';
import { createTemplateForComponent } from '../../reactAdapters';
import UserAvatar from '../../components/avatar/UserAvatar';

const setStatus = (status, statusText) => {
AccountBox.setStatus(status, statusText);
callbacks.run('userStatusManuallySet', status);
popover.close();
};
const UserDropdown = createTemplateForComponent('UserDropdown', () => import('./UserDropdown'));

const onClick = (e, t, allowAnonymousRead) => {
const openDropdown = (e, user, onClose, allowAnonymousRead) => {
if (!(Meteor.userId() == null && allowAnonymousRead)) {
const user = Meteor.user();
const STATUS_MAP = [
'offline',
'online',
'away',
'busy',
];
const userStatusList = Object.keys(userStatus.list).map((key) => {
const status = userStatus.list[key];
const name = status.localizeName ? t(status.name) : status.name;
const modifier = status.statusType || user.status;
const defaultStatus = STATUS_MAP.includes(status.id);
const statusText = defaultStatus ? null : name;

return {
icon: 'circle',
name,
modifier,
action: () => setStatus(status.statusType, statusText),
};
});

const statusText = user.statusText || t(user.status);

userStatusList.push({
icon: 'edit',
name: t('Edit_Status'),
type: 'open',
action: (e) => {
e.preventDefault();
modal.open({
title: t('Edit_Status'),
content: 'editStatus',
data: {
onSave() {
modal.close();
},
},
modalClass: 'modal',
showConfirmButton: false,
showCancelButton: false,
confirmOnEnter: false,
});
},
});

const config = {
popoverClass: 'sidebar-header',
columns: [
{
groups: [
{
title: user.name,
items: [{
icon: 'circle',
name: statusText,
modifier: user.status,
}],
},
{
title: t('User'),
items: userStatusList,
},
{
items: [
{
icon: 'user',
name: t('My_Account'),
type: 'open',
id: 'account',
action: () => {
FlowRouter.go('account');
popover.close();
},
},
{
icon: 'sign-out',
name: t('Logout'),
type: 'open',
id: 'logout',
action: () => {
Meteor.logout(() => {
callbacks.run('afterLogoutCleanUp', user);
Meteor.call('logoutCleanUp', user);
FlowRouter.go('home');
popover.close();
});
},
},
],
},
],
},
],
popover.open({
template: UserDropdown,
currentTarget: e.currentTarget,
data: {
user,
onClose,
},
offsetVertical: e.currentTarget.clientHeight + 10,
};

popover.open(config);
});
}
};

export default React.memo(({ user = {} }) => {
const t = useTranslation();

const {
_id: uid,
status = !uid && 'online',
Expand All @@ -135,7 +36,9 @@ export default React.memo(({ user = {} }) => {

const allowAnonymousRead = useSetting('Accounts_AllowAnonymousRead');

const handleClick = useMutableCallback((e) => uid && onClick(e, t, allowAnonymousRead));
const onClose = useMutableCallback(() => popover.close());

const handleClick = useMutableCallback((e) => uid && openDropdown(e, user, onClose, allowAnonymousRead));

return <Box position='relative' onClick={handleClick} className={css`cursor: pointer;`} data-qa='sidebar-avatar-button'>
<UserAvatar size='x24' username={username} etag={avatarETag}/>
Expand Down
171 changes: 171 additions & 0 deletions client/sidebar/header/UserDropdown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import React from 'react';
import { Box, Margins, Divider, Option } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { Meteor } from 'meteor/meteor';
import { FlowRouter } from 'meteor/kadira:flow-router';

import UserAvatar from '../../components/avatar/UserAvatar';
import { UserStatus } from '../../components/UserStatus';
import { useSetting } from '../../contexts/SettingsContext';
import { useTranslation } from '../../contexts/TranslationContext';
import { useRoute } from '../../contexts/RouterContext';
import { useReactiveValue } from '../../hooks/useReactiveValue';
import { useAtLeastOnePermission } from '../../contexts/AuthorizationContext';
import { userStatus } from '../../../app/user-status';
import { callbacks } from '../../../app/callbacks';
import { popover, AccountBox, modal, SideNav } from '../../../app/ui-utils';

const ADMIN_PERMISSIONS = [
'view-logs',
'manage-emoji',
'manage-sounds',
'view-statistics',
'manage-oauth-apps',
'view-privileged-setting',
'manage-selected-settings',
'view-room-administration',
'view-user-administration',
'access-setting-permissions',
'manage-outgoing-integrations',
'manage-incoming-integrations',
'manage-own-outgoing-integrations',
'manage-own-incoming-integrations',
];

const style = {
marginInline: '-16px',
};

const setStatus = (status, statusText) => {
AccountBox.setStatus(status, statusText);
callbacks.run('userStatusManuallySet', status);
};

const getItems = () => AccountBox.getItems();

const UserDropdown = ({ user, onClose }) => {
const t = useTranslation();
const homeRoute = useRoute('home');
const accountRoute = useRoute('account');
const adminRoute = useRoute('admin');

const {
name,
username,
avatarETag,
status,
statusText,
} = user;

const useRealName = useSetting('UI_Use_Real_Name');
const showAdmin = useAtLeastOnePermission(ADMIN_PERMISSIONS);


const handleCustomStatus = useMutableCallback((e) => {
e.preventDefault();
modal.open({
title: t('Edit_Status'),
content: 'editStatus',
data: {
onSave() {
modal.close();
},
},
modalClass: 'modal',
showConfirmButton: false,
showCancelButton: false,
confirmOnEnter: false,
});
onClose();
});

const handleLogout = useMutableCallback(() => {
Meteor.logout(() => {
callbacks.run('afterLogoutCleanUp', user);
Meteor.call('logoutCleanUp', user);
homeRoute.push({});
popover.close();
});
});

const handleMyAccount = useMutableCallback(() => {
accountRoute.push({});
popover.close();
});

const handleAdmin = useMutableCallback(() => {
adminRoute.push({ group: 'info' });
popover.close();
});

const accountBoxItems = useReactiveValue(getItems);

return <Box display='flex' flexDirection='column' maxWidth='244px'>

<Box display='flex' flexDirection='row' mi='neg-x8' >
<Box mie='x4' mis='x8'><UserAvatar size='x36' username={username} etag={avatarETag} /></Box>
<Box mie='x8' mis='x4' display='flex' overflow='hidden' flexDirection='column' fontScale='p1' mb='neg-x4' flexGrow={1} flexShrink={1}>
<Box withTruncatedText w='full' display='flex' alignItems='center' flexDirection='row'>
<Margins inline='x4'>
<UserStatus status={status}/>
<Box is='span' withTruncatedText display='inline-block'>{useRealName ? name || username : username}</Box>
</Margins>
</Box>
<Box color='hint' withTruncatedText display='inline-block'>
{statusText || t(status)}
</Box>
</Box>
</Box>

<Divider mi='neg-x16' mb='x16' borderColor='muted'/>
<div style={style}>
<Box pi='x16' fontScale='c1' textTransform='uppercase'>{t('Status')}</Box>
{Object.keys(userStatus.list).map((key) => {
const status = userStatus.list[key];
const name = status.localizeName ? t(status.name) : status.name;
const modifier = status.statusType || user.status;

return <Option onClick={() => { setStatus(status.statusType, name); onClose(); }}>
<Option.Column><UserStatus status={modifier}/></Option.Column>
<Option.Content withTruncatedText fontScale='p2'>{name}</Option.Content>
</Option>;
})}
<Option icon='emoji' label={`${ t('Custom_Status') }...`} onClick={handleCustomStatus}/>
</div>

{(accountBoxItems.length || showAdmin) && <>
<Divider mi='neg-x16' mb='x16'/>
<div style={style}>
{showAdmin && <Option icon={'customize'} label={t('Administration')} onClick={handleAdmin}/>}
{accountBoxItems.map((item, i) => {
let action;

if (item.href || item.sideNav) {
action = () => {
if (item.href) {
FlowRouter.go(item.href);
popover.close();
}
if (item.sideNav) {
SideNav.setFlex(item.sideNav);
SideNav.openFlex();
popover.close();
}
};
}

return <Option icon={item.icon} label={t(item.name)} onClick={action} key={i}/>;
})}
</div>
</>}

<Divider mi='neg-x16' mb='x16'/>
<div style={style}>
<Option icon='user' label={t('My_Account')} onClick={handleMyAccount}/>
<Option icon='sign-out' label={t('Logout')} onClick={handleLogout}/>
</div>

</Box>;
};

export default UserDropdown;

0 comments on commit c444e5d

Please sign in to comment.