Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
przemyslaw-wlodek committed Jan 9, 2025
1 parent a811048 commit 46f3d2c
Show file tree
Hide file tree
Showing 11 changed files with 109 additions and 156 deletions.
1 change: 1 addition & 0 deletions apps/browser-extension-wallet/.env.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ CARDANO_WS_SERVER_URL_PREVIEW=wss://dev-preview.lw.iog.io
CARDANO_WS_SERVER_URL_SANCHONET=wss://dev-sanchonet.lw.iog.io

# Blockfrost
BLOCKFROST_IPFS_URL=https://ipfs.blockfrost.dev
BLOCKFROST_URL_MAINNET=https://cardano-mainnet.blockfrost.io
BLOCKFROST_URL_PREPROD=https://cardano-preprod.blockfrost.io
BLOCKFROST_URL_PREVIEW=https://cardano-preview.blockfrost.io
Expand Down
1 change: 1 addition & 0 deletions apps/browser-extension-wallet/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ CARDANO_WS_SERVER_URL_PREVIEW=wss://dev-preview.lw.iog.io
CARDANO_WS_SERVER_URL_SANCHONET=wss://dev-sanchonet.lw.iog.io

# Blockfrost
BLOCKFROST_IPFS_URL=https://ipfs.blockfrost.dev
BLOCKFROST_URL_MAINNET=https://cardano-mainnet.blockfrost.io
BLOCKFROST_URL_PREPROD=https://cardano-preprod.blockfrost.io
BLOCKFROST_URL_PREVIEW=https://cardano-preview.blockfrost.io
Expand Down
1 change: 1 addition & 0 deletions apps/browser-extension-wallet/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export type Config = {
// eslint-disable-next-line complexity
const envChecks = (chosenChain: Wallet.ChainName): void => {
if (
!process.env.BLOCKFROST_IPFS_URL ||
!process.env.CARDANO_SERVICES_URL_MAINNET ||
!process.env.CARDANO_SERVICES_URL_PREPROD ||
!process.env.CARDANO_SERVICES_URL_PREVIEW ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export const fortmatImgSrc = (params: { img: string; type: string }): string =>

export const getAssetImageUrl = (image: string): string => {
if (image.startsWith('ipfs')) {
return image.replace('ipfs://', 'https://ipfs.blockfrost.dev/ipfs/');
return `https://ipfs.blockfrost.dev/ipfs/${image.replace('ipfs://', '').replace('ipfs/', '')}`;
}

if (image.startsWith('data:image/')) return image;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
/* eslint-disable sonarjs/cognitive-complexity */
/* eslint-disable unicorn/no-useless-undefined */
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import debounce from 'lodash/debounce';
import useResizeObserver, { ObservedSize } from 'use-resize-observer';
import { useMediaQuery } from 'react-responsive';
import cn from 'classnames';
import styles from './NftsLayout.module.scss';
import { useWalletStore } from '@stores';
Expand All @@ -29,7 +26,7 @@ import {
useOutputInitialState
} from '../../send-transaction';
import { Button, useObservable } from '@lace/common';
import { DEFAULT_WALLET_BALANCE } from '@src/utils/constants';
import { APP_MODE_POPUP, DEFAULT_WALLET_BALANCE } from '@src/utils/constants';
import { Skeleton } from 'antd';
import { EducationalList, FundWalletBanner, Layout, SectionLayout } from '@src/views/browser-view/components';
import { DrawerContent } from '@src/views/browser-view/components/Drawer';
Expand Down Expand Up @@ -60,14 +57,19 @@ const LACE_APP_ID = 'lace-app';
export const STAKE_POOL_CARD_HEIGHT = 84;
export const STAKE_POOL_GRID_ROW_GAP = 12;

export type StakePoolsGridColumnCount = 2 | 3 | 4;
export type NftGridColumnCount = 2 | 4;

const DEFAULT_DEBOUNCE = 200;
const increaseViewportBy = { bottom: 100, top: 0 };

// eslint-disable-next-line max-statements, complexity
export const NftsLayout = withNftsFoldersContext((): React.ReactElement => {
const { walletInfo, inMemoryWallet, blockchainProvider, environmentName } = useWalletStore();
const {
walletInfo,
inMemoryWallet,
blockchainProvider,
environmentName,
walletUI: { appMode }
} = useWalletStore();
const [selectedFolderId, setSelectedFolderId] = useState<number | undefined>();
const { t } = useTranslation();
const assetsInfo = useAssetInfo();
Expand Down Expand Up @@ -235,51 +237,11 @@ export const NftsLayout = withNftsFoldersContext((): React.ReactElement => {
}, []);

const showCreateFolder = nfts.length > 0 && nftsNotInFolders.length > 0 && process.env.USE_NFT_FOLDERS === 'true';
const [numberOfItemsPerRow, setNumberOfItemsPerRow] = useState<StakePoolsGridColumnCount>();
const [containerWidth, setContainerWidth] = useState<number>();
const numberOfItemsPerRow = appMode === APP_MODE_POPUP ? 2 : 4;
const [initialItemsCount, setInitialItemsCount] = useState(0);

const ref = useRef<HTMLDivElement>(null);

const matchTwoColumnsLayout = useMediaQuery({ maxWidth: 668 });
const matchThreeColumnsLayout = useMediaQuery({ maxWidth: 1660, minWidth: 668 });
const matchFourColumnsLayout = useMediaQuery({ minWidth: 1660 });
const numberOfItemsPerMediaQueryMap: Partial<Record<StakePoolsGridColumnCount, boolean>> = useMemo(
() => ({
2: matchTwoColumnsLayout,
3: matchThreeColumnsLayout,
4: matchFourColumnsLayout
}),
[matchFourColumnsLayout, matchThreeColumnsLayout, matchTwoColumnsLayout]
);

const updateNumberOfItemsInRow = useCallback(() => {
if (!ref?.current) return;

const result = Number(
Object.entries(numberOfItemsPerMediaQueryMap).find(([, matches]) => matches)?.[0]
) as StakePoolsGridColumnCount;

setNumberOfItemsPerRow(result);
}, [numberOfItemsPerMediaQueryMap]);

const setContainerWidthCb = useCallback(
(size: ObservedSize) => {
if (size.width !== containerWidth) {
updateNumberOfItemsInRow();
setContainerWidth(size.width);
}
},
[containerWidth, updateNumberOfItemsInRow]
);

const onResize = useMemo(
() => debounce(setContainerWidthCb, DEFAULT_DEBOUNCE, { leading: true }),
[setContainerWidthCb]
);

useResizeObserver<HTMLDivElement>({ onResize, ref });

const tableReference = useRef<HTMLDivElement | null>(null);
const initialRowsCount = useVisibleItemsCount({
containerRef: tableReference,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ process.env.CARDANO_SERVICES_URL_PREVIEW = 'https://preview-prod.com';
process.env.CARDANO_SERVICES_URL_PREPROD = 'https://preprod-prod.com';
process.env.CARDANO_SERVICES_URL_MAINNET = 'https://mainnet-url.com';
process.env.CARDANO_SERVICES_URL_SANCHONET = 'https://sanchonet-url.com';
process.env.BLOCKFROST_IPFS_URL = 'https://ipfs.blockfrost.dev';
process.env.BLOCKFROST_URL_MAINNET = 'https://cardano-mainnet.blockfrost.io';
process.env.BLOCKFROST_URL_PREPROD = 'https://cardano-preprod.blockfrost.io';
process.env.BLOCKFROST_URL_PREVIEW = 'https://cardano-preview.blockfrost.io';
Expand Down
2 changes: 1 addition & 1 deletion apps/browser-extension-wallet/webpack-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const transformManifest = (content, mode) => {
)
.replace(
'$BLOCKFROST_URLS',
`${process.env.BLOCKFROST_URL_MAINNET} ${process.env.BLOCKFROST_URL_PREPROD} ${process.env.BLOCKFROST_URL_PREVIEW} ${process.env.BLOCKFROST_URL_SANCHONET}`
`${process.env.BLOCKFROST_IPFS_URL} ${process.env.BLOCKFROST_URL_MAINNET} ${process.env.BLOCKFROST_URL_PREPROD} ${process.env.BLOCKFROST_URL_PREVIEW} ${process.env.BLOCKFROST_URL_SANCHONET}`
)
.replace('$LOCALHOST_DEFAULT_SRC', mode === 'development' ? 'http://localhost:3000' : '')
.replace('$LOCALHOST_SCRIPT_SRC', mode === 'development' ? 'http://localhost:3000' : '')
Expand Down
171 changes: 82 additions & 89 deletions packages/common/src/ui/hooks/useFetchImage.ts
Original file line number Diff line number Diff line change
@@ -1,101 +1,94 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useCallback, useMemo, useReducer } from 'react';

enum IMAGE_FETCH_ACTION_TYPES {
FETCHING = 'FETCHING',
FETCHED = 'FETCHED',
FETCH_ERROR = 'FETCH_ERROR'
}

export enum IMAGE_FETCH_STATUS {
LOADING,
LOADED,
ERROR
}

interface ImageResponse {
status?: IMAGE_FETCH_STATUS;
src?: string;
}

interface FetchImageArgs {
url: string;
fallback: string;
}

type FetchAction = {
type: IMAGE_FETCH_ACTION_TYPES;
payload?: string;
};

const handleImageFetch = (image: string) => {
const downloadingImage = new Image();

const imageResponse: Promise<ImageResponse> = new Promise<ImageResponse>((resolve) => {
const onLoadEvent = (event: any) => {
resolve({
status: IMAGE_FETCH_STATUS.LOADED,
src: event.path?.[0].currentSrc || image
});
};
const onErrorEvent = () => {
resolve({
status: IMAGE_FETCH_STATUS.ERROR
});
};

downloadingImage.addEventListener('load', onLoadEvent);
downloadingImage.addEventListener('error', onErrorEvent);
});
import { useState, useEffect } from 'react';

downloadingImage.src = image;
export type ImageFetchStatus = 'loading' | 'loaded' | 'error';

return imageResponse;
type ImageFetchStateMap = {
loading: { status: 'loading' };
loaded: { status: 'loaded'; imageSrc: string };
error: { status: 'error'; imageSrc: string };
};

const initialState = { status: IMAGE_FETCH_STATUS.LOADING };

const fetchImageReducer = (state: ImageResponse, action: FetchAction): ImageResponse => {
switch (action.type) {
case IMAGE_FETCH_ACTION_TYPES.FETCHING:
return { ...initialState, status: IMAGE_FETCH_STATUS.LOADING };
case IMAGE_FETCH_ACTION_TYPES.FETCHED:
return { ...initialState, status: IMAGE_FETCH_STATUS.LOADED, src: action.payload };
case IMAGE_FETCH_ACTION_TYPES.FETCH_ERROR:
return { ...initialState, status: IMAGE_FETCH_STATUS.ERROR };
default:
return state;
export type UseFetchImageState = ImageFetchStateMap[ImageFetchStatus];

const maxConcurrentRequests = 5;
const requestQueue: Set<() => Promise<void>> = new Set();
let concurrentRequestsCount = 0;

const processNextRequest = async () => {
if (requestQueue.size > 0 && concurrentRequestsCount < maxConcurrentRequests) {
const [nextRequest] = requestQueue;
requestQueue.delete(nextRequest);
concurrentRequestsCount++;
try {
await nextRequest();
} finally {
processNextRequest();
}
}
};

const fetchImageDispatcher = async (url: string, dispatcher: React.Dispatch<FetchAction>) => {
const response = await handleImageFetch(url);
const fetchImage = async (url: string, controller: AbortController) => {
const response = await fetch(url, {
signal: controller.signal,
headers: { 'Cache-Control': 'public, max-age=86400' }
});

const type =
response.status === IMAGE_FETCH_STATUS.LOADED
? IMAGE_FETCH_ACTION_TYPES.FETCHED
: // eslint-disable-next-line unicorn/no-nested-ternary
response.status === IMAGE_FETCH_STATUS.ERROR
? IMAGE_FETCH_ACTION_TYPES.FETCH_ERROR
: IMAGE_FETCH_ACTION_TYPES.FETCHING;
const dispatchValue = { payload: response.src, type };
if (!response.ok) throw new Error('Image fetch failed');

dispatcher(dispatchValue);
const blob = await response.blob();
return URL.createObjectURL(blob);
};

export const useFetchImage = ({ url, fallback }: FetchImageArgs): [ImageResponse, () => Promise<void>] => {
const [result, dispatch] = useReducer(fetchImageReducer, initialState);
const handleLoad = useCallback(async () => {
await fetchImageDispatcher(url, dispatch);
}, [url]);

const resultWithFallback = useMemo(
() => ({
...result,
src: result?.src || fallback
}),
[result, fallback]
);

return [resultWithFallback, handleLoad];
export const useFetchImage = ({ url, fallbackImage }: { url: string; fallbackImage: string }) => {

Check warning on line 42 in packages/common/src/ui/hooks/useFetchImage.ts

View workflow job for this annotation

GitHub Actions / Prepare

Missing return type on function
const [state, setState] = useState<UseFetchImageState>({ status: 'loading' });

useEffect(() => {
let realUrl = url;

if (url.startsWith('data:')) {
// TODO investigate the below problem + use getAssetImageUrl
// Solve broken links e.g. data:image/png;base64,https://storage.something.com/assets/1235
const httpPosition = url.indexOf('http');
if (httpPosition === -1) {
setState({ status: 'loaded', imageSrc: url });
// eslint-disable-next-line @typescript-eslint/no-empty-function
return () => {};
}

realUrl = url.slice(httpPosition);
}

const controller = new AbortController();

const loadImage = async () => {
try {
setState({ status: 'loading' });

const imageSrc = await fetchImage(realUrl, controller);

setState({ status: 'loaded', imageSrc });
} catch {
if (!controller.signal.aborted) {
setState({ status: 'error', imageSrc: fallbackImage });
}
} finally {
concurrentRequestsCount--;
processNextRequest();
}
};

if (concurrentRequestsCount < maxConcurrentRequests) {
concurrentRequestsCount++;
loadImage();
} else {
requestQueue.add(loadImage);
}

return () => {
controller.abort();
requestQueue.delete(loadImage);
};
}, [url, fallbackImage]);

return state;
};
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, { useEffect } from 'react';
import React from 'react';
import styles from './HandleAddressCard.module.scss';
import { Image, Spin } from 'antd';
import { Base } from './Base';
import symbol from '../../assets/images/handle.png';
import placeholder from '../../assets/images/nft-placeholder.png';
import { IMAGE_FETCH_STATUS, useFetchImage } from '@lace/common';
import { useFetchImage } from '@lace/common';

export type Props = {
name: string;
Expand All @@ -14,12 +14,8 @@ export type Props = {
};

export const HandleAddressCard = ({ name, image, copiedMessage, onCopyClick }: Readonly<Props>): JSX.Element => {
const [imageResponse, handleLoad] = useFetchImage({ url: image, fallback: placeholder });
const isLoading = imageResponse?.status === IMAGE_FETCH_STATUS.LOADING;

useEffect(() => {
handleLoad();
}, [handleLoad]);
const imageResponse = useFetchImage({ url: image, fallbackImage: placeholder });
const isLoading = imageResponse?.status === 'loading';

return (
<Base copiedMessage={copiedMessage} copyText={name} onCopyClick={onCopyClick}>
Expand Down
15 changes: 6 additions & 9 deletions packages/core/src/ui/components/Nft/NftImage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useEffect } from 'react';
import React from 'react';
import cn from 'classnames';
import { Spin } from 'antd';
import { useFetchImage, IMAGE_FETCH_STATUS } from '@lace/common';
import { useFetchImage } from '@lace/common';
import NFTPlaceholderImage from '../../assets/images/nft-placeholder.png';
import styles from './NftImage.module.scss';

Expand All @@ -18,18 +18,15 @@ export const NftImage = ({
detailView = false,
withBorder = false
}: NftImageProps): React.ReactElement => {
const [imageResponse, handleLoad] = useFetchImage({ url: image, fallback: NFTPlaceholderImage });
const imageResponse = useFetchImage({ url: image, fallbackImage: NFTPlaceholderImage });

useEffect(() => {
handleLoad();
}, [handleLoad]);

if (imageResponse?.status === IMAGE_FETCH_STATUS.LOADING)
if (imageResponse?.status === 'loading')
return (
<div className={styles.spinnerContainer}>
<Spin />
</div>
);

return (
<img
className={cn(styles.nftImage, {
Expand All @@ -39,7 +36,7 @@ export const NftImage = ({
})}
data-testid={'nft-image'}
alt="NFT"
src={imageResponse?.src}
src={imageResponse.imageSrc}
/>
);
};
Loading

0 comments on commit 46f3d2c

Please sign in to comment.