@arcblock/ux 2.10.14 → 2.10.16

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ {
2
+ "address": "zjdvx8aWRFo96Y974NDRDgynnRKVJyxeRpmv",
3
+ "display": {
4
+ "content": "https://storage.staging.abtnet.io/app/resolve/display",
5
+ "type": "url"
6
+ }
7
+ }
@@ -7,7 +7,6 @@ import get from 'lodash/get';
7
7
  import pako from 'pako';
8
8
  import base64 from 'base64-url';
9
9
  import isSvg from 'is-svg';
10
- import { ReactSVG } from 'react-svg';
11
10
  import noop from 'lodash/noop';
12
11
  import AspectRatioContainer from './aspect-ratio-container';
13
12
  import ImgSvgEmbedder from './svg-embedder/img';
@@ -65,6 +64,7 @@ function NFTDisplay({
65
64
  checkSvg,
66
65
  minimumLoadingTime,
67
66
  onCompleted,
67
+ imageFilter,
68
68
  ...rest
69
69
  }) {
70
70
  const wrapRoot = children => /*#__PURE__*/_jsx(Root, {
@@ -96,16 +96,77 @@ function NFTDisplay({
96
96
  // 首次加载, 对于 url type 的情况, loading 为 true
97
97
  const [state, setState] = useState({
98
98
  loading: isUrlType,
99
- error: false
99
+ error: false,
100
+ loadingUrlType: true,
101
+ urlType: null
100
102
  });
101
103
  const [minimumLoadingReady, setMinimumLoadingReady] = useState(minimumLoadingTime <= 0);
102
104
  // console.log('[debug] render', {type, minimumLoadingTime}, JSON.stringify(state))
103
105
 
106
+ // assemble the complete url
107
+ const getFullContentUrl = ({
108
+ useImageFilter = false,
109
+ t
110
+ } = {}) => {
111
+ const urlObj = new URL(content);
112
+
113
+ // check protocol
114
+ if (window.location.protocol === 'https:' && urlObj.protocol === 'http:') {
115
+ urlObj.protocol = 'https:';
116
+ }
117
+ if (!urlObj.searchParams.has('assetId')) {
118
+ urlObj.searchParams.append('assetId', address);
119
+ }
120
+ if (!urlObj.searchParams.has('vcId') && vcId) {
121
+ urlObj.searchParams.append('vcId', vcId);
122
+ }
123
+
124
+ // image filter
125
+ if (useImageFilter && imageFilter) {
126
+ Object.entries(imageFilter).forEach(([key, value]) => {
127
+ urlObj.searchParams.append(key, value);
128
+ });
129
+ }
130
+
131
+ // other params
132
+ if (t) {
133
+ urlObj.searchParams.append('t', t);
134
+ }
135
+ return urlObj.href;
136
+ };
137
+
138
+ // get url content type
139
+ const getUrlContentType = async () => {
140
+ try {
141
+ const response = await fetch(getFullContentUrl({
142
+ useImageFilter: false,
143
+ t: 'nftdisplay'
144
+ }), {
145
+ method: 'HEAD'
146
+ });
147
+ const contentType = response.headers.get('Content-Type');
148
+ setState({
149
+ ...state,
150
+ loadingUrlType: false,
151
+ urlType: contentType
152
+ });
153
+ } catch (error) {
154
+ console.error('Failed to fetch url content type', error);
155
+ setState({
156
+ ...state,
157
+ loadingUrlType: false,
158
+ urlType: null
159
+ });
160
+ }
161
+ };
104
162
  useEffect(() => {
105
163
  let timer;
106
164
  if (minimumLoadingTime > 0) {
107
165
  timer = setTimeout(() => setMinimumLoadingReady(true), minimumLoadingTime);
108
166
  }
167
+ if (type === 'url') {
168
+ getUrlContentType();
169
+ }
109
170
  return () => clearTimeout(timer);
110
171
  // eslint-disable-next-line react-hooks/exhaustive-deps
111
172
  }, []);
@@ -120,69 +181,73 @@ function NFTDisplay({
120
181
  if (state.error) {
121
182
  throw new Error('Failed to render NFT Display.');
122
183
  }
184
+ const onLoad = () => {
185
+ if (state.loading) {
186
+ setState({
187
+ ...state,
188
+ loading: false
189
+ });
190
+ }
191
+ };
192
+ const onError = () => {
193
+ setState({
194
+ ...state,
195
+ error: true,
196
+ loading: false
197
+ });
198
+ };
199
+
200
+ // render image
201
+ const renderImg = () => {
202
+ const url = getFullContentUrl({
203
+ useImageFilter: true
204
+ });
205
+ return /*#__PURE__*/_jsx("img", {
206
+ src: url,
207
+ onError: onError,
208
+ onLoad: onLoad,
209
+ alt: "NFT Display",
210
+ style: {
211
+ width: 'auto',
212
+ height: 'auto'
213
+ }
214
+ });
215
+ };
216
+
217
+ // render object
218
+ const renderObject = () => {
219
+ const objectType = state.urlType || 'image/svg+xml';
220
+ const url = getFullContentUrl();
221
+ return (
222
+ /*#__PURE__*/
223
+ // eslint-disable-next-line jsx-a11y/alt-text
224
+ _jsx("object", {
225
+ type: objectType,
226
+ data: url,
227
+ onErrorCapture: onError,
228
+ onLoad: onLoad,
229
+ style: {
230
+ width: 'auto',
231
+ height: 'auto',
232
+ pointerEvents: 'none'
233
+ }
234
+ }, url)
235
+ );
236
+ };
123
237
  const renderNFT = () => {
124
238
  if (content) {
125
239
  switch (type) {
126
240
  case 'url':
127
241
  {
128
- const urlObj = new URL(content);
129
- if (!urlObj.searchParams.has('assetId')) {
130
- urlObj.searchParams.append('assetId', address);
242
+ if (state.loadingUrlType) {
243
+ return null;
131
244
  }
132
- if (!urlObj.searchParams.has('vcId') && vcId) {
133
- urlObj.searchParams.append('vcId', vcId);
245
+
246
+ // render image
247
+ if (state.urlType?.startsWith('image') && !state.urlType.startsWith('image/svg')) {
248
+ return renderImg();
134
249
  }
135
- const url = urlObj.href;
136
- const safeOnLoad = () => {
137
- if (state.loading) {
138
- setState({
139
- ...state,
140
- loading: false
141
- });
142
- }
143
- };
144
- return state.fallback ? state.fallback?.() : /*#__PURE__*/_jsx(ReactSVG, {
145
- src: url,
146
- beforeInjection: svg => {
147
- svg.setAttribute('style', 'pointer-events: none; width: 100%; height: 100%;');
148
- },
149
- afterInjection: safeOnLoad,
150
- onError: error => {
151
- let objectType = null;
152
- if (error?.message?.indexOf('Invalid content type: ') > -1) {
153
- objectType = error.message.split('Invalid content type: ')?.[1];
154
- } else if (error?.message?.indexOf('Unable to load SVG file: ') > -1) {
155
- objectType = 'image/svg+xml';
156
- }
157
- setState({
158
- ...state,
159
- // fallback to object, and use objectType to render
160
- fallback: () =>
161
- /*#__PURE__*/
162
- // eslint-disable-next-line jsx-a11y/alt-text
163
- _jsx("object", {
164
- type: objectType,
165
- data: url,
166
- onErrorCapture: () => {
167
- setState({
168
- ...state,
169
- error: true,
170
- loading: false
171
- });
172
- },
173
- onLoad: safeOnLoad,
174
- style: {
175
- width: 'auto',
176
- height: 'auto',
177
- pointerEvents: 'none'
178
- }
179
- }, url)
180
- });
181
- }
182
- // evalScripts="always"
183
- ,
184
- wrapper: "div"
185
- });
250
+ return renderObject();
186
251
  }
187
252
  case 'uri':
188
253
  {
@@ -259,7 +324,10 @@ NFTDisplay.propTypes = {
259
324
  // loading 最小显示时间 (避免闪烁)
260
325
  minimumLoadingTime: PropTypes.number,
261
326
  // 完成回调, 无论加载成功|失败
262
- onCompleted: PropTypes.func
327
+ onCompleted: PropTypes.func,
328
+ // 图片处理,参考:https://team.arcblock.io/comment/docs/c158aee4-accd-42f4-9ced-6a23f28c00e0/en/blocklet-image-service-guide
329
+ // 配置参数会全部转发给 Image Service
330
+ imageFilter: PropTypes.object
263
331
  };
264
332
  NFTDisplay.defaultProps = {
265
333
  component: 'span',
@@ -271,7 +339,8 @@ NFTDisplay.defaultProps = {
271
339
  preferredSvgEmbedder: 'img',
272
340
  checkSvg: false,
273
341
  minimumLoadingTime: 0,
274
- onCompleted: noop
342
+ onCompleted: noop,
343
+ imageFilter: null
275
344
  };
276
345
  const Root = styled('div')`
277
346
  display: flex;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcblock/ux",
3
- "version": "2.10.14",
3
+ "version": "2.10.16",
4
4
  "description": "Common used react components for arcblock products",
5
5
  "keywords": [
6
6
  "react",
@@ -54,12 +54,12 @@
54
54
  "react": ">=18.2.0",
55
55
  "react-router-dom": ">=6.22.3"
56
56
  },
57
- "gitHead": "11320e1a4438e518799515f9e30f5fa091f6cf60",
57
+ "gitHead": "4e446d45fbf0db374ffe0ebc9ba0b9dce08fbe0a",
58
58
  "dependencies": {
59
59
  "@arcblock/did-motif": "^1.1.13",
60
- "@arcblock/icons": "^2.10.14",
61
- "@arcblock/nft-display": "^2.10.14",
62
- "@arcblock/react-hooks": "^2.10.14",
60
+ "@arcblock/icons": "^2.10.16",
61
+ "@arcblock/nft-display": "^2.10.16",
62
+ "@arcblock/react-hooks": "^2.10.16",
63
63
  "@babel/plugin-syntax-dynamic-import": "^7.8.3",
64
64
  "@fontsource/inter": "^5.0.16",
65
65
  "@fontsource/ubuntu-mono": "^5.0.18",
@@ -7,7 +7,6 @@ import get from 'lodash/get';
7
7
  import pako from 'pako';
8
8
  import base64 from 'base64-url';
9
9
  import isSvg from 'is-svg';
10
- import { ReactSVG } from 'react-svg';
11
10
  import noop from 'lodash/noop';
12
11
 
13
12
  import AspectRatioContainer from './aspect-ratio-container';
@@ -64,6 +63,7 @@ function NFTDisplay({
64
63
  checkSvg,
65
64
  minimumLoadingTime,
66
65
  onCompleted,
66
+ imageFilter,
67
67
  ...rest
68
68
  }) {
69
69
  const wrapRoot = (children) => (
@@ -90,15 +90,71 @@ function NFTDisplay({
90
90
  const isUrlType = type === 'url';
91
91
 
92
92
  // 首次加载, 对于 url type 的情况, loading 为 true
93
- const [state, setState] = useState({ loading: isUrlType, error: false });
93
+ const [state, setState] = useState({
94
+ loading: isUrlType,
95
+ error: false,
96
+ loadingUrlType: true,
97
+ urlType: null,
98
+ });
94
99
  const [minimumLoadingReady, setMinimumLoadingReady] = useState(minimumLoadingTime <= 0);
95
100
  // console.log('[debug] render', {type, minimumLoadingTime}, JSON.stringify(state))
96
101
 
102
+ // assemble the complete url
103
+ const getFullContentUrl = ({ useImageFilter = false, t } = {}) => {
104
+ const urlObj = new URL(content);
105
+
106
+ // check protocol
107
+ if (window.location.protocol === 'https:' && urlObj.protocol === 'http:') {
108
+ urlObj.protocol = 'https:';
109
+ }
110
+
111
+ if (!urlObj.searchParams.has('assetId')) {
112
+ urlObj.searchParams.append('assetId', address);
113
+ }
114
+
115
+ if (!urlObj.searchParams.has('vcId') && vcId) {
116
+ urlObj.searchParams.append('vcId', vcId);
117
+ }
118
+
119
+ // image filter
120
+ if (useImageFilter && imageFilter) {
121
+ Object.entries(imageFilter).forEach(([key, value]) => {
122
+ urlObj.searchParams.append(key, value);
123
+ });
124
+ }
125
+
126
+ // other params
127
+ if (t) {
128
+ urlObj.searchParams.append('t', t);
129
+ }
130
+
131
+ return urlObj.href;
132
+ };
133
+
134
+ // get url content type
135
+ const getUrlContentType = async () => {
136
+ try {
137
+ const response = await fetch(getFullContentUrl({ useImageFilter: false, t: 'nftdisplay' }), {
138
+ method: 'HEAD',
139
+ });
140
+ const contentType = response.headers.get('Content-Type');
141
+ setState({ ...state, loadingUrlType: false, urlType: contentType });
142
+ } catch (error) {
143
+ console.error('Failed to fetch url content type', error);
144
+ setState({ ...state, loadingUrlType: false, urlType: null });
145
+ }
146
+ };
147
+
97
148
  useEffect(() => {
98
149
  let timer;
99
150
  if (minimumLoadingTime > 0) {
100
151
  timer = setTimeout(() => setMinimumLoadingReady(true), minimumLoadingTime);
101
152
  }
153
+
154
+ if (type === 'url') {
155
+ getUrlContentType();
156
+ }
157
+
102
158
  return () => clearTimeout(timer);
103
159
  // eslint-disable-next-line react-hooks/exhaustive-deps
104
160
  }, []);
@@ -115,66 +171,55 @@ function NFTDisplay({
115
171
  throw new Error('Failed to render NFT Display.');
116
172
  }
117
173
 
174
+ const onLoad = () => {
175
+ if (state.loading) {
176
+ setState({ ...state, loading: false });
177
+ }
178
+ };
179
+
180
+ const onError = () => {
181
+ setState({ ...state, error: true, loading: false });
182
+ };
183
+
184
+ // render image
185
+ const renderImg = () => {
186
+ const url = getFullContentUrl({ useImageFilter: true });
187
+ return (
188
+ <img src={url} onError={onError} onLoad={onLoad} alt="NFT Display" style={{ width: 'auto', height: 'auto' }} />
189
+ );
190
+ };
191
+
192
+ // render object
193
+ const renderObject = () => {
194
+ const objectType = state.urlType || 'image/svg+xml';
195
+ const url = getFullContentUrl();
196
+ return (
197
+ // eslint-disable-next-line jsx-a11y/alt-text
198
+ <object
199
+ key={url}
200
+ type={objectType}
201
+ data={url}
202
+ onErrorCapture={onError}
203
+ onLoad={onLoad}
204
+ style={{ width: 'auto', height: 'auto', pointerEvents: 'none' }}
205
+ />
206
+ );
207
+ };
208
+
118
209
  const renderNFT = () => {
119
210
  if (content) {
120
211
  switch (type) {
121
212
  case 'url': {
122
- const urlObj = new URL(content);
123
- if (!urlObj.searchParams.has('assetId')) {
124
- urlObj.searchParams.append('assetId', address);
213
+ if (state.loadingUrlType) {
214
+ return null;
125
215
  }
126
216
 
127
- if (!urlObj.searchParams.has('vcId') && vcId) {
128
- urlObj.searchParams.append('vcId', vcId);
217
+ // render image
218
+ if (state.urlType?.startsWith('image') && !state.urlType.startsWith('image/svg')) {
219
+ return renderImg();
129
220
  }
130
221
 
131
- const url = urlObj.href;
132
-
133
- const safeOnLoad = () => {
134
- if (state.loading) {
135
- setState({ ...state, loading: false });
136
- }
137
- };
138
-
139
- return state.fallback ? (
140
- state.fallback?.()
141
- ) : (
142
- <ReactSVG
143
- src={url}
144
- beforeInjection={(svg) => {
145
- svg.setAttribute('style', 'pointer-events: none; width: 100%; height: 100%;');
146
- }}
147
- afterInjection={safeOnLoad}
148
- onError={(error) => {
149
- let objectType = null;
150
- if (error?.message?.indexOf('Invalid content type: ') > -1) {
151
- objectType = error.message.split('Invalid content type: ')?.[1];
152
- } else if (error?.message?.indexOf('Unable to load SVG file: ') > -1) {
153
- objectType = 'image/svg+xml';
154
- }
155
-
156
- setState({
157
- ...state,
158
- // fallback to object, and use objectType to render
159
- fallback: () => (
160
- // eslint-disable-next-line jsx-a11y/alt-text
161
- <object
162
- key={url}
163
- type={objectType}
164
- data={url}
165
- onErrorCapture={() => {
166
- setState({ ...state, error: true, loading: false });
167
- }}
168
- onLoad={safeOnLoad}
169
- style={{ width: 'auto', height: 'auto', pointerEvents: 'none' }}
170
- />
171
- ),
172
- });
173
- }}
174
- // evalScripts="always"
175
- wrapper="div"
176
- />
177
- );
222
+ return renderObject();
178
223
  }
179
224
  case 'uri': {
180
225
  return (
@@ -245,6 +290,9 @@ NFTDisplay.propTypes = {
245
290
  minimumLoadingTime: PropTypes.number,
246
291
  // 完成回调, 无论加载成功|失败
247
292
  onCompleted: PropTypes.func,
293
+ // 图片处理,参考:https://team.arcblock.io/comment/docs/c158aee4-accd-42f4-9ced-6a23f28c00e0/en/blocklet-image-service-guide
294
+ // 配置参数会全部转发给 Image Service
295
+ imageFilter: PropTypes.object,
248
296
  };
249
297
 
250
298
  NFTDisplay.defaultProps = {
@@ -258,6 +306,7 @@ NFTDisplay.defaultProps = {
258
306
  checkSvg: false,
259
307
  minimumLoadingTime: 0,
260
308
  onCompleted: noop,
309
+ imageFilter: null,
261
310
  };
262
311
 
263
312
  const Root = styled('div')`