1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 01:39:44 +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,
...initState,
qrHardware: {},
collectiblesDropdownState: {},
});
this.timer = null;
@ -282,4 +283,15 @@ export default class AppStateController extends EventEmitter {
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(
appStateController,
),
updateCollectibleDropDownState: appStateController.updateCollectibleDropDownState.bind(
appStateController,
),
// EnsController
tryReverseResolveAddress: ensController.reverseResolveAddress.bind(
ensController,

View File

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

View File

@ -53,18 +53,40 @@ $spacer-break-small: 16px;
overflow-wrap: break-word;
}
&__contract-link,
&__image-link {
word-break: break-all;
&__contract-wrapper {
max-width: calc(100% - #{$link-title-width});
}
@media screen and (max-width: $break-small) {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 90%;
&__contract-copy-button {
@include H6;
width: 80px;
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 {
flex: 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 { useSelector } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import Box from '../../ui/box';
import Typography from '../../ui/typography/typography';
import Card from '../../ui/card';
import {
COLORS,
TYPOGRAPHY,
@ -19,6 +20,9 @@ import { getEnvironmentType } from '../../../../app/scripts/lib/util';
import { getIpfsGateway } from '../../../selectors';
import { ASSET_ROUTE } from '../../../helpers/constants/routes';
import { getAssetImageURL } from '../../../helpers/utils/util';
import { updateCollectibleDropDownState } from '../../../store/actions';
import { usePrevious } from '../../../hooks/usePrevious';
import { getCollectiblesDropdownState } from '../../../ducks/metamask/metamask';
const width =
getEnvironmentType() === ENVIRONMENT_TYPE_POPUP
@ -31,20 +35,30 @@ export default function CollectiblesItems({
collections = {},
previouslyOwnedCollection = {},
}) {
const dispatch = useDispatch();
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
const [dropdownState, setDropdownState] = useState(() => {
return collectionsKeys.length === 1
? {
[PREVIOUSLY_OWNED_KEY]: false,
[collectionsKeys[0]]: true,
}
: { [PREVIOUSLY_OWNED_KEY]: false };
});
useEffect(() => {
if (
!Object.keys(collectiblesDropdownState).length &&
previousCollectionKeys !== collectionsKeys
) {
const initState = {};
collectionsKeys.forEach((key) => {
initState[key] = true;
});
dispatch(updateCollectibleDropDownState(initState));
}
}, [
collectionsKeys,
previousCollectionKeys,
collectiblesDropdownState,
dispatch,
]);
const ipfsGateway = useSelector(getIpfsGateway);
const history = useHistory();
const renderCollectionImage = (
@ -81,46 +95,50 @@ export default function CollectiblesItems({
return null;
}
const isExpanded = dropdownState[key];
const isExpanded = collectiblesDropdownState[key];
return (
<div
className="collectibles-items__collection"
key={`collection-${key}`}
onClick={() => {
setDropdownState((_dropdownState) => ({
..._dropdownState,
[key]: !isExpanded,
}));
}}
>
<Box
marginBottom={2}
display={DISPLAY.FLEX}
alignItems={ALIGN_ITEMS.CENTER}
justifyContent={JUSTIFY_CONTENT.SPACE_BETWEEN}
className="collectibles-items__collection-accordion-title"
<div className="collectibles-items__collection" key={`collection-${key}`}>
<button
onClick={() => {
dispatch(
updateCollectibleDropDownState({
...collectiblesDropdownState,
[key]: !isExpanded,
}),
);
}}
className="collectibles-items__collection-wrapper"
>
<Box
marginBottom={2}
display={DISPLAY.FLEX}
alignItems={ALIGN_ITEMS.CENTER}
className="collectibles-items__collection-header"
justifyContent={JUSTIFY_CONTENT.SPACE_BETWEEN}
className="collectibles-items__collection-accordion-title"
>
{renderCollectionImage(
isPreviouslyOwnedCollection,
collectionImage,
collectionName,
)}
<Typography
color={COLORS.BLACK}
variant={TYPOGRAPHY.H5}
margin={[0, 0, 0, 2]}
<Box
alignItems={ALIGN_ITEMS.CENTER}
className="collectibles-items__collection-header"
>
{`${collectionName} (${collectibles.length})`}
</Typography>
{renderCollectionImage(
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 alignItems={ALIGN_ITEMS.FLEX_END}>
<i className={`fa fa-chevron-${isExpanded ? 'down' : 'right'}`} />
</Box>
</Box>
</button>
{isExpanded ? (
<Box display={DISPLAY.FLEX} flexWrap={FLEX_WRAP.WRAP} gap={4}>
{collectibles.map((collectible, i) => {
@ -132,20 +150,22 @@ export default function CollectiblesItems({
key={`collectible-${i}`}
className="collectibles-items__collection-item-wrapper"
>
<div
className="collectibles-items__collection-item"
style={{
backgroundColor,
}}
>
<img
onClick={() =>
history.push(`${ASSET_ROUTE}/${address}/${tokenId}`)
}
className="collectibles-items__collection-item-image"
src={collectibleImage}
/>
</div>
<Card padding={0} justifyContent={JUSTIFY_CONTENT.CENTER}>
<div
className="collectibles-items__collection-item"
style={{
backgroundColor,
}}
>
<img
onClick={() =>
history.push(`${ASSET_ROUTE}/${address}/${tokenId}`)
}
className="collectibles-items__collection-item-image"
src={collectibleImage}
/>
</div>
</Card>
</Box>
);
})}

View File

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

View File

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

View File

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

View File

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

View File

@ -1491,6 +1491,19 @@ export function updateSendAsset({ type, details }) {
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) {
error = null;
balance = '0x1';

View File

@ -4,7 +4,7 @@ import SendRowWrapper from '../send-row-wrapper';
import Identicon from '../../../../components/ui/identicon';
import TokenBalance from '../../../../components/ui/token-balance';
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 { isEqualCaseInsensitive } from '../../../../helpers/utils/util';
@ -57,7 +57,8 @@ export default class SendAssetRow extends Component {
async componentDidMount() {
const sendableTokens = this.props.tokens.filter((token) => !token.isERC721);
const sendableCollectibles = this.props.collectibles.filter(
(collectible) => collectible.isCurrentlyOwned,
(collectible) =>
collectible.isCurrentlyOwned && collectible.standard === ERC721,
);
this.setState({ sendableTokens, sendableCollectibles });
}

View File

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

View File

@ -168,6 +168,15 @@
display: flex;
flex-direction: column;
padding: 10px 0 20px;
&--parent {
padding: 10px 0 10px;
}
&--dependent {
margin-left: 48px;
padding: 0 0 20px;
}
}
&__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
* an object with the following structure {type, values}