Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: convert icon-factory.js to typescript #23823

Merged
merged 4 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 0 additions & 69 deletions ui/helpers/utils/icon-factory.js

This file was deleted.

141 changes: 141 additions & 0 deletions ui/helpers/utils/icon-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { isValidHexAddress } from '../../../shared/modules/hexstring-utils';

/**
* Defines the metadata for a token including optional icon URL.
*/
type TokenMetadata = {
iconUrl: string;
};

/**
* A factory for generating icons for cryptocurrency addresses using Jazzicon or predefined token metadata.
*/
class IconFactory {
/**
* Function to generate a Jazzicon SVG element.
*/
jazzicon: (diameter: number, seed: number) => SVGSVGElement;

/**
* Cache for storing generated SVG elements to avoid re-rendering.
*/
cache: { [key: string]: SVGSVGElement };

/**
* Constructs an IconFactory instance with a given Jazzicon function.
*
* @param jazzicon - A function that returns a Jazzicon SVG given a diameter and seed.
*/
constructor(jazzicon: (diameter: number, seed: number) => SVGSVGElement) {
this.jazzicon = jazzicon;
this.cache = {};
}

/**
* Generates an icon for a given address. Returns a predefined image or generates a new Jazzicon.
*
* @param address - The cryptocurrency address to generate the icon for.
* @param diameter - The diameter of the icon to be generated.
* @param tokenMetadata - Metadata containing optional icon URL for predefined icons.
* @returns An HTML element representing the icon.
*/
iconForAddress(
address: string,
diameter: number,
tokenMetadata?: Partial<TokenMetadata>,
): HTMLElement | SVGSVGElement {
if (iconExistsFor(address, tokenMetadata)) {
return imageElFor(tokenMetadata);
}

return this.generateIdenticonSvg(address, diameter);
}

/**
* Generates or retrieves from cache a Jazzicon SVG for a given address and diameter.
*
* @param address - The cryptocurrency address for the identicon.
* @param diameter - The diameter of the identicon.
* @returns A Jazzicon SVG element.
*/
generateIdenticonSvg(address: string, diameter: number): SVGSVGElement {
const cacheId = `${address}:${diameter}`;
const identicon: SVGSVGElement =
this.cache[cacheId] ||
(this.cache[cacheId] = this.generateNewIdenticon(address, diameter));
const cleanCopy: SVGSVGElement = identicon.cloneNode(true) as SVGSVGElement;
return cleanCopy;
}

/**
* Generates a new Jazzicon SVG for a given address and diameter.
*
* @param address - The cryptocurrency address for the identicon.
* @param diameter - The diameter of the identicon.
* @returns A new Jazzicon SVG element.
*/
generateNewIdenticon(address: string, diameter: number): SVGSVGElement {
const numericRepresentation = jsNumberForAddress(address);
const identicon = this.jazzicon(diameter, numericRepresentation);
return identicon;
}
}

let iconFactory: IconFactory | undefined;

/**
* Generates or retrieves an existing IconFactory instance.
*
* @param jazzicon - A function that returns a Jazzicon SVG given a diameter and seed.
* @returns An IconFactory instance.
*/
export default function iconFactoryGenerator(
jazzicon: (diameter: number, seed: number) => SVGSVGElement,
): IconFactory {
if (!iconFactory) {
iconFactory = new IconFactory(jazzicon);
}
return iconFactory;
}

/**
* Determines if an icon already exists for a given address based on token metadata.
*
* @param address - The cryptocurrency address.
* @param tokenMetadata - Metadata containing optional icon URL.
* @returns True if an icon exists, otherwise false.
*/
function iconExistsFor(
address: string,
tokenMetadata?: Partial<TokenMetadata>,
): tokenMetadata is TokenMetadata {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be boolean here?

Suggested change
): tokenMetadata is TokenMetadata {
): boolean {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the iconExistsFor function acts as a type guard via a type predicate (the tokenMetadata is TokenMetadata part).

It enables the following, where tokenMetadata is optional in iconExistsFor, but not in imageElFor. TypeScript isn't "smart" enough to infer that tokenMetadata is never null/undefined when iconExistsFor returns true, so a type predicate is used to give TypeScript that information, otherwise we'd have to cast TokenMetadata or check for null/undefined again inside the if statement.

    if (iconExistsFor(address, tokenMetadata)) {
      return imageElFor(tokenMetadata);
    }

See https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates for details.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks ! it makes sense !

if (!tokenMetadata?.iconUrl) {
return false;
}
return isValidHexAddress(address, { allowNonPrefixed: false });
}

/**
* Creates an HTMLImageElement for a given token metadata.
*
* @param tokenMetadata - Metadata containing the icon URL. Defaults to an empty object.
* @returns An HTMLImageElement with the source set to the icon URL.
*/
function imageElFor(tokenMetadata: TokenMetadata): HTMLImageElement {
const img = document.createElement('img');
img.src = tokenMetadata.iconUrl;
img.style.width = '100%';
return img;
}

/**
* Converts a hexadecimal address into a numerical seed.
*
* @param address - The cryptocurrency address.
* @returns A numerical seed derived from the address.
*/
function jsNumberForAddress(address: string): number {
const addr = address.slice(2, 10);
const seed = parseInt(addr, 16);
return seed;
}