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

Yet more NFT UX cleanups (#13435)

* yet more nft ux cleanups
This commit is contained in:
Alex Donesky 2022-01-31 12:56:49 -06:00 committed by GitHub
parent 09ac38977e
commit dc217dd536
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 152 additions and 70 deletions

View File

@ -1640,6 +1640,9 @@
"loading": { "loading": {
"message": "Loading..." "message": "Loading..."
}, },
"loadingNFTs": {
"message": "Loading NFTs..."
},
"loadingTokens": { "loadingTokens": {
"message": "Loading Tokens..." "message": "Loading Tokens..."
}, },

View File

@ -134,16 +134,10 @@ export default class PreferencesController {
/** /**
* Setter for the `useCollectibleDetection` property * Setter for the `useCollectibleDetection` property
* *
* @param {boolean} val - Whether or not the user prefers to autodetect collectibles. * @param {boolean} useCollectibleDetection - Whether or not the user prefers to autodetect collectibles.
*/ */
setUseCollectibleDetection(val) { setUseCollectibleDetection(useCollectibleDetection) {
const { openSeaEnabled } = this.store.getState(); this.store.updateState({ useCollectibleDetection });
if (val && !openSeaEnabled) {
throw new Error(
'useCollectibleDetection cannot be enabled if openSeaEnabled is false',
);
}
this.store.updateState({ useCollectibleDetection: val });
} }
/** /**

View File

@ -17,7 +17,11 @@ import {
} from '../../../helpers/constants/design-system'; } from '../../../helpers/constants/design-system';
import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app'; import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app';
import { getEnvironmentType } from '../../../../app/scripts/lib/util'; import { getEnvironmentType } from '../../../../app/scripts/lib/util';
import { getIpfsGateway } from '../../../selectors'; import {
getCurrentChainId,
getIpfsGateway,
getSelectedAddress,
} 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 { updateCollectibleDropDownState } from '../../../store/actions';
@ -39,22 +43,39 @@ export default function CollectiblesItems({
const collectionsKeys = Object.keys(collections); const collectionsKeys = Object.keys(collections);
const collectiblesDropdownState = useSelector(getCollectiblesDropdownState); const collectiblesDropdownState = useSelector(getCollectiblesDropdownState);
const previousCollectionKeys = usePrevious(collectionsKeys); const previousCollectionKeys = usePrevious(collectionsKeys);
const selectedAddress = useSelector(getSelectedAddress);
const chainId = useSelector(getCurrentChainId);
useEffect(() => { useEffect(() => {
if ( if (
!Object.keys(collectiblesDropdownState).length && chainId !== undefined &&
previousCollectionKeys !== collectionsKeys selectedAddress !== undefined &&
previousCollectionKeys !== collectionsKeys &&
(collectiblesDropdownState?.[selectedAddress]?.[chainId] === undefined ||
Object.keys(collectiblesDropdownState?.[selectedAddress]?.[chainId])
.length === 0)
) { ) {
const initState = {}; const initState = {};
collectionsKeys.forEach((key) => { collectionsKeys.forEach((key) => {
initState[key] = true; initState[key] = true;
}); });
dispatch(updateCollectibleDropDownState(initState));
const newCollectibleDropdownState = {
...collectiblesDropdownState,
[selectedAddress]: {
...collectiblesDropdownState?.[selectedAddress],
[chainId]: initState,
},
};
dispatch(updateCollectibleDropDownState(newCollectibleDropdownState));
} }
}, [ }, [
collectionsKeys, collectionsKeys,
previousCollectionKeys, previousCollectionKeys,
collectiblesDropdownState, collectiblesDropdownState,
selectedAddress,
chainId,
dispatch, dispatch,
]); ]);
@ -84,6 +105,22 @@ export default function CollectiblesItems({
); );
}; };
const updateCollectibleDropDownStateKey = (key, isExpanded) => {
const currentAccountCollectibleDropdownState =
collectiblesDropdownState[selectedAddress][chainId];
const newCurrentAccountState = {
...currentAccountCollectibleDropdownState,
[key]: !isExpanded,
};
collectiblesDropdownState[selectedAddress][
chainId
] = newCurrentAccountState;
dispatch(updateCollectibleDropDownState(collectiblesDropdownState));
};
const renderCollection = ({ const renderCollection = ({
collectibles, collectibles,
collectionName, collectionName,
@ -95,17 +132,13 @@ export default function CollectiblesItems({
return null; return null;
} }
const isExpanded = collectiblesDropdownState[key]; const isExpanded =
collectiblesDropdownState[selectedAddress]?.[chainId]?.[key];
return ( return (
<div className="collectibles-items__collection" key={`collection-${key}`}> <div className="collectibles-items__collection" key={`collection-${key}`}>
<button <button
onClick={() => { onClick={() => {
dispatch( updateCollectibleDropDownStateKey(key, isExpanded);
updateCollectibleDropDownState({
...collectiblesDropdownState,
[key]: !isExpanded,
}),
);
}} }}
className="collectibles-items__collection-wrapper" className="collectibles-items__collection-wrapper"
> >

View File

@ -26,7 +26,6 @@
background: var(--ui-4); background: var(--ui-4);
color: var(--ui-white); color: var(--ui-white);
text-align: center; text-align: center;
line-height: 1;
} }
&-item-wrapper { &-item-wrapper {

View File

@ -1,8 +1,7 @@
import React, { useEffect, useState } from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { isEqual } from 'lodash';
import Box from '../../ui/box'; import Box from '../../ui/box';
import Button from '../../ui/button'; import Button from '../../ui/button';
import Typography from '../../ui/typography/typography'; import Typography from '../../ui/typography/typography';
@ -18,22 +17,16 @@ import {
ALIGN_ITEMS, ALIGN_ITEMS,
} from '../../../helpers/constants/design-system'; } from '../../../helpers/constants/design-system';
import { useI18nContext } from '../../../hooks/useI18nContext'; import { useI18nContext } from '../../../hooks/useI18nContext';
import { import { getCollectiblesDetectionNoticeDismissed } from '../../../ducks/metamask/metamask';
getCollectibles,
getCollectibleContracts,
getCollectiblesDetectionNoticeDismissed,
} from '../../../ducks/metamask/metamask';
import { getIsMainnet, getUseCollectibleDetection } from '../../../selectors'; import { getIsMainnet, getUseCollectibleDetection } from '../../../selectors';
import { EXPERIMENTAL_ROUTE } from '../../../helpers/constants/routes'; import { EXPERIMENTAL_ROUTE } from '../../../helpers/constants/routes';
import { import {
checkAndUpdateAllCollectiblesOwnershipStatus, checkAndUpdateAllCollectiblesOwnershipStatus,
detectCollectibles, detectCollectibles,
} from '../../../store/actions'; } from '../../../store/actions';
import { usePrevious } from '../../../hooks/usePrevious'; import { useCollectiblesCollections } from '../../../hooks/useCollectiblesCollections';
export default function CollectiblesTab({ onAddNFT }) { export default function CollectiblesTab({ onAddNFT }) {
const collectibles = useSelector(getCollectibles);
const collectibleContracts = useSelector(getCollectibleContracts);
const useCollectibleDetection = useSelector(getUseCollectibleDetection); const useCollectibleDetection = useSelector(getUseCollectibleDetection);
const isMainnet = useSelector(getIsMainnet); const isMainnet = useSelector(getIsMainnet);
const collectibleDetectionNoticeDismissed = useSelector( const collectibleDetectionNoticeDismissed = useSelector(
@ -42,46 +35,12 @@ export default function CollectiblesTab({ onAddNFT }) {
const history = useHistory(); const history = useHistory();
const t = useI18nContext(); const t = useI18nContext();
const dispatch = useDispatch(); const dispatch = useDispatch();
const [collections, setCollections] = useState({});
const [previouslyOwnedCollection, setPreviouslyOwnedCollection] = useState({
collectionName: 'Previously Owned',
collectibles: [],
});
const prevCollectibles = usePrevious(collectibles); const {
useEffect(() => { collectiblesLoading,
const getCollections = () => { collections,
const newCollections = {}; previouslyOwnedCollection,
const newPreviouslyOwnedCollections = { } = useCollectiblesCollections();
collectionName: 'Previously Owned',
collectibles: [],
};
collectibles.forEach((collectible) => {
if (collectible?.isCurrentlyOwned === false) {
newPreviouslyOwnedCollections.collectibles.push(collectible);
} else if (newCollections[collectible.address]) {
newCollections[collectible.address].collectibles.push(collectible);
} else {
const collectionContract = collectibleContracts.find(
({ address }) => address === collectible.address,
);
newCollections[collectible.address] = {
collectionName: collectionContract?.name || collectible.name,
collectionImage:
collectionContract?.logo || collectible.collectionImage,
collectibles: [collectible],
};
}
});
setCollections(newCollections);
setPreviouslyOwnedCollection(newPreviouslyOwnedCollections);
};
if (!isEqual(prevCollectibles, collectibles)) {
getCollections();
}
}, [collectibles, prevCollectibles, collectibleContracts]);
const onEnableAutoDetect = () => { const onEnableAutoDetect = () => {
history.push(EXPERIMENTAL_ROUTE); history.push(EXPERIMENTAL_ROUTE);
@ -94,6 +53,10 @@ export default function CollectiblesTab({ onAddNFT }) {
checkAndUpdateAllCollectiblesOwnershipStatus(); checkAndUpdateAllCollectiblesOwnershipStatus();
}; };
if (collectiblesLoading) {
return <div className="collectibles-tab__loading">{t('loadingNFTs')}</div>;
}
return ( return (
<div className="collectibles-tab"> <div className="collectibles-tab">
{Object.keys(collections).length > 0 || {Object.keys(collections).length > 0 ||

View File

@ -180,11 +180,13 @@ describe('Collectible Items', () => {
const setCollectiblesDetectionNoticeDismissedStub = jest.fn(); const setCollectiblesDetectionNoticeDismissedStub = jest.fn();
const getStateStub = jest.fn(); const getStateStub = jest.fn();
const checkAndUpdateAllCollectiblesOwnershipStatusStub = jest.fn(); const checkAndUpdateAllCollectiblesOwnershipStatusStub = jest.fn();
const updateCollectibleDropDownStateStub = jest.fn();
setBackgroundConnection({ setBackgroundConnection({
setCollectiblesDetectionNoticeDismissed: setCollectiblesDetectionNoticeDismissedStub, setCollectiblesDetectionNoticeDismissed: setCollectiblesDetectionNoticeDismissedStub,
detectCollectibles: detectCollectiblesStub, detectCollectibles: detectCollectiblesStub,
getState: getStateStub, getState: getStateStub,
checkAndUpdateAllCollectiblesOwnershipStatus: checkAndUpdateAllCollectiblesOwnershipStatusStub, checkAndUpdateAllCollectiblesOwnershipStatus: checkAndUpdateAllCollectiblesOwnershipStatusStub,
updateCollectibleDropDownState: updateCollectibleDropDownStateStub,
}); });
const historyPushMock = jest.fn(); const historyPushMock = jest.fn();

View File

@ -5,4 +5,12 @@
font-size: 0.875rem; font-size: 0.875rem;
} }
} }
&__loading {
display: flex;
height: 250px;
align-items: center;
justify-content: center;
padding: 30px;
}
} }

View File

@ -0,0 +1,80 @@
import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { isEqual } from 'lodash';
import {
getCollectibles,
getCollectibleContracts,
} from '../ducks/metamask/metamask';
import { getCurrentChainId, getSelectedAddress } from '../selectors';
import { usePrevious } from './usePrevious';
export function useCollectiblesCollections() {
const [collections, setCollections] = useState({});
const [previouslyOwnedCollection, setPreviouslyOwnedCollection] = useState({
collectionName: 'Previously Owned',
collectibles: [],
});
const collectibles = useSelector(getCollectibles);
const [collectiblesLoading, setCollectiblesLoading] = useState(
() => collectibles?.length >= 0,
);
const selectedAddress = useSelector(getSelectedAddress);
const chainId = useSelector(getCurrentChainId);
const collectibleContracts = useSelector(getCollectibleContracts);
const prevCollectibles = usePrevious(collectibles);
const prevChainId = usePrevious(chainId);
const prevSelectedAddress = usePrevious(selectedAddress);
useEffect(() => {
const getCollections = () => {
setCollectiblesLoading(true);
if (selectedAddress === undefined || chainId === undefined) {
return;
}
const newCollections = {};
const newPreviouslyOwnedCollections = {
collectionName: 'Previously Owned',
collectibles: [],
};
collectibles.forEach((collectible) => {
if (collectible?.isCurrentlyOwned === false) {
newPreviouslyOwnedCollections.collectibles.push(collectible);
} else if (newCollections[collectible.address]) {
newCollections[collectible.address].collectibles.push(collectible);
} else {
const collectionContract = collectibleContracts.find(
({ address }) => address === collectible.address,
);
newCollections[collectible.address] = {
collectionName: collectionContract?.name || collectible.name,
collectionImage:
collectionContract?.logo || collectible.collectionImage,
collectibles: [collectible],
};
}
});
setCollections(newCollections);
setPreviouslyOwnedCollection(newPreviouslyOwnedCollections);
setCollectiblesLoading(false);
};
if (
!isEqual(prevCollectibles, collectibles) ||
!isEqual(prevSelectedAddress, selectedAddress) ||
!isEqual(prevChainId, chainId)
) {
getCollections();
}
}, [
collectibles,
prevCollectibles,
collectibleContracts,
setCollectiblesLoading,
chainId,
prevChainId,
selectedAddress,
prevSelectedAddress,
]);
return { collectiblesLoading, collections, previouslyOwnedCollection };
}