You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
441 lines
12 KiB
441 lines
12 KiB
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 { 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 } from '../../../helpers/constants/common';
|
|
import {
|
|
SETTINGS_ROUTE,
|
|
NEW_ACCOUNT_ROUTE,
|
|
IMPORT_ACCOUNT_ROUTE,
|
|
CONNECT_HARDWARE_ROUTE,
|
|
DEFAULT_ROUTE,
|
|
} from '../../../helpers/constants/routes';
|
|
import TextField from '../../ui/text-field';
|
|
import SearchIcon from '../../ui/search-icon';
|
|
|
|
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 ? (
|
|
<div className={itemClassName} onClick={onClick}>
|
|
{children}
|
|
</div>
|
|
) : (
|
|
<div className={itemClassName} onClick={onClick}>
|
|
{icon ? <div className="account-menu__item__icon">{icon}</div> : null}
|
|
{text ? <div className="account-menu__item__text">{text}</div> : null}
|
|
{subText ? (
|
|
<div className="account-menu__item__subtext">{subText}</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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,
|
|
metricsEvent: 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,
|
|
addressConnectedDomainMap: PropTypes.object,
|
|
originOfCurrentTab: PropTypes.string,
|
|
};
|
|
|
|
accountsRef;
|
|
|
|
state = {
|
|
shouldShowScrollButton: false,
|
|
searchQuery: '',
|
|
};
|
|
|
|
addressFuse = new Fuse([], {
|
|
threshold: 0.45,
|
|
location: 0,
|
|
distance: 100,
|
|
maxPatternLength: 32,
|
|
minMatchCharLength: 1,
|
|
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 inputAdornment = (
|
|
<InputAdornment
|
|
position="start"
|
|
style={{
|
|
maxHeight: 'none',
|
|
marginRight: 0,
|
|
marginLeft: '8px',
|
|
}}
|
|
>
|
|
<SearchIcon />
|
|
</InputAdornment>
|
|
);
|
|
|
|
return [
|
|
<TextField
|
|
key="search-text-field"
|
|
id="search-accounts"
|
|
placeholder={this.context.t('searchAccounts')}
|
|
type="text"
|
|
value={this.state.searchQuery}
|
|
onChange={(e) => this.setSearchQuery(e.target.value)}
|
|
startAdornment={inputAdornment}
|
|
fullWidth
|
|
theme="material-white-padded"
|
|
/>,
|
|
<div className="account-menu__divider" key="search-divider" />,
|
|
];
|
|
}
|
|
|
|
renderAccounts() {
|
|
const {
|
|
accounts,
|
|
selectedAddress,
|
|
keyrings,
|
|
showAccountDetail,
|
|
addressConnectedDomainMap,
|
|
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 (
|
|
<p className="account-menu__no-accounts">
|
|
{this.context.t('noAccountsFound')}
|
|
</p>
|
|
);
|
|
}
|
|
|
|
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 addressDomains = addressConnectedDomainMap[identity.address] || {};
|
|
const iconAndNameForOpenDomain = addressDomains[originOfCurrentTab];
|
|
|
|
return (
|
|
<div
|
|
className="account-menu__account account-menu__item--clickable"
|
|
onClick={() => {
|
|
this.context.metricsEvent({
|
|
eventOpts: {
|
|
category: 'Navigation',
|
|
action: 'Main Menu',
|
|
name: 'Switched Account',
|
|
},
|
|
});
|
|
showAccountDetail(identity.address);
|
|
}}
|
|
key={identity.address}
|
|
>
|
|
<div className="account-menu__check-mark">
|
|
{isSelected && <div className="account-menu__check-mark-icon" />}
|
|
</div>
|
|
<Identicon address={identity.address} diameter={24} />
|
|
<div className="account-menu__account-info">
|
|
<div className="account-menu__name">{identity.name || ''}</div>
|
|
<UserPreferencedCurrencyDisplay
|
|
className="account-menu__balance"
|
|
value={identity.balance}
|
|
type={PRIMARY}
|
|
/>
|
|
</div>
|
|
{this.renderKeyringType(keyring)}
|
|
{iconAndNameForOpenDomain ? (
|
|
<div className="account-menu__icon-list">
|
|
<SiteIcon
|
|
icon={iconAndNameForOpenDomain.icon}
|
|
name={iconAndNameForOpenDomain.name}
|
|
size={32}
|
|
/>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
});
|
|
}
|
|
|
|
renderKeyringType(keyring) {
|
|
const { t } = this.context;
|
|
|
|
// Sometimes keyrings aren't loaded yet
|
|
if (!keyring) {
|
|
return null;
|
|
}
|
|
|
|
const { type } = keyring;
|
|
let label;
|
|
|
|
switch (type) {
|
|
case 'Trezor Hardware':
|
|
case 'Ledger Hardware':
|
|
label = t('hardware');
|
|
break;
|
|
case 'Simple Key Pair':
|
|
label = t('imported');
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
|
|
return <div className="keyring-label allcaps">{label}</div>;
|
|
}
|
|
|
|
resetSearchQuery() {
|
|
this.setSearchQuery('');
|
|
}
|
|
|
|
setSearchQuery(searchQuery) {
|
|
this.setState({ searchQuery });
|
|
}
|
|
|
|
setShouldShowScrollButton = () => {
|
|
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 (
|
|
<div
|
|
className="account-menu__scroll-button"
|
|
onClick={this.handleScrollDown}
|
|
>
|
|
<img
|
|
src="./images/icons/down-arrow.svg"
|
|
width="28"
|
|
height="28"
|
|
alt={this.context.t('scrollDown')}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
render() {
|
|
const { t, metricsEvent } = this.context;
|
|
const {
|
|
shouldShowAccountsSearch,
|
|
isAccountMenuOpen,
|
|
toggleAccountMenu,
|
|
lockMetamask,
|
|
history,
|
|
} = this.props;
|
|
|
|
if (!isAccountMenuOpen) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="account-menu">
|
|
<div className="account-menu__close-area" onClick={toggleAccountMenu} />
|
|
<AccountMenuItem className="account-menu__header">
|
|
{t('myAccounts')}
|
|
<button
|
|
className="account-menu__lock-button"
|
|
onClick={() => {
|
|
lockMetamask();
|
|
history.push(DEFAULT_ROUTE);
|
|
}}
|
|
>
|
|
{t('lock')}
|
|
</button>
|
|
</AccountMenuItem>
|
|
<div className="account-menu__divider" />
|
|
<div className="account-menu__accounts-container">
|
|
{shouldShowAccountsSearch ? this.renderAccountsSearch() : null}
|
|
<div
|
|
className="account-menu__accounts"
|
|
onScroll={this.onScroll}
|
|
ref={(ref) => {
|
|
this.accountsRef = ref;
|
|
}}
|
|
>
|
|
{this.renderAccounts()}
|
|
</div>
|
|
{this.renderScrollButton()}
|
|
</div>
|
|
<div className="account-menu__divider" />
|
|
<AccountMenuItem
|
|
onClick={() => {
|
|
toggleAccountMenu();
|
|
metricsEvent({
|
|
eventOpts: {
|
|
category: 'Navigation',
|
|
action: 'Main Menu',
|
|
name: 'Clicked Create Account',
|
|
},
|
|
});
|
|
history.push(NEW_ACCOUNT_ROUTE);
|
|
}}
|
|
icon={
|
|
<img
|
|
className="account-menu__item-icon"
|
|
src="images/plus-btn-white.svg"
|
|
alt={t('createAccount')}
|
|
/>
|
|
}
|
|
text={t('createAccount')}
|
|
/>
|
|
<AccountMenuItem
|
|
onClick={() => {
|
|
toggleAccountMenu();
|
|
metricsEvent({
|
|
eventOpts: {
|
|
category: 'Navigation',
|
|
action: 'Main Menu',
|
|
name: 'Clicked Import Account',
|
|
},
|
|
});
|
|
history.push(IMPORT_ACCOUNT_ROUTE);
|
|
}}
|
|
icon={
|
|
<img
|
|
className="account-menu__item-icon"
|
|
src="images/import-account.svg"
|
|
alt={t('importAccount')}
|
|
/>
|
|
}
|
|
text={t('importAccount')}
|
|
/>
|
|
<AccountMenuItem
|
|
onClick={() => {
|
|
toggleAccountMenu();
|
|
metricsEvent({
|
|
eventOpts: {
|
|
category: 'Navigation',
|
|
action: 'Main Menu',
|
|
name: 'Clicked Connect Hardware',
|
|
},
|
|
});
|
|
if (getEnvironmentType() === ENVIRONMENT_TYPE_POPUP) {
|
|
global.platform.openExtensionInBrowser(CONNECT_HARDWARE_ROUTE);
|
|
} else {
|
|
history.push(CONNECT_HARDWARE_ROUTE);
|
|
}
|
|
}}
|
|
icon={
|
|
<img
|
|
className="account-menu__item-icon"
|
|
src="images/connect-icon.svg"
|
|
alt={t('connectHardwareWallet')}
|
|
/>
|
|
}
|
|
text={t('connectHardwareWallet')}
|
|
/>
|
|
<div className="account-menu__divider" />
|
|
<AccountMenuItem
|
|
onClick={() => {
|
|
global.platform.openTab({ url: 'https://support.metamask.io' });
|
|
}}
|
|
icon={<img src="images/support.svg" alt={t('support')} />}
|
|
text={t('support')}
|
|
/>
|
|
|
|
<AccountMenuItem
|
|
onClick={() => {
|
|
toggleAccountMenu();
|
|
history.push(SETTINGS_ROUTE);
|
|
this.context.metricsEvent({
|
|
eventOpts: {
|
|
category: 'Navigation',
|
|
action: 'Main Menu',
|
|
name: 'Opened Settings',
|
|
},
|
|
});
|
|
}}
|
|
icon={
|
|
<img
|
|
className="account-menu__item-icon"
|
|
src="images/settings.svg"
|
|
/>
|
|
}
|
|
text={t('settings')}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|