1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

More nft ux fixes (#13388)

* a batch of nft ux fixes
This commit is contained in:
Alex Donesky 2022-01-27 11:26:33 -06:00 committed by GitHub
parent a0e97f4681
commit be65eb7339
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 240 additions and 102 deletions

View File

@ -36,6 +36,7 @@ export default class AppStateController extends EventEmitter {
trezorModel: null, trezorModel: null,
...initState, ...initState,
qrHardware: {}, qrHardware: {},
collectiblesDropdownState: {},
}); });
this.timer = null; this.timer = null;
@ -282,4 +283,15 @@ export default class AppStateController extends EventEmitter {
enableEIP1559V2NoticeDismissed, enableEIP1559V2NoticeDismissed,
}); });
} }
/**
* A setter for the `collectiblesDropdownState` property
*
* @param collectiblesDropdownState
*/
updateCollectibleDropDownState(collectiblesDropdownState) {
this.store.updateState({
collectiblesDropdownState,
});
}
} }

View File

@ -1271,6 +1271,9 @@ export default class MetamaskController extends EventEmitter {
setEnableEIP1559V2NoticeDismissed: appStateController.setEnableEIP1559V2NoticeDismissed.bind( setEnableEIP1559V2NoticeDismissed: appStateController.setEnableEIP1559V2NoticeDismissed.bind(
appStateController, appStateController,
), ),
updateCollectibleDropDownState: appStateController.updateCollectibleDropDownState.bind(
appStateController,
),
// EnsController // EnsController
tryReverseResolveAddress: ensController.reverseResolveAddress.bind( tryReverseResolveAddress: ensController.reverseResolveAddress.bind(
ensController, ensController,

View File

@ -30,6 +30,7 @@ import {
getSelectedIdentity, getSelectedIdentity,
} from '../../../selectors'; } from '../../../selectors';
import AssetNavigation from '../../../pages/asset/components/asset-navigation'; import AssetNavigation from '../../../pages/asset/components/asset-navigation';
import Copy from '../../ui/icon/copy-icon.component';
import { getCollectibleContracts } from '../../../ducks/metamask/metamask'; import { getCollectibleContracts } from '../../../ducks/metamask/metamask';
import { DEFAULT_ROUTE, SEND_ROUTE } from '../../../helpers/constants/routes'; import { DEFAULT_ROUTE, SEND_ROUTE } from '../../../helpers/constants/routes';
import { import {
@ -52,10 +53,12 @@ import { ASSET_TYPES, updateSendAsset } from '../../../ducks/send';
import InfoTooltip from '../../ui/info-tooltip'; import InfoTooltip from '../../ui/info-tooltip';
import { ERC721 } from '../../../helpers/constants/common'; import { ERC721 } from '../../../helpers/constants/common';
import { usePrevious } from '../../../hooks/usePrevious'; import { usePrevious } from '../../../hooks/usePrevious';
import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard';
export default function CollectibleDetails({ collectible }) { export default function CollectibleDetails({ collectible }) {
const { const {
image, image,
imageOriginal,
name, name,
description, description,
address, address,
@ -70,6 +73,7 @@ export default function CollectibleDetails({ collectible }) {
const ipfsGateway = useSelector(getIpfsGateway); const ipfsGateway = useSelector(getIpfsGateway);
const collectibleContracts = useSelector(getCollectibleContracts); const collectibleContracts = useSelector(getCollectibleContracts);
const currentNetwork = useSelector(getCurrentChainId); const currentNetwork = useSelector(getCurrentChainId);
const [copied, handleCopy] = useCopyToClipboard();
const collectibleContractName = collectibleContracts.find( const collectibleContractName = collectibleContracts.find(
({ address: contractAddress }) => ({ address: contractAddress }) =>
@ -78,7 +82,10 @@ export default function CollectibleDetails({ collectible }) {
const selectedAccountName = useSelector( const selectedAccountName = useSelector(
(state) => getSelectedIdentity(state).name, (state) => getSelectedIdentity(state).name,
); );
const collectibleImageURL = getAssetImageURL(image, ipfsGateway); const collectibleImageURL = getAssetImageURL(
imageOriginal ?? image,
ipfsGateway,
);
const onRemove = () => { const onRemove = () => {
dispatch(removeAndIgnoreCollectible(address, tokenId)); dispatch(removeAndIgnoreCollectible(address, tokenId));
@ -171,10 +178,7 @@ export default function CollectibleDetails({ collectible }) {
justifyContent={JUSTIFY_CONTENT.CENTER} justifyContent={JUSTIFY_CONTENT.CENTER}
className="collectible-details__card" className="collectible-details__card"
> >
<img <img className="collectible-details__image" src={image} />
className="collectible-details__image"
src={collectibleImageURL}
/>
</Card> </Card>
<Box <Box
flexDirection={FLEX_DIRECTION.COLUMN} flexDirection={FLEX_DIRECTION.COLUMN}
@ -252,7 +256,7 @@ export default function CollectibleDetails({ collectible }) {
href={collectibleImageURL} href={collectibleImageURL}
title={collectibleImageURL} title={collectibleImageURL}
> >
{image} {collectibleImageURL}
</a> </a>
</Typography> </Typography>
</Box> </Box>
@ -270,31 +274,49 @@ export default function CollectibleDetails({ collectible }) {
> >
{t('contractAddress')} {t('contractAddress')}
</Typography> </Typography>
<Typography <Box
color={COLORS.PRIMARY1} display={DISPLAY.FLEX}
variant={TYPOGRAPHY.H6} flexDirection={FLEX_DIRECTION.ROW}
overflowWrap={OVERFLOW_WRAP.BREAK_WORD} className="collectible-details__contract-wrapper"
boxProps={{
margin: 0,
marginBottom: 4,
}}
className="collectible-details__contract-link"
> >
<a <Typography
target="_blank" color={COLORS.PRIMARY1}
rel="noopener noreferrer" variant={TYPOGRAPHY.H6}
href={getTokenTrackerLink( overflowWrap={OVERFLOW_WRAP.BREAK_WORD}
address, boxProps={{
currentNetwork, margin: 0,
null, marginBottom: 4,
null, }}
rpcPrefs, className="collectible-details__contract-link"
)}
title={address}
> >
{inPopUp ? shortenAddress(address) : address} <a
</a> target="_blank"
</Typography> rel="noopener noreferrer"
href={getTokenTrackerLink(
address,
currentNetwork,
null,
null,
rpcPrefs,
)}
title={address}
>
{inPopUp ? shortenAddress(address) : address}
</a>
</Typography>
<button
className="collectible-details__contract-copy-button"
onClick={() => {
handleCopy(address);
}}
>
{copied ? (
t('copiedExclamation')
) : (
<Copy size={15} color="#6a737d" />
)}
</button>
</Box>
</Box> </Box>
{inPopUp ? renderSendButton() : null} {inPopUp ? renderSendButton() : null}
</Box> </Box>
@ -314,6 +336,7 @@ CollectibleDetails.propTypes = {
standard: PropTypes.string, standard: PropTypes.string,
imageThumbnail: PropTypes.string, imageThumbnail: PropTypes.string,
imagePreview: PropTypes.string, imagePreview: PropTypes.string,
imageOriginal: PropTypes.string,
creator: PropTypes.shape({ creator: PropTypes.shape({
address: PropTypes.string, address: PropTypes.string,
config: PropTypes.string, config: PropTypes.string,

View File

@ -53,18 +53,40 @@ $spacer-break-small: 16px;
overflow-wrap: break-word; overflow-wrap: break-word;
} }
&__contract-link, &__contract-wrapper {
&__image-link { max-width: calc(100% - #{$link-title-width});
word-break: break-all; }
@media screen and (max-width: $break-small) { &__contract-copy-button {
overflow: hidden; @include H6;
text-overflow: ellipsis;
white-space: nowrap; width: 80px;
width: 90%; display: flex;
align-items: flex-start;
justify-content: center;
background-color: transparent;
cursor: pointer;
color: var(--ui-4);
border: 0;
&:active {
transform: scale(0.97);
} }
} }
&__contract-link {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__image-link {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 332px;
}
&__link-title { &__link-title {
flex: 0 0 $link-title-width; flex: 0 0 $link-title-width;
max-width: 0 0 $link-title-width; max-width: 0 0 $link-title-width;

View File

@ -1,9 +1,10 @@
import React, { useState } from 'react'; import React, { useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import Box from '../../ui/box'; import Box from '../../ui/box';
import Typography from '../../ui/typography/typography'; import Typography from '../../ui/typography/typography';
import Card from '../../ui/card';
import { import {
COLORS, COLORS,
TYPOGRAPHY, TYPOGRAPHY,
@ -19,6 +20,9 @@ import { getEnvironmentType } from '../../../../app/scripts/lib/util';
import { getIpfsGateway } from '../../../selectors'; import { getIpfsGateway } from '../../../selectors';
import { ASSET_ROUTE } from '../../../helpers/constants/routes'; import { ASSET_ROUTE } from '../../../helpers/constants/routes';
import { getAssetImageURL } from '../../../helpers/utils/util'; import { getAssetImageURL } from '../../../helpers/utils/util';
import { updateCollectibleDropDownState } from '../../../store/actions';
import { usePrevious } from '../../../hooks/usePrevious';
import { getCollectiblesDropdownState } from '../../../ducks/metamask/metamask';
const width = const width =
getEnvironmentType() === ENVIRONMENT_TYPE_POPUP getEnvironmentType() === ENVIRONMENT_TYPE_POPUP
@ -31,20 +35,30 @@ export default function CollectiblesItems({
collections = {}, collections = {},
previouslyOwnedCollection = {}, previouslyOwnedCollection = {},
}) { }) {
const dispatch = useDispatch();
const collectionsKeys = Object.keys(collections); const collectionsKeys = Object.keys(collections);
const collectiblesDropdownState = useSelector(getCollectiblesDropdownState);
const previousCollectionKeys = usePrevious(collectionsKeys);
// if there is only one collection present set it to open when component mounts useEffect(() => {
const [dropdownState, setDropdownState] = useState(() => { if (
return collectionsKeys.length === 1 !Object.keys(collectiblesDropdownState).length &&
? { previousCollectionKeys !== collectionsKeys
[PREVIOUSLY_OWNED_KEY]: false, ) {
[collectionsKeys[0]]: true, const initState = {};
} collectionsKeys.forEach((key) => {
: { [PREVIOUSLY_OWNED_KEY]: false }; initState[key] = true;
}); });
dispatch(updateCollectibleDropDownState(initState));
}
}, [
collectionsKeys,
previousCollectionKeys,
collectiblesDropdownState,
dispatch,
]);
const ipfsGateway = useSelector(getIpfsGateway); const ipfsGateway = useSelector(getIpfsGateway);
const history = useHistory(); const history = useHistory();
const renderCollectionImage = ( const renderCollectionImage = (
@ -81,46 +95,50 @@ export default function CollectiblesItems({
return null; return null;
} }
const isExpanded = dropdownState[key]; const isExpanded = collectiblesDropdownState[key];
return ( return (
<div <div className="collectibles-items__collection" key={`collection-${key}`}>
className="collectibles-items__collection" <button
key={`collection-${key}`} onClick={() => {
onClick={() => { dispatch(
setDropdownState((_dropdownState) => ({ updateCollectibleDropDownState({
..._dropdownState, ...collectiblesDropdownState,
[key]: !isExpanded, [key]: !isExpanded,
})); }),
}} );
> }}
<Box className="collectibles-items__collection-wrapper"
marginBottom={2}
display={DISPLAY.FLEX}
alignItems={ALIGN_ITEMS.CENTER}
justifyContent={JUSTIFY_CONTENT.SPACE_BETWEEN}
className="collectibles-items__collection-accordion-title"
> >
<Box <Box
marginBottom={2}
display={DISPLAY.FLEX}
alignItems={ALIGN_ITEMS.CENTER} alignItems={ALIGN_ITEMS.CENTER}
className="collectibles-items__collection-header" justifyContent={JUSTIFY_CONTENT.SPACE_BETWEEN}
className="collectibles-items__collection-accordion-title"
> >
{renderCollectionImage( <Box
isPreviouslyOwnedCollection, alignItems={ALIGN_ITEMS.CENTER}
collectionImage, className="collectibles-items__collection-header"
collectionName,
)}
<Typography
color={COLORS.BLACK}
variant={TYPOGRAPHY.H5}
margin={[0, 0, 0, 2]}
> >
{`${collectionName} (${collectibles.length})`} {renderCollectionImage(
</Typography> isPreviouslyOwnedCollection,
collectionImage,
collectionName,
)}
<Typography
color={COLORS.BLACK}
variant={TYPOGRAPHY.H5}
margin={[0, 0, 0, 2]}
>
{`${collectionName} (${collectibles.length})`}
</Typography>
</Box>
<Box alignItems={ALIGN_ITEMS.FLEX_END}>
<i className={`fa fa-chevron-${isExpanded ? 'down' : 'right'}`} />
</Box>
</Box> </Box>
<Box alignItems={ALIGN_ITEMS.FLEX_END}> </button>
<i className={`fa fa-chevron-${isExpanded ? 'down' : 'right'}`} />
</Box>
</Box>
{isExpanded ? ( {isExpanded ? (
<Box display={DISPLAY.FLEX} flexWrap={FLEX_WRAP.WRAP} gap={4}> <Box display={DISPLAY.FLEX} flexWrap={FLEX_WRAP.WRAP} gap={4}>
{collectibles.map((collectible, i) => { {collectibles.map((collectible, i) => {
@ -132,20 +150,22 @@ export default function CollectiblesItems({
key={`collectible-${i}`} key={`collectible-${i}`}
className="collectibles-items__collection-item-wrapper" className="collectibles-items__collection-item-wrapper"
> >
<div <Card padding={0} justifyContent={JUSTIFY_CONTENT.CENTER}>
className="collectibles-items__collection-item" <div
style={{ className="collectibles-items__collection-item"
backgroundColor, style={{
}} backgroundColor,
> }}
<img >
onClick={() => <img
history.push(`${ASSET_ROUTE}/${address}/${tokenId}`) onClick={() =>
} history.push(`${ASSET_ROUTE}/${address}/${tokenId}`)
className="collectibles-items__collection-item-image" }
src={collectibleImage} className="collectibles-items__collection-item-image"
/> src={collectibleImage}
</div> />
</div>
</Card>
</Box> </Box>
); );
})} })}

View File

@ -6,6 +6,12 @@
cursor: pointer; cursor: pointer;
} }
&-wrapper {
background-color: transparent;
border: 0;
width: 100%;
}
&-image { &-image {
width: 32px; width: 32px;
height: 32px; height: 32px;

View File

@ -136,6 +136,11 @@ const COLLECTIBLES_CONTRACTS = [
}, },
]; ];
const collectiblesDropdownState = {
0x495f947276749ce646f68ac8c248420045cb7b5e: true,
0xdc7382eb0bc9c352a4cba23c909bda01e0206414: true,
};
const ACCOUNT_1 = '0x123'; const ACCOUNT_1 = '0x123';
const ACCOUNT_2 = '0x456'; const ACCOUNT_2 = '0x456';
@ -164,6 +169,7 @@ const render = ({
selectedAddress, selectedAddress,
collectiblesDetectionNoticeDismissed, collectiblesDetectionNoticeDismissed,
useCollectibleDetection, useCollectibleDetection,
collectiblesDropdownState,
}, },
}); });
return renderWithProvider(<CollectiblesTab onAddNFT={onAddNFT} />, store); return renderWithProvider(<CollectiblesTab onAddNFT={onAddNFT} />, store);

View File

@ -707,6 +707,7 @@
&__label { &__label {
margin-right: 0.25rem; margin-right: 0.25rem;
min-width: 52px;
} }
} }

View File

@ -264,6 +264,10 @@ export function getCollectiblesDetectionNoticeDismissed(state) {
return state.metamask.collectiblesDetectionNoticeDismissed; return state.metamask.collectiblesDetectionNoticeDismissed;
} }
export function getCollectiblesDropdownState(state) {
return state.metamask.collectiblesDropdownState;
}
export function getEnableEIP1559V2NoticeDismissed(state) { export function getEnableEIP1559V2NoticeDismissed(state) {
return state.metamask.enableEIP1559V2NoticeDismissed; return state.metamask.enableEIP1559V2NoticeDismissed;
} }

View File

@ -1491,6 +1491,19 @@ export function updateSendAsset({ type, details }) {
dispatch(displayWarning(err.message)); dispatch(displayWarning(err.message));
} }
} }
if (details.standard === undefined) {
const { standard } = await getTokenStandardAndDetails(
details.address,
userAddress,
);
details.standard = standard;
}
if (details.standard === ERC1155) {
throw new Error('Sends of ERC1155 tokens are not currently supported');
}
if (isCurrentOwner) { if (isCurrentOwner) {
error = null; error = null;
balance = '0x1'; balance = '0x1';

View File

@ -4,7 +4,7 @@ import SendRowWrapper from '../send-row-wrapper';
import Identicon from '../../../../components/ui/identicon'; import Identicon from '../../../../components/ui/identicon';
import TokenBalance from '../../../../components/ui/token-balance'; import TokenBalance from '../../../../components/ui/token-balance';
import UserPreferencedCurrencyDisplay from '../../../../components/app/user-preferenced-currency-display'; import UserPreferencedCurrencyDisplay from '../../../../components/app/user-preferenced-currency-display';
import { ERC20, PRIMARY } from '../../../../helpers/constants/common'; import { ERC20, ERC721, PRIMARY } from '../../../../helpers/constants/common';
import { ASSET_TYPES } from '../../../../ducks/send'; import { ASSET_TYPES } from '../../../../ducks/send';
import { isEqualCaseInsensitive } from '../../../../helpers/utils/util'; import { isEqualCaseInsensitive } from '../../../../helpers/utils/util';
@ -57,7 +57,8 @@ export default class SendAssetRow extends Component {
async componentDidMount() { async componentDidMount() {
const sendableTokens = this.props.tokens.filter((token) => !token.isERC721); const sendableTokens = this.props.tokens.filter((token) => !token.isERC721);
const sendableCollectibles = this.props.collectibles.filter( const sendableCollectibles = this.props.collectibles.filter(
(collectible) => collectible.isCurrentlyOwned, (collectible) =>
collectible.isCurrentlyOwned && collectible.standard === ERC721,
); );
this.setState({ sendableTokens, sendableCollectibles }); this.setState({ sendableTokens, sendableCollectibles });
} }

View File

@ -64,10 +64,11 @@ export default class ExperimentalTab extends PureComponent {
useCollectibleDetection, useCollectibleDetection,
setUseCollectibleDetection, setUseCollectibleDetection,
openSeaEnabled, openSeaEnabled,
setOpenSeaEnabled,
} = this.props; } = this.props;
return ( return (
<div className="settings-page__content-row"> <div className="settings-page__content-row--dependent">
<div className="settings-page__content-item"> <div className="settings-page__content-item">
<span>{t('useCollectibleDetection')}</span> <span>{t('useCollectibleDetection')}</span>
<div className="settings-page__content-description"> <div className="settings-page__content-description">
@ -77,7 +78,6 @@ export default class ExperimentalTab extends PureComponent {
<div className="settings-page__content-item"> <div className="settings-page__content-item">
<div className="settings-page__content-item-col"> <div className="settings-page__content-item-col">
<ToggleButton <ToggleButton
disabled={!openSeaEnabled}
value={useCollectibleDetection} value={useCollectibleDetection}
onToggle={(value) => { onToggle={(value) => {
this.context.metricsEvent({ this.context.metricsEvent({
@ -87,6 +87,9 @@ export default class ExperimentalTab extends PureComponent {
name: 'Collectible Detection', name: 'Collectible Detection',
}, },
}); });
if (!value && !openSeaEnabled) {
setOpenSeaEnabled(!value);
}
setUseCollectibleDetection(!value); setUseCollectibleDetection(!value);
}} }}
offLabel={t('off')} offLabel={t('off')}
@ -103,10 +106,15 @@ export default class ExperimentalTab extends PureComponent {
return null; return null;
} }
const { t } = this.context; const { t } = this.context;
const { openSeaEnabled, setOpenSeaEnabled } = this.props; const {
openSeaEnabled,
setOpenSeaEnabled,
useCollectibleDetection,
setUseCollectibleDetection,
} = this.props;
return ( return (
<div className="settings-page__content-row"> <div className="settings-page__content-row--parent">
<div className="settings-page__content-item"> <div className="settings-page__content-item">
<span>{t('enableOpenSeaAPI')}</span> <span>{t('enableOpenSeaAPI')}</span>
<div className="settings-page__content-description"> <div className="settings-page__content-description">
@ -126,6 +134,9 @@ export default class ExperimentalTab extends PureComponent {
}, },
}); });
setOpenSeaEnabled(!value); setOpenSeaEnabled(!value);
if (value && !useCollectibleDetection) {
setUseCollectibleDetection(true);
}
}} }}
offLabel={t('off')} offLabel={t('off')}
onLabel={t('on')} onLabel={t('on')}

View File

@ -168,6 +168,15 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 10px 0 20px; padding: 10px 0 20px;
&--parent {
padding: 10px 0 10px;
}
&--dependent {
margin-left: 48px;
padding: 0 0 20px;
}
} }
&__content-item { &__content-item {

View File

@ -1829,6 +1829,13 @@ export function hideAlert() {
}; };
} }
export function updateCollectibleDropDownState(value) {
return async (dispatch) => {
await promisifiedBackground.updateCollectibleDropDownState(value);
await forceUpdateMetamaskState(dispatch);
};
}
/** /**
* This action will receive two types of values via qrCodeData * This action will receive two types of values via qrCodeData
* an object with the following structure {type, values} * an object with the following structure {type, values}