Skip to content

Commit 9484839

Browse files
authored
Image support in string as source, for svg strings (#2983)
* Image support in string as source, for svg strings * code review refactor, image utils testing * removed ImagePropTypes from Image index file
1 parent bbeb393 commit 9484839

File tree

4 files changed

+78
-18
lines changed

4 files changed

+78
-18
lines changed

src/components/icon/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {getAsset, isSvg, isBase64ImageContent} from '../../utils/imageUtils';
77
import {RecorderProps} from '../../typings/recorderTypes';
88
import Badge, {BadgeProps} from '../badge';
99
import SvgImage from '../svgImage';
10+
import type {ImageSourceType} from '../image';
1011

1112
export type IconProps = Omit<ImageProps, 'source'> &
1213
MarginModifiers &
@@ -35,7 +36,7 @@ export type IconProps = Omit<ImageProps, 'source'> &
3536
* whether the icon should flip horizontally on RTL
3637
*/
3738
supportRTL?: boolean;
38-
source?: ImageProps['source'];
39+
source?: ImageSourceType
3940
};
4041

4142
/**

src/components/image/index.tsx

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ import {
99
NativeSyntheticEvent,
1010
ImageErrorEventData
1111
} from 'react-native';
12-
// @ts-expect-error No typings available for 'deprecated-react-native-prop-types'
13-
import {ImagePropTypes} from 'deprecated-react-native-prop-types';
1412
import {
1513
Constants,
1614
asBaseComponent,
@@ -24,14 +22,17 @@ import Overlay, {OverlayTypeType, OverlayIntensityType} from '../overlay';
2422
import SvgImage from '../svgImage';
2523
import View from '../view';
2624
import {Colors} from '../../style';
25+
import {ComponentStatics} from 'src/typings/common';
2726

28-
export type ImageProps = RNImageProps &
27+
export type ImageSourceType = string | RNImageProps['source'];
28+
29+
export type ImageProps = Omit<RNImageProps, 'source'> &
2930
MarginModifiers &
3031
RecorderProps & {
3132
/**
3233
* custom source transform handler for manipulating the image source (great for size control)
3334
*/
34-
sourceTransformer?: (props: any) => ImagePropTypes.source;
35+
sourceTransformer?: (props: any) => ImageSourceType;
3536
/**
3637
* if provided image source will be driven from asset name
3738
*/
@@ -76,7 +77,7 @@ export type ImageProps = RNImageProps &
7677
/**
7778
* Default image source in case of an error
7879
*/
79-
errorSource?: ImagePropTypes.source;
80+
errorSource?: ImageSourceType;
8081
/**
8182
* An imageId that can be used in sourceTransformer logic
8283
*/
@@ -95,13 +96,14 @@ export type ImageProps = RNImageProps &
9596
* The image height
9697
*/
9798
height?: string | number;
99+
source: ImageSourceType;
98100
};
99101

100102
type Props = ImageProps & ForwardRefInjectedProps & BaseComponentInjectedProps;
101103

102104
type State = {
103105
error: boolean;
104-
prevSource: ImagePropTypes.source;
106+
prevSource: ImageSourceType;
105107
};
106108

107109
/**
@@ -122,7 +124,7 @@ class Image extends PureComponent<Props, State> {
122124
public static overlayTypes = Overlay.overlayTypes;
123125
public static overlayIntensityType = Overlay.intensityTypes;
124126

125-
sourceTransformer?: (props: any) => ImagePropTypes.source;
127+
sourceTransformer?: (props: any) => ImageSourceType;
126128

127129
constructor(props: Props) {
128130
super(props);
@@ -160,7 +162,7 @@ class Image extends PureComponent<Props, State> {
160162
return !!overlayType || this.isGif() || !_.isUndefined(customOverlayContent);
161163
}
162164

163-
getVerifiedSource(source?: ImagePropTypes.source) {
165+
getVerifiedSource(source?: ImageSourceType) {
164166
if (_.get(source, 'uri') === null || _.get(source, 'uri') === '') {
165167
// @ts-ignore
166168
return {...source, uri: undefined};
@@ -308,4 +310,6 @@ const styles = StyleSheet.create({
308310

309311
hoistNonReactStatic(Image, RNImage);
310312
export {Image};
311-
export default asBaseComponent<ImageProps, typeof Image & typeof RNImage>(Image, {modifiersOptions: {margins: true}});
313+
export default asBaseComponent<ImageProps, ComponentStatics<typeof Image & typeof RNImage>>(Image, {
314+
modifiersOptions: {margins: true}
315+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import {isSvgUri, isSvg, isBase64ImageContent, getAsset} from '../imageUtils';
2+
3+
const svgImage = {uri: 'https://example.com/image.svg'};
4+
const randomPngImage = {uri: 'https://picsum.photos/300'};
5+
const svgString =
6+
'<?xml version="1.0" encoding="UTF-8"?><svg data-bbox="4 2.122 17.878 17.878" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24" data-type="color"><g><path fill="#116DFF" d="M15.232 2.854a2.5 2.5 0 0 1 3.536 0l2.378 2.378a2.5 2.5 0 0 1 0 3.536L10.5 19.414A2 2 0 0 1 9.086 20H5a1 1 0 0 1-1-1v-4.086a2 2 0 0 1 .586-1.414L15.232 2.854ZM6 18h3.086l7.255-7.255-3.086-3.086L6 14.914V18Zm8.67-11.755 3.085 3.086 1.977-1.977a.5.5 0 0 0 0-.708l-2.378-2.378a.5.5 0 0 0-.708 0L14.67 6.245Z" clip-rule="evenodd" fill-rule="evenodd" data-color="1"/></g></svg>';
7+
const base64Image =
8+
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAApgAAAKYB3X3/OAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANCSURBVEiJtZZPbBtFFMZ/M7ubXdtdb1xSFyeilBapySVU8h8OoFaooFSqiihIVIpQBKci6KEg9Q6H9kovIHoCIVQJJCKE1ENFjnAgcaSGC6rEnxBwA04Tx43t2FnvDAfjkNibxgHxnWb2e/u992bee7tCa00YFsffekFY+nUzFtjW0LrvjRXrCDIAaPLlW0nHL0SsZtVoaF98mLrx3pdhOqLtYPHChahZcYYO7KvPFxvRl5XPp1sN3adWiD1ZAqD6XYK1b/dvE5IWryTt2udLFedwc1+9kLp+vbbpoDh+6TklxBeAi9TL0taeWpdmZzQDry0AcO+jQ12RyohqqoYoo8RDwJrU+qXkjWtfi8Xxt58BdQuwQs9qC/afLwCw8tnQbqYAPsgxE1S6F3EAIXux2oQFKm0ihMsOF71dHYx+f3NND68ghCu1YIoePPQN1pGRABkJ6Bus96CutRZMydTl+TvuiRW1m3n0eDl0vRPcEysqdXn+jsQPsrHMquGeXEaY4Yk4wxWcY5V/9scqOMOVUFthatyTy8QyqwZ+kDURKoMWxNKr2EeqVKcTNOajqKoBgOE28U4tdQl5p5bwCw7BWquaZSzAPlwjlithJtp3pTImSqQRrb2Z8PHGigD4RZuNX6JYj6wj7O4TFLbCO/Mn/m8R+h6rYSUb3ekokRY6f/YukArN979jcW+V/S8g0eT/N3VN3kTqWbQ428m9/8k0P/1aIhF36PccEl6EhOcAUCrXKZXXWS3XKd2vc/TRBG9O5ELC17MmWubD2nKhUKZa26Ba2+D3P+4/MNCFwg59oWVeYhkzgN/JDR8deKBoD7Y+ljEjGZ0sosXVTvbc6RHirr2reNy1OXd6pJsQ+gqjk8VWFYmHrwBzW/n+uMPFiRwHB2I7ih8ciHFxIkd/3Omk5tCDV1t+2nNu5sxxpDFNx+huNhVT3/zMDz8usXC3ddaHBj1GHj/As08fwTS7Kt1HBTmyN29vdwAw+/wbwLVOJ3uAD1wi/dUH7Qei66PfyuRj4Ik9is+hglfbkbfR3cnZm7chlUWLdwmprtCohX4HUtlOcQjLYCu+fzGJH2QRKvP3UNz8bWk1qMxjGTOMThZ3kvgLI5AzFfo379UAAAAASUVORK5CYII=';
9+
10+
describe('ImageUtils', () => {
11+
describe('isSvgUri', () => {
12+
it('should return true if the uri is svg', () => {
13+
const result = isSvgUri(svgImage);
14+
expect(result).toBe(true);
15+
});
16+
it('should return false if the uri is not svg', () => {
17+
const result = isSvgUri(randomPngImage);
18+
expect(result).toBe(false);
19+
});
20+
});
21+
22+
describe('isSvg', () => {
23+
it('should return true if the string is svg', () => {
24+
const result = isSvg(svgString);
25+
expect(result).toBe(true);
26+
});
27+
it('should return false if the string is not svg', () => {
28+
const result = isSvg('<svg data-bbox="4.../>');
29+
expect(result).toBe(false);
30+
});
31+
});
32+
33+
describe('isBase64ImageContent', () => {
34+
it('should return true if the string is base64', () => {
35+
const result = isBase64ImageContent(base64Image);
36+
expect(result).toBe(true);
37+
});
38+
it('should return false if the string is not base64', () => {
39+
const result = isBase64ImageContent(svgString);
40+
expect(result).toBe(false);
41+
});
42+
});
43+
44+
describe('getAsset', () => {
45+
it('should return the asset if asset group and asset name exsist', () => {
46+
const result = getAsset('search', 'icons');
47+
expect(result).toBeDefined();
48+
});
49+
it('should return the asset if asset group and asset name exsist', () => {
50+
const result = getAsset('blah', 'icons');
51+
expect(result).toBeUndefined();
52+
});
53+
});
54+
});

src/utils/imageUtils.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import get from 'lodash/get';
2-
import {ImageProps} from 'react-native';
32
import Assets from '../assets';
4-
import {Constants} from '../commons/new';
3+
import type {ImageSourceType} from '../components/image';
54

6-
export function isSvgUri(source?: ImageProps['source']) {
5+
export function isSvgUri(source?: ImageSourceType) {
76
// @ts-expect-error
87
return typeof source === 'object' && source?.uri?.endsWith?.('.svg');
98
}
109

11-
export function isSvg(source?: ImageProps['source']) {
12-
return typeof source === 'function' || isSvgUri(source) || (Constants.isWeb && isSvgData(source));
10+
export function isSvg(source?: ImageSourceType) {
11+
return typeof source === 'function' || isSvgUri(source) || isSvgData(source);
1312
}
1413

1514
export function isBase64ImageContent(data: string) {
@@ -22,7 +21,9 @@ export function getAsset(assetName = '', assetGroup = '') {
2221
return get(Assets, `${assetGroup}.${assetName}`);
2322
}
2423

25-
function isSvgData(source?: ImageProps['source']) {
26-
const sourceString = (source as string);
27-
return typeof source === 'string' && (sourceString.includes('</svg>') || sourceString.includes('data:image/svg'));
24+
function isSvgData(source?: ImageSourceType) {
25+
if (typeof source === 'string') {
26+
const sourceString = source;
27+
return sourceString.includes('</svg>') || sourceString.includes('data:image/svg');
28+
}
2829
}

0 commit comments

Comments
 (0)