@atlaskit/media-file-preview 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1 -0
- package/LICENSE.md +13 -0
- package/README.md +9 -0
- package/dist/cjs/analytics.js +50 -0
- package/dist/cjs/errors.js +143 -0
- package/dist/cjs/getPreview/cache.js +39 -0
- package/dist/cjs/getPreview/getPreview.js +119 -0
- package/dist/cjs/getPreview/helpers.js +167 -0
- package/dist/cjs/getPreview/index.js +56 -0
- package/dist/cjs/getPreview/objectURLCache.js +85 -0
- package/dist/cjs/getPreview/videoSnapshot.js +63 -0
- package/dist/cjs/globalScope/getSSRData.js +14 -0
- package/dist/cjs/globalScope/globalScope.js +66 -0
- package/dist/cjs/globalScope/index.js +37 -0
- package/dist/cjs/globalScope/printScript.js +32 -0
- package/dist/cjs/globalScope/types.js +5 -0
- package/dist/cjs/helpers.js +56 -0
- package/dist/cjs/index.js +12 -0
- package/dist/cjs/types.js +5 -0
- package/dist/cjs/useFilePreview.js +355 -0
- package/dist/es2019/analytics.js +44 -0
- package/dist/es2019/errors.js +90 -0
- package/dist/es2019/getPreview/cache.js +30 -0
- package/dist/es2019/getPreview/getPreview.js +75 -0
- package/dist/es2019/getPreview/helpers.js +77 -0
- package/dist/es2019/getPreview/index.js +3 -0
- package/dist/es2019/getPreview/objectURLCache.js +44 -0
- package/dist/es2019/getPreview/videoSnapshot.js +41 -0
- package/dist/es2019/globalScope/getSSRData.js +8 -0
- package/dist/es2019/globalScope/globalScope.js +48 -0
- package/dist/es2019/globalScope/index.js +2 -0
- package/dist/es2019/globalScope/printScript.js +16 -0
- package/dist/es2019/globalScope/types.js +1 -0
- package/dist/es2019/helpers.js +53 -0
- package/dist/es2019/index.js +1 -0
- package/dist/es2019/types.js +1 -0
- package/dist/es2019/useFilePreview.js +333 -0
- package/dist/esm/analytics.js +44 -0
- package/dist/esm/errors.js +133 -0
- package/dist/esm/getPreview/cache.js +32 -0
- package/dist/esm/getPreview/getPreview.js +112 -0
- package/dist/esm/getPreview/helpers.js +161 -0
- package/dist/esm/getPreview/index.js +3 -0
- package/dist/esm/getPreview/objectURLCache.js +78 -0
- package/dist/esm/getPreview/videoSnapshot.js +56 -0
- package/dist/esm/globalScope/getSSRData.js +8 -0
- package/dist/esm/globalScope/globalScope.js +56 -0
- package/dist/esm/globalScope/index.js +2 -0
- package/dist/esm/globalScope/printScript.js +25 -0
- package/dist/esm/globalScope/types.js +1 -0
- package/dist/esm/helpers.js +49 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/types.js +1 -0
- package/dist/esm/useFilePreview.js +348 -0
- package/dist/types/analytics.d.ts +28 -0
- package/dist/types/errors.d.ts +42 -0
- package/dist/types/getPreview/cache.d.ts +21 -0
- package/dist/types/getPreview/getPreview.d.ts +9 -0
- package/dist/types/getPreview/helpers.d.ts +10 -0
- package/dist/types/getPreview/index.d.ts +3 -0
- package/dist/types/getPreview/objectURLCache.d.ts +12 -0
- package/dist/types/getPreview/videoSnapshot.d.ts +1 -0
- package/dist/types/globalScope/getSSRData.d.ts +3 -0
- package/dist/types/globalScope/globalScope.d.ts +15 -0
- package/dist/types/globalScope/index.d.ts +4 -0
- package/dist/types/globalScope/printScript.d.ts +2 -0
- package/dist/types/globalScope/types.d.ts +8 -0
- package/dist/types/helpers.d.ts +10 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/types.d.ts +12 -0
- package/dist/types/useFilePreview.d.ts +33 -0
- package/dist/types-ts4.5/analytics.d.ts +28 -0
- package/dist/types-ts4.5/errors.d.ts +42 -0
- package/dist/types-ts4.5/getPreview/cache.d.ts +21 -0
- package/dist/types-ts4.5/getPreview/getPreview.d.ts +9 -0
- package/dist/types-ts4.5/getPreview/helpers.d.ts +10 -0
- package/dist/types-ts4.5/getPreview/index.d.ts +3 -0
- package/dist/types-ts4.5/getPreview/objectURLCache.d.ts +12 -0
- package/dist/types-ts4.5/getPreview/videoSnapshot.d.ts +1 -0
- package/dist/types-ts4.5/globalScope/getSSRData.d.ts +3 -0
- package/dist/types-ts4.5/globalScope/globalScope.d.ts +15 -0
- package/dist/types-ts4.5/globalScope/index.d.ts +4 -0
- package/dist/types-ts4.5/globalScope/printScript.d.ts +2 -0
- package/dist/types-ts4.5/globalScope/types.d.ts +8 -0
- package/dist/types-ts4.5/helpers.d.ts +10 -0
- package/dist/types-ts4.5/index.d.ts +2 -0
- package/dist/types-ts4.5/types.d.ts +12 -0
- package/dist/types-ts4.5/useFilePreview.d.ts +33 -0
- package/package.json +98 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { EventEmitter2 } from 'eventemitter2';
|
|
2
|
+
import { LRUMap } from 'lru_map';
|
|
3
|
+
export const PREVIEW_CACHE_LRU_SIZE = 50;
|
|
4
|
+
class ExtendedLRUCache extends LRUMap {
|
|
5
|
+
constructor(limit) {
|
|
6
|
+
super(limit);
|
|
7
|
+
this.eventEmitter = new EventEmitter2();
|
|
8
|
+
}
|
|
9
|
+
shift() {
|
|
10
|
+
const entry = super.shift();
|
|
11
|
+
this.eventEmitter.emit('shift', entry);
|
|
12
|
+
return entry;
|
|
13
|
+
}
|
|
14
|
+
on(event, callback) {
|
|
15
|
+
this.eventEmitter.on(event, callback);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export class ObjectURLCache {
|
|
19
|
+
constructor(size) {
|
|
20
|
+
this.cache = new ExtendedLRUCache(size);
|
|
21
|
+
this.cache.on('shift', entry => {
|
|
22
|
+
if (entry && entry[1].dataURI) {
|
|
23
|
+
URL.revokeObjectURL(entry[1].dataURI);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
has(key) {
|
|
28
|
+
return !!this.cache.find(key);
|
|
29
|
+
}
|
|
30
|
+
get(key) {
|
|
31
|
+
return this.cache.get(key);
|
|
32
|
+
}
|
|
33
|
+
set(key, value) {
|
|
34
|
+
this.cache.set(key, value);
|
|
35
|
+
}
|
|
36
|
+
remove(key) {
|
|
37
|
+
const removed = this.cache.delete(key);
|
|
38
|
+
removed && URL.revokeObjectURL(removed.dataURI);
|
|
39
|
+
}
|
|
40
|
+
clear() {
|
|
41
|
+
this.cache.clear();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export const createObjectURLCache = () => new ObjectURLCache(PREVIEW_CACHE_LRU_SIZE);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const takeSnapshot = async blob => {
|
|
2
|
+
return new Promise((resolve, reject) => {
|
|
3
|
+
const url = URL.createObjectURL(blob);
|
|
4
|
+
const video = document.createElement('video');
|
|
5
|
+
video.preload = 'metadata';
|
|
6
|
+
video.src = url;
|
|
7
|
+
video.muted = true;
|
|
8
|
+
video.play().catch(() => {
|
|
9
|
+
return reject(new Error('failed to play video'));
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
|
|
13
|
+
video.addEventListener('timeupdate', function timeUpdateHandler() {
|
|
14
|
+
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
|
|
15
|
+
video.removeEventListener('timeupdate', timeUpdateHandler);
|
|
16
|
+
video.pause();
|
|
17
|
+
URL.revokeObjectURL(url);
|
|
18
|
+
//create canvas to draw our first frame on.
|
|
19
|
+
|
|
20
|
+
if (!video.videoWidth && !video.videoHeight) {
|
|
21
|
+
return reject(new Error('error retrieving video dimensions'));
|
|
22
|
+
}
|
|
23
|
+
const canvas = document.createElement('canvas');
|
|
24
|
+
canvas.width = video.videoWidth;
|
|
25
|
+
canvas.height = video.videoHeight;
|
|
26
|
+
const context = canvas.getContext('2d');
|
|
27
|
+
if (!context) {
|
|
28
|
+
return reject(new Error('error creating canvas context'));
|
|
29
|
+
}
|
|
30
|
+
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
31
|
+
const dataURL = canvas.toDataURL('image/jpeg', 0.85);
|
|
32
|
+
resolve(dataURL);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
|
|
36
|
+
video.addEventListener('error', () => {
|
|
37
|
+
reject(new Error('failed to load video'));
|
|
38
|
+
URL.revokeObjectURL(url);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { printFunctionCall, printScript } from './printScript';
|
|
2
|
+
// ----- WARNING -----
|
|
3
|
+
// This is a very sensitive fraction of code.
|
|
4
|
+
// Any changes to this file must be tested directly in product before merging.
|
|
5
|
+
// The scripts printed here might differ from what we observe in our internal tests
|
|
6
|
+
// due to minimification, for example.
|
|
7
|
+
export const GLOBAL_MEDIA_CARD_SSR = 'mediaCardSsr';
|
|
8
|
+
export const GLOBAL_MEDIA_NAMESPACE = '__MEDIA_INTERNAL';
|
|
9
|
+
export function getMediaGlobalScope(globalScope = window) {
|
|
10
|
+
// Must match GLOBAL_MEDIA_NAMESPACE. Can't reference the constant from here.
|
|
11
|
+
const namespace = '__MEDIA_INTERNAL';
|
|
12
|
+
if (!globalScope[namespace]) {
|
|
13
|
+
globalScope[namespace] = {};
|
|
14
|
+
}
|
|
15
|
+
return globalScope[namespace];
|
|
16
|
+
}
|
|
17
|
+
export function getMediaCardSSR(globalScope = window) {
|
|
18
|
+
const globalMedia = getMediaGlobalScope(globalScope);
|
|
19
|
+
// Must match GLOBAL_MEDIA_CARD_SSR. Can't reference the constant from here.
|
|
20
|
+
const key = 'mediaCardSsr';
|
|
21
|
+
if (!globalMedia[key]) {
|
|
22
|
+
globalMedia[key] = {};
|
|
23
|
+
}
|
|
24
|
+
return globalMedia[key];
|
|
25
|
+
}
|
|
26
|
+
const dashed = param => param ? `-${param}` : '';
|
|
27
|
+
export const getKey = ({
|
|
28
|
+
id,
|
|
29
|
+
collectionName,
|
|
30
|
+
occurrenceKey
|
|
31
|
+
}) => `${id}${dashed(collectionName)}${dashed(occurrenceKey)}`;
|
|
32
|
+
export const storeDataURI = (key, dataURI, dimensions, error, globalScope = window) => {
|
|
33
|
+
const mediaCardSsr = getMediaCardSSR(globalScope);
|
|
34
|
+
mediaCardSsr[key] = {
|
|
35
|
+
dataURI,
|
|
36
|
+
dimensions,
|
|
37
|
+
error
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
const generateScript = (identifier, dataURI, dimensions, error) => {
|
|
41
|
+
const functionCall = printFunctionCall(storeDataURI, getKey(identifier), dataURI, dimensions, error);
|
|
42
|
+
return printScript([getMediaCardSSR.toString(), getMediaGlobalScope.toString(), functionCall]);
|
|
43
|
+
};
|
|
44
|
+
export const generateScriptProps = (identifier, dataURI, dimensions, error) => ({
|
|
45
|
+
dangerouslySetInnerHTML: {
|
|
46
|
+
__html: generateScript(identifier, dataURI, dimensions, error)
|
|
47
|
+
}
|
|
48
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const printParam = param => {
|
|
2
|
+
if (typeof param === 'string') {
|
|
3
|
+
return `'${param}'`;
|
|
4
|
+
} else if (typeof param === 'object') {
|
|
5
|
+
return JSON.stringify(param);
|
|
6
|
+
} else if (param === undefined) {
|
|
7
|
+
return 'undefined';
|
|
8
|
+
}
|
|
9
|
+
return param;
|
|
10
|
+
};
|
|
11
|
+
const printParams = args => args.map(arg => printParam(arg)).join(',');
|
|
12
|
+
export const printFunctionCall = (fn, ...args) => `(${fn.toString()})(${printParams(args)});`;
|
|
13
|
+
export const printScript = statements => `(function(){
|
|
14
|
+
${statements.join(';')}
|
|
15
|
+
})();
|
|
16
|
+
`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useRef } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Checks if at least one of next dimensions is bigger than current
|
|
4
|
+
* If a single dimension is undefined, returns false
|
|
5
|
+
*/
|
|
6
|
+
export const isBigger = (current, next) => {
|
|
7
|
+
const {
|
|
8
|
+
width: currentWidth,
|
|
9
|
+
height: currentHeight
|
|
10
|
+
} = current || {};
|
|
11
|
+
const {
|
|
12
|
+
width: nextWidth,
|
|
13
|
+
height: nextHeight
|
|
14
|
+
} = next || {};
|
|
15
|
+
if (!!currentWidth && !!currentHeight && !!nextWidth && !!nextHeight) {
|
|
16
|
+
const nextIsWider = currentWidth < nextWidth;
|
|
17
|
+
const nextIsHigher = currentHeight < nextHeight;
|
|
18
|
+
return nextIsHigher || nextIsWider;
|
|
19
|
+
} else {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Verifies if the current screen is retina display */
|
|
25
|
+
function isRetina() {
|
|
26
|
+
const mediaQuery = '(-webkit-min-device-pixel-ratio: 1.5), (min--moz-device-pixel-ratio: 1.5), (-o-min-device-pixel-ratio: 3/2), (min-resolution: 1.5dppx)';
|
|
27
|
+
return window.devicePixelRatio > 1 || window.matchMedia && window.matchMedia(mediaQuery).matches;
|
|
28
|
+
}
|
|
29
|
+
export const createRequestDimensions = dimensions => {
|
|
30
|
+
if (!dimensions) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const retinaFactor = isRetina() ? 2 : 1;
|
|
34
|
+
const {
|
|
35
|
+
width,
|
|
36
|
+
height
|
|
37
|
+
} = dimensions;
|
|
38
|
+
const result = {};
|
|
39
|
+
if (width) {
|
|
40
|
+
result.width = width * retinaFactor;
|
|
41
|
+
}
|
|
42
|
+
if (height) {
|
|
43
|
+
result.height = height * retinaFactor;
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** Stores the provided value in a ref object to avoid "component rerenders" when the value is used as a hook dependency */
|
|
49
|
+
export function useCurrentValueRef(value) {
|
|
50
|
+
const ref = useRef(value);
|
|
51
|
+
ref.current = value;
|
|
52
|
+
return ref;
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useFilePreview } from './useFilePreview';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { imageResizeModeToFileImageMode, isImageRepresentationReady } from '@atlaskit/media-client';
|
|
3
|
+
import { MediaFileStateError, useFileState, useMediaClient } from '@atlaskit/media-client-react';
|
|
4
|
+
import { isMimeTypeSupportedByBrowser } from '@atlaskit/media-common';
|
|
5
|
+
import { extractErrorInfo } from './analytics';
|
|
6
|
+
import { ensureMediaFilePreviewError, ImageLoadError, MediaFilePreviewError } from './errors';
|
|
7
|
+
import { getAndCacheLocalPreview, getAndCacheRemotePreview, getSSRPreview, isLocalPreview, isSSRClientPreview, isSSRDataPreview, isSupportedLocalPreview, mediaFilePreviewCache } from './getPreview';
|
|
8
|
+
import { generateScriptProps, getSSRData } from './globalScope';
|
|
9
|
+
import { createRequestDimensions, isBigger, useCurrentValueRef } from './helpers';
|
|
10
|
+
export const useFilePreview = ({
|
|
11
|
+
resizeMode = 'crop',
|
|
12
|
+
identifier,
|
|
13
|
+
ssr,
|
|
14
|
+
dimensions,
|
|
15
|
+
traceContext,
|
|
16
|
+
previewDidRender,
|
|
17
|
+
skipRemote,
|
|
18
|
+
mediaBlobUrlAttrs
|
|
19
|
+
}) => {
|
|
20
|
+
const mediaClient = useMediaClient();
|
|
21
|
+
const [status, setStatus] = useState('loading');
|
|
22
|
+
const [error, setError] = useState();
|
|
23
|
+
const [nonCriticalError, setNonCriticalError] = useState();
|
|
24
|
+
const [isBannedLocalPreview, setIsBannedLocalPreview] = useState(false);
|
|
25
|
+
const wasResolvedUpfrontPreviewRef = useRef(false);
|
|
26
|
+
const ssrReliabilityRef = useRef(initialSsrReliability);
|
|
27
|
+
const requestDimensions = useMemo(() => dimensions ? createRequestDimensions(dimensions) : undefined, [dimensions]);
|
|
28
|
+
const requestDimensionsRef = useCurrentValueRef(requestDimensions);
|
|
29
|
+
const imageURLParams = useMemo(() => ({
|
|
30
|
+
collection: identifier.collectionName,
|
|
31
|
+
mode: resizeMode === 'stretchy-fit' ? 'full-fit' : resizeMode,
|
|
32
|
+
...requestDimensions,
|
|
33
|
+
allowAnimated: true
|
|
34
|
+
}), [requestDimensions, identifier.collectionName, resizeMode]);
|
|
35
|
+
const previewInitializer = () => {
|
|
36
|
+
const fileImageMode = imageResizeModeToFileImageMode(resizeMode);
|
|
37
|
+
const preview = mediaFilePreviewCache.get(identifier.id, fileImageMode);
|
|
38
|
+
if (preview) {
|
|
39
|
+
return preview;
|
|
40
|
+
}
|
|
41
|
+
if (ssr) {
|
|
42
|
+
const ssrData = getSSRData(identifier);
|
|
43
|
+
if (ssrData !== null && ssrData !== void 0 && ssrData.error) {
|
|
44
|
+
ssrReliabilityRef.current.server = {
|
|
45
|
+
status: 'fail',
|
|
46
|
+
...ssrData.error
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (!(ssrData !== null && ssrData !== void 0 && ssrData.dataURI)) {
|
|
50
|
+
try {
|
|
51
|
+
return getSSRPreview(ssr, mediaClient, identifier.id, imageURLParams, mediaBlobUrlAttrs);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
ssrReliabilityRef.current[ssr] = {
|
|
54
|
+
status: 'fail',
|
|
55
|
+
...extractErrorInfo(e)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
const {
|
|
60
|
+
dimensions,
|
|
61
|
+
dataURI
|
|
62
|
+
} = ssrData;
|
|
63
|
+
return {
|
|
64
|
+
dataURI,
|
|
65
|
+
dimensions,
|
|
66
|
+
source: 'ssr-data'
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const [preview, setPreview] = useState(previewInitializer);
|
|
72
|
+
const {
|
|
73
|
+
fileState
|
|
74
|
+
} = useFileState(identifier.id, {
|
|
75
|
+
skipRemote,
|
|
76
|
+
collectionName: identifier.collectionName,
|
|
77
|
+
occurrenceKey: identifier.occurrenceKey
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
//----------------------------------------------------------------
|
|
81
|
+
// Update status
|
|
82
|
+
//----------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
// TOOD: make a full hook reset (remount) on New identifier or client
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
setStatus('loading');
|
|
87
|
+
}, [identifier]);
|
|
88
|
+
const updateFileStateRef = useCurrentValueRef(() => {
|
|
89
|
+
if (fileState) {
|
|
90
|
+
// do not update the status if the status is final
|
|
91
|
+
if (['complete', 'error', 'failed-processing'].includes(status)) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (fileState.status !== 'error') {
|
|
95
|
+
const mediaType = 'mediaType' in fileState ? fileState.mediaType : undefined;
|
|
96
|
+
const isPreviewable = !!mediaType && ['audio', 'video', 'image', 'doc'].indexOf(mediaType) > -1;
|
|
97
|
+
const isPreviewableFileState = !!fileState.preview;
|
|
98
|
+
const isSupportedLocalPreview = mediaType === 'image' || mediaType === 'video';
|
|
99
|
+
const hasLocalPreview = !isBannedLocalPreview && isPreviewableFileState && isSupportedLocalPreview && !!fileState.mimeType && isMimeTypeSupportedByBrowser(fileState.mimeType);
|
|
100
|
+
const hasRemotePreview = isImageRepresentationReady(fileState);
|
|
101
|
+
const hasPreview = hasLocalPreview || hasRemotePreview;
|
|
102
|
+
let newStatus;
|
|
103
|
+
switch (fileState.status) {
|
|
104
|
+
case 'uploading':
|
|
105
|
+
case 'failed-processing':
|
|
106
|
+
case 'processing':
|
|
107
|
+
newStatus = fileState.status;
|
|
108
|
+
break;
|
|
109
|
+
case 'processed':
|
|
110
|
+
if (!isPreviewable || !hasPreview) {
|
|
111
|
+
newStatus = 'complete';
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
newStatus = 'loading-preview';
|
|
115
|
+
break;
|
|
116
|
+
default:
|
|
117
|
+
newStatus = 'loading';
|
|
118
|
+
}
|
|
119
|
+
setStatus(newStatus);
|
|
120
|
+
} else {
|
|
121
|
+
const e = new MediaFileStateError(fileState.id, fileState.reason, fileState.message, fileState.details);
|
|
122
|
+
const errorReason = status === 'uploading' ? 'upload' : 'metadata-fetch';
|
|
123
|
+
setError(new MediaFilePreviewError(errorReason, e));
|
|
124
|
+
setStatus('error');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
updateFileStateRef.current();
|
|
130
|
+
}, [fileState, updateFileStateRef]);
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (previewDidRender &&
|
|
133
|
+
// We should't complete if status is uploading
|
|
134
|
+
['loading-preview', 'processing'].includes(status)) {
|
|
135
|
+
setStatus('complete');
|
|
136
|
+
// TODO MEX-788: add test for "do not remove the preview when unsubscribing".
|
|
137
|
+
setIsBannedLocalPreview(false); // CXP-2723 TODO: we might be able to remove this??
|
|
138
|
+
}
|
|
139
|
+
}, [previewDidRender, status]);
|
|
140
|
+
|
|
141
|
+
// CXP-2723 TODO: Create test cases for banning local preview after status is complete
|
|
142
|
+
|
|
143
|
+
//----------------------------------------------------------------
|
|
144
|
+
// Preview Fetch Helper
|
|
145
|
+
//----------------------------------------------------------------
|
|
146
|
+
const getAndCacheRemotePreviewRef = useCurrentValueRef(() => {
|
|
147
|
+
return getAndCacheRemotePreview(mediaClient, identifier.id, requestDimensions || {}, imageURLParams, mediaBlobUrlAttrs, traceContext);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
//----------------------------------------------------------------
|
|
151
|
+
// Cache SSR Preview
|
|
152
|
+
//----------------------------------------------------------------
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
if (!skipRemote && ssr && !!preview && isSSRClientPreview(preview)) {
|
|
155
|
+
// Since the SSR preview brings the token in the query params,
|
|
156
|
+
// We need to fetch the remote preview to be able to cache it,
|
|
157
|
+
getAndCacheRemotePreviewRef.current().catch(() => {
|
|
158
|
+
// No need to log this error.
|
|
159
|
+
// If preview fails, it will be refetched later
|
|
160
|
+
//TODO: test this catch
|
|
161
|
+
// https://product-fabric.atlassian.net/browse/MEX-1071
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}, [getAndCacheRemotePreviewRef, preview, skipRemote, ssr]);
|
|
165
|
+
|
|
166
|
+
//----------------------------------------------------------------
|
|
167
|
+
// Refetch SRR Preview if dimensions from Server have changed and are bigger,
|
|
168
|
+
//----------------------------------------------------------------
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
// CXP-2813 TODO: This is called too many times if the refetch failed. Should be called only once
|
|
171
|
+
if (preview && !skipRemote && isSSRDataPreview(preview) && isBigger(preview.dimensions, requestDimensions)) {
|
|
172
|
+
getAndCacheRemotePreviewRef.current().then(setPreview).catch(e => {
|
|
173
|
+
const wrappedError = ensureMediaFilePreviewError('remote-preview-fetch-ssr', e, true);
|
|
174
|
+
setNonCriticalError(wrappedError);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}, [getAndCacheRemotePreviewRef, preview, requestDimensions, skipRemote]);
|
|
178
|
+
|
|
179
|
+
//----------------------------------------------------------------
|
|
180
|
+
// Upfront Preview
|
|
181
|
+
//----------------------------------------------------------------
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (!preview && !wasResolvedUpfrontPreviewRef.current && !skipRemote) {
|
|
184
|
+
// We block any possible future call to this method regardless of the outcome (success or fail)
|
|
185
|
+
// If it fails, the normal preview fetch should occur after the file state is fetched anyways
|
|
186
|
+
wasResolvedUpfrontPreviewRef.current = true;
|
|
187
|
+
const fetchedDimensions = {
|
|
188
|
+
...requestDimensions
|
|
189
|
+
};
|
|
190
|
+
getAndCacheRemotePreviewRef.current().then(newPreview => {
|
|
191
|
+
// If there are new and bigger dimensions in the props, and the upfront preview is still resolving,
|
|
192
|
+
// the fetched preview is no longer valid, and thus, we dismiss it
|
|
193
|
+
if (!isBigger(fetchedDimensions, requestDimensionsRef.current)) {
|
|
194
|
+
setPreview(newPreview);
|
|
195
|
+
}
|
|
196
|
+
}).catch(() => {
|
|
197
|
+
// NO need to log error. If this call fails, a refetch will happen after
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}, [getAndCacheRemotePreviewRef, preview, requestDimensions, requestDimensionsRef, skipRemote]);
|
|
201
|
+
|
|
202
|
+
//----------------------------------------------------------------
|
|
203
|
+
// Cache, Local & Remote Preview
|
|
204
|
+
//----------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
const cachedPreview = mediaFilePreviewCache.get(identifier.id, imageURLParams.mode);
|
|
208
|
+
|
|
209
|
+
// Cached Preview ----------------------------------------------------------------
|
|
210
|
+
if (!preview && cachedPreview && !isBigger(cachedPreview === null || cachedPreview === void 0 ? void 0 : cachedPreview.dimensions, requestDimensions)) {
|
|
211
|
+
setPreview(cachedPreview);
|
|
212
|
+
}
|
|
213
|
+
// Local Preview ----------------------------------------------------------------
|
|
214
|
+
else if (!preview && !isBannedLocalPreview && !!fileState && 'preview' in fileState && !!fileState.preview && isSupportedLocalPreview(fileState.mediaType) && isMimeTypeSupportedByBrowser(fileState.mimeType)) {
|
|
215
|
+
// Local preview is available only if it's supported by browser and supported by Media Card (isSupportedLocalPreview)
|
|
216
|
+
// For example, SVGs are mime type NOT supported by browser but media type supported by Media Card (image)
|
|
217
|
+
// Then, local Preview NOT available
|
|
218
|
+
|
|
219
|
+
getAndCacheLocalPreview(identifier.id, fileState.preview, requestDimensions || {}, imageURLParams.mode, mediaBlobUrlAttrs).then(setPreview).catch(e => {
|
|
220
|
+
setIsBannedLocalPreview(true);
|
|
221
|
+
// CXP-2723 TODO: We might have to wrap this error in MediaCardError
|
|
222
|
+
setNonCriticalError(e);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
// Remote Preview ----------------------------------------------------------------
|
|
226
|
+
else if ((!preview || isBigger(preview.dimensions, requestDimensions)) && !skipRemote && wasResolvedUpfrontPreviewRef.current && !!fileState && isImageRepresentationReady(fileState)) {
|
|
227
|
+
getAndCacheRemotePreviewRef.current().then(setPreview).catch(e => {
|
|
228
|
+
setStatus('error');
|
|
229
|
+
setError(ensureMediaFilePreviewError('preview-fetch', e));
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}, [fileState, getAndCacheRemotePreviewRef, identifier.id, imageURLParams.mode, isBannedLocalPreview, mediaBlobUrlAttrs, preview, requestDimensions, skipRemote]);
|
|
233
|
+
|
|
234
|
+
//----------------------------------------------------------------
|
|
235
|
+
// RETURN
|
|
236
|
+
//----------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
const onImageError = useCallback(newPreview => {
|
|
239
|
+
if (newPreview) {
|
|
240
|
+
const failedSSRObject = {
|
|
241
|
+
status: 'fail',
|
|
242
|
+
...extractErrorInfo(new ImageLoadError(newPreview.source))
|
|
243
|
+
};
|
|
244
|
+
if (isSSRClientPreview(newPreview)) {
|
|
245
|
+
ssrReliabilityRef.current.client = failedSSRObject;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/*
|
|
249
|
+
If the preview failed and it comes from server (global scope / ssrData), it means that we have reused it in client and the error counts for both: server & client.
|
|
250
|
+
*/
|
|
251
|
+
|
|
252
|
+
if (isSSRDataPreview(newPreview)) {
|
|
253
|
+
ssrReliabilityRef.current.server = failedSSRObject;
|
|
254
|
+
ssrReliabilityRef.current.client = failedSSRObject;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// If the dataURI has been replaced, we can dismiss this error
|
|
259
|
+
if ((newPreview === null || newPreview === void 0 ? void 0 : newPreview.dataURI) !== (preview === null || preview === void 0 ? void 0 : preview.dataURI)) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const error = new ImageLoadError(newPreview === null || newPreview === void 0 ? void 0 : newPreview.source);
|
|
263
|
+
const isLocal = newPreview && isLocalPreview(newPreview);
|
|
264
|
+
const isSSR = newPreview && (isSSRClientPreview(newPreview) || isSSRDataPreview(newPreview));
|
|
265
|
+
if (isLocal || isSSR) {
|
|
266
|
+
if (isLocal) {
|
|
267
|
+
setIsBannedLocalPreview(true);
|
|
268
|
+
setNonCriticalError(error);
|
|
269
|
+
}
|
|
270
|
+
const fileImageMode = imageResizeModeToFileImageMode(resizeMode);
|
|
271
|
+
mediaFilePreviewCache.remove(identifier.id, fileImageMode);
|
|
272
|
+
setPreview(undefined);
|
|
273
|
+
} else {
|
|
274
|
+
if (!['complete', 'error', 'failed-processing'].includes(status)) {
|
|
275
|
+
setStatus('error');
|
|
276
|
+
setError(error);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}, [identifier.id, preview === null || preview === void 0 ? void 0 : preview.dataURI, resizeMode, status]);
|
|
280
|
+
const onImageLoad = useCallback(newPreview => {
|
|
281
|
+
if (newPreview) {
|
|
282
|
+
if (isSSRClientPreview(newPreview) && ssrReliabilityRef.current.client.status === 'unknown') {
|
|
283
|
+
ssrReliabilityRef.current.client = {
|
|
284
|
+
status: 'success'
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/*
|
|
289
|
+
If the image loads successfully and it comes from server (global scope / ssrData), it means that we have reused it in client and the success counts for both: server & client.
|
|
290
|
+
*/
|
|
291
|
+
|
|
292
|
+
if (isSSRDataPreview(newPreview) && ssrReliabilityRef.current.server.status === 'unknown') {
|
|
293
|
+
ssrReliabilityRef.current.server = {
|
|
294
|
+
status: 'success'
|
|
295
|
+
};
|
|
296
|
+
ssrReliabilityRef.current.client = {
|
|
297
|
+
status: 'success'
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// If the dataURI has been replaced, we can dismiss this callback
|
|
303
|
+
if ((newPreview === null || newPreview === void 0 ? void 0 : newPreview.dataURI) !== (preview === null || preview === void 0 ? void 0 : preview.dataURI)) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
}, [preview === null || preview === void 0 ? void 0 : preview.dataURI]);
|
|
307
|
+
|
|
308
|
+
// FOR SSR
|
|
309
|
+
const getScriptProps = () => {
|
|
310
|
+
var _ssrReliabilityRef$cu;
|
|
311
|
+
return generateScriptProps(identifier, preview === null || preview === void 0 ? void 0 : preview.dataURI, requestDimensions, ((_ssrReliabilityRef$cu = ssrReliabilityRef.current.server) === null || _ssrReliabilityRef$cu === void 0 ? void 0 : _ssrReliabilityRef$cu.status) === 'fail' ? ssrReliabilityRef.current.server : undefined);
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// CXP-2723 TODO: should consider simplifying our analytics, and how
|
|
315
|
+
// we might get rid of ssrReliabiltyRef from our hook
|
|
316
|
+
return {
|
|
317
|
+
preview,
|
|
318
|
+
error,
|
|
319
|
+
nonCriticalError,
|
|
320
|
+
ssrReliabilityRef,
|
|
321
|
+
onImageError,
|
|
322
|
+
onImageLoad,
|
|
323
|
+
getScriptProps
|
|
324
|
+
};
|
|
325
|
+
};
|
|
326
|
+
const initialSsrReliability = {
|
|
327
|
+
server: {
|
|
328
|
+
status: 'unknown'
|
|
329
|
+
},
|
|
330
|
+
client: {
|
|
331
|
+
status: 'unknown'
|
|
332
|
+
}
|
|
333
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { getMediaClientErrorReason, isRequestError } from '@atlaskit/media-client';
|
|
2
|
+
import { getFileStateErrorReason, isMediaFilePreviewError, isMediaFileStateError } from './errors';
|
|
3
|
+
export var getErrorTraceContext = function getErrorTraceContext(error) {
|
|
4
|
+
if (isMediaFilePreviewError(error) && !!error.secondaryError) {
|
|
5
|
+
if (isRequestError(error.secondaryError)) {
|
|
6
|
+
var _error$secondaryError;
|
|
7
|
+
return (_error$secondaryError = error.secondaryError.metadata) === null || _error$secondaryError === void 0 ? void 0 : _error$secondaryError.traceContext;
|
|
8
|
+
} else if (isMediaFileStateError(error.secondaryError)) {
|
|
9
|
+
var _error$secondaryError2;
|
|
10
|
+
return (_error$secondaryError2 = error.secondaryError.details) === null || _error$secondaryError2 === void 0 || (_error$secondaryError2 = _error$secondaryError2.metadata) === null || _error$secondaryError2 === void 0 ? void 0 : _error$secondaryError2.traceContext;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
export var getRenderErrorFailReason = function getRenderErrorFailReason(error) {
|
|
15
|
+
if (isMediaFilePreviewError(error)) {
|
|
16
|
+
return error.primaryReason;
|
|
17
|
+
} else {
|
|
18
|
+
return 'nativeError';
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
export var getRenderErrorErrorReason = function getRenderErrorErrorReason(error) {
|
|
22
|
+
if (isMediaFilePreviewError(error) && error.secondaryError) {
|
|
23
|
+
var mediaClientReason = isMediaFileStateError(error.secondaryError) ? getFileStateErrorReason(error.secondaryError) : getMediaClientErrorReason(error.secondaryError);
|
|
24
|
+
if (mediaClientReason !== 'unknown') {
|
|
25
|
+
return mediaClientReason;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return 'nativeError';
|
|
29
|
+
};
|
|
30
|
+
export var getRenderErrorErrorDetail = function getRenderErrorErrorDetail(error) {
|
|
31
|
+
if (isMediaFilePreviewError(error) && error.secondaryError) {
|
|
32
|
+
return error.secondaryError.message;
|
|
33
|
+
} else {
|
|
34
|
+
return error.message;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
export var extractErrorInfo = function extractErrorInfo(error, metadataTraceContext) {
|
|
38
|
+
return {
|
|
39
|
+
failReason: getRenderErrorFailReason(error),
|
|
40
|
+
error: getRenderErrorErrorReason(error),
|
|
41
|
+
errorDetail: getRenderErrorErrorDetail(error),
|
|
42
|
+
metadataTraceContext: metadataTraceContext !== null && metadataTraceContext !== void 0 ? metadataTraceContext : getErrorTraceContext(error)
|
|
43
|
+
};
|
|
44
|
+
};
|