import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { debounce } from 'lodash';
import Fuse from 'fuse.js';
import InputAdornment from '@material-ui/core/InputAdornment';
import classnames from 'classnames';
import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app';
import {
EVENT,
EVENT_NAMES,
CONTEXT_PROPS,
} from '../../../../shared/constants/metametrics';
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
import Identicon from '../../ui/identicon';
import SiteIcon from '../../ui/site-icon';
import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display';
import {
PRIMARY,
///: BEGIN:ONLY_INCLUDE_IN(beta,flask)
SUPPORT_REQUEST_LINK,
///: END:ONLY_INCLUDE_IN
} from '../../../helpers/constants/common';
import {
SETTINGS_ROUTE,
NEW_ACCOUNT_ROUTE,
IMPORT_ACCOUNT_ROUTE,
CONNECT_HARDWARE_ROUTE,
DEFAULT_ROUTE,
///: BEGIN:ONLY_INCLUDE_IN(flask)
NOTIFICATIONS_ROUTE,
///: END:ONLY_INCLUDE_IN
} from '../../../helpers/constants/routes';
import TextField from '../../ui/text-field';
import IconCheck from '../../ui/icon/icon-check';
import IconSpeechBubbles from '../../ui/icon/icon-speech-bubbles';
import IconConnect from '../../ui/icon/icon-connect';
import IconCog from '../../ui/icon/icon-cog';
import IconPlus from '../../ui/icon/icon-plus';
import IconImport from '../../ui/icon/icon-import';
import Button from '../../ui/button';
import SearchIcon from '../../ui/icon/search-icon';
import { SUPPORT_LINK } from '../../../../shared/lib/ui-utils';
import KeyRingLabel from './keyring-label';
export function AccountMenuItem(props) {
const { icon, children, text, subText, className, onClick } = props;
const itemClassName = classnames('account-menu__item', className, {
'account-menu__item--clickable': Boolean(onClick),
});
return children ? (
{children}
) : (
);
}
AccountMenuItem.propTypes = {
icon: PropTypes.node,
children: PropTypes.node,
text: PropTypes.node,
subText: PropTypes.node,
onClick: PropTypes.func,
className: PropTypes.string,
};
export default class AccountMenu extends Component {
static contextTypes = {
t: PropTypes.func,
trackEvent: PropTypes.func,
};
static propTypes = {
shouldShowAccountsSearch: PropTypes.bool,
accounts: PropTypes.array,
history: PropTypes.object,
isAccountMenuOpen: PropTypes.bool,
keyrings: PropTypes.array,
lockMetamask: PropTypes.func,
selectedAddress: PropTypes.string,
showAccountDetail: PropTypes.func,
toggleAccountMenu: PropTypes.func,
addressConnectedSubjectMap: PropTypes.object,
originOfCurrentTab: PropTypes.string,
///: BEGIN:ONLY_INCLUDE_IN(flask)
unreadNotificationsCount: PropTypes.number,
///: END:ONLY_INCLUDE_IN
};
accountsRef;
state = {
shouldShowScrollButton: false,
searchQuery: '',
};
addressFuse = new Fuse([], {
threshold: 0.55,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
ignoreFieldNorm: true,
keys: [
{ name: 'name', weight: 0.5 },
{ name: 'address', weight: 0.5 },
],
});
componentDidUpdate(prevProps, prevState) {
const { isAccountMenuOpen: prevIsAccountMenuOpen } = prevProps;
const { searchQuery: prevSearchQuery } = prevState;
const { isAccountMenuOpen } = this.props;
const { searchQuery } = this.state;
if (!prevIsAccountMenuOpen && isAccountMenuOpen) {
this.setShouldShowScrollButton();
this.resetSearchQuery();
}
// recalculate on each search query change
// whether we can show scroll down button
if (isAccountMenuOpen && prevSearchQuery !== searchQuery) {
this.setShouldShowScrollButton();
}
}
renderAccountsSearch() {
const handleChange = (e) => {
const val = e.target.value.length > 1 ? e.target.value : '';
this.setSearchQuery(val);
};
const inputAdornment = (
);
return [
,
,
];
}
renderAccounts() {
const {
accounts,
selectedAddress,
keyrings,
showAccountDetail,
addressConnectedSubjectMap,
originOfCurrentTab,
} = this.props;
const { searchQuery } = this.state;
let filteredIdentities = accounts;
if (searchQuery) {
this.addressFuse.setCollection(accounts);
filteredIdentities = this.addressFuse.search(searchQuery);
}
if (filteredIdentities.length === 0) {
return (
{this.context.t('noAccountsFound')}
);
}
return filteredIdentities.map((identity) => {
const isSelected = identity.address === selectedAddress;
const simpleAddress = identity.address.substring(2).toLowerCase();
const keyring = keyrings.find((kr) => {
return (
kr.accounts.includes(simpleAddress) ||
kr.accounts.includes(identity.address)
);
});
const addressSubjects =
addressConnectedSubjectMap[identity.address] || {};
const iconAndNameForOpenSubject = addressSubjects[originOfCurrentTab];
return (
);
});
}
resetSearchQuery() {
this.setSearchQuery('');
}
setSearchQuery(searchQuery) {
this.setState({ searchQuery });
}
setShouldShowScrollButton = () => {
if (!this.accountsRef) {
return;
}
const { scrollTop, offsetHeight, scrollHeight } = this.accountsRef;
const canScroll = scrollHeight > offsetHeight;
const atAccountListBottom = scrollTop + offsetHeight >= scrollHeight;
const shouldShowScrollButton = canScroll && !atAccountListBottom;
this.setState({ shouldShowScrollButton });
};
onScroll = debounce(this.setShouldShowScrollButton, 25);
handleScrollDown = (e) => {
e.stopPropagation();
const { scrollHeight } = this.accountsRef;
this.accountsRef.scroll({ left: 0, top: scrollHeight, behavior: 'smooth' });
this.setShouldShowScrollButton();
};
renderScrollButton() {
if (!this.state.shouldShowScrollButton) {
return null;
}
return (
);
}
render() {
const { t, trackEvent } = this.context;
const {
shouldShowAccountsSearch,
isAccountMenuOpen,
toggleAccountMenu,
lockMetamask,
history,
///: BEGIN:ONLY_INCLUDE_IN(flask)
unreadNotificationsCount,
///: END:ONLY_INCLUDE_IN
} = this.props;
if (!isAccountMenuOpen) {
return null;
}
let supportText = t('support');
let supportLink = SUPPORT_LINK;
///: BEGIN:ONLY_INCLUDE_IN(beta,flask)
supportText = t('needHelpSubmitTicket');
supportLink = SUPPORT_REQUEST_LINK;
///: END:ONLY_INCLUDE_IN
return (
{t('myAccounts')}
{shouldShowAccountsSearch ? this.renderAccountsSearch() : null}
{
this.accountsRef = ref;
}}
>
{this.renderAccounts()}
{this.renderScrollButton()}
{
toggleAccountMenu();
trackEvent({
category: EVENT.CATEGORIES.NAVIGATION,
event: EVENT_NAMES.ACCOUNT_ADD_SELECTED,
properties: {
account_type: EVENT.ACCOUNT_TYPES.DEFAULT,
location: 'Main Menu',
},
});
history.push(NEW_ACCOUNT_ROUTE);
}}
icon={}
text={t('createAccount')}
/>
{
toggleAccountMenu();
trackEvent({
category: EVENT.CATEGORIES.NAVIGATION,
event: EVENT_NAMES.ACCOUNT_ADD_SELECTED,
properties: {
account_type: EVENT.ACCOUNT_TYPES.IMPORTED,
location: 'Main Menu',
},
});
history.push(IMPORT_ACCOUNT_ROUTE);
}}
icon={
}
text={t('importAccount')}
/>
{
toggleAccountMenu();
trackEvent({
category: EVENT.CATEGORIES.NAVIGATION,
event: EVENT_NAMES.ACCOUNT_ADD_SELECTED,
properties: {
account_type: EVENT.ACCOUNT_TYPES.HARDWARE,
location: 'Main Menu',
},
});
if (getEnvironmentType() === ENVIRONMENT_TYPE_POPUP) {
global.platform.openExtensionInBrowser(CONNECT_HARDWARE_ROUTE);
} else {
history.push(CONNECT_HARDWARE_ROUTE);
}
}}
icon={
}
text={t('connectHardwareWallet')}
/>
{
///: BEGIN:ONLY_INCLUDE_IN(flask)
<>
{
toggleAccountMenu();
history.push(NOTIFICATIONS_ROUTE);
}}
icon={
{unreadNotificationsCount > 0 && (
{unreadNotificationsCount}
)}
}
text={t('notifications')}
/>
>
///: END:ONLY_INCLUDE_IN
}
{
trackEvent(
{
category: EVENT.CATEGORIES.NAVIGATION,
event: EVENT_NAMES.SUPPORT_LINK_CLICKED,
properties: {
url: supportLink,
},
},
{
contextPropsIntoEventProperties: [CONTEXT_PROPS.PAGE_TITLE],
},
);
global.platform.openTab({ url: supportLink });
}}
icon={
}
text={supportText}
/>
{
toggleAccountMenu();
history.push(SETTINGS_ROUTE);
this.context.trackEvent({
category: EVENT.CATEGORIES.NAVIGATION,
event: EVENT_NAMES.NAV_SETTINGS_OPENED,
properties: {
location: 'Main Menu',
},
});
}}
icon={
}
text={t('settings')}
/>
);
}
}