Skip to content
Open
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
98 changes: 89 additions & 9 deletions src/elements/content-preview/ContentPreview.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ type Props = {
isLarge: boolean,
isVeryLarge?: boolean,
language: string,
loadingIndicatorDelayMs?: number,
logoUrl?: string,
measureRef: Function,
messages?: StringMap,
Expand Down Expand Up @@ -148,6 +149,7 @@ type State = {
error?: ErrorType,
file?: BoxItem,
isLoading: boolean,
isDeferringLoading?: boolean,
isReloadNotificationVisible: boolean,
isThumbnailSidebarOpen: boolean,
prevFileIdProp?: string, // the previous value of the "fileId" prop. Needed to implement getDerivedStateFromProps
Expand Down Expand Up @@ -238,6 +240,7 @@ class ContentPreview extends React.PureComponent<Props, State> {
canPrint: false,
error: undefined,
isLoading: true,
isDeferringLoading: false,
isReloadNotificationVisible: false,
isThumbnailSidebarOpen: false,
};
Expand All @@ -255,6 +258,7 @@ class ContentPreview extends React.PureComponent<Props, State> {
enableThumbnailsSidebar: false,
hasHeader: false,
language: DEFAULT_LOCALE,
loadingIndicatorDelayMs: 0,
onAnnotator: noop,
onAnnotatorEvent: noop,
onContentInsightsEventReport: noop,
Expand All @@ -281,6 +285,10 @@ class ContentPreview extends React.PureComponent<Props, State> {
*/
fetchFileStartTime: ?number;

loadingIndicatorShownThisSession: boolean;

loadingIndicatorDelayTimeoutId: TimeoutID | void;

/**
* [constructor]
*
Expand All @@ -301,6 +309,7 @@ class ContentPreview extends React.PureComponent<Props, State> {
} = props;

this.id = uniqueid('bcpr_');
this.loadingIndicatorShownThisSession = false;
this.api = new API({
apiHost,
cache,
Expand All @@ -313,8 +322,11 @@ class ContentPreview extends React.PureComponent<Props, State> {
token,
version: CLIENT_VERSION,
});
const delayMs = this.getLoadingIndicatorDelayMs();
// When delay is used, start in defer state so the spinner is not shown until after delayMs
this.state = {
...this.initialState,
...(delayMs > 0 ? { isLoading: false, isDeferringLoading: true } : {}),
currentFileId: fileId,
// eslint-disable-next-line react/no-unused-state
prevFileIdProp: fileId,
Expand All @@ -331,15 +343,21 @@ class ContentPreview extends React.PureComponent<Props, State> {
* @return {void}
*/
componentWillUnmount(): void {
this.clearLoadingIndicatorDelayTimeout();
// Don't destroy the cache while unmounting
this.api.destroy(false);
this.destroyPreview();
}

/**
* Cleans up the preview instance
* Cleans up the preview instance.
* When shouldReset is true (e.g. file changed), clears the loading-indicator delay timeout.
* When false (e.g. loading preview for same file), leaves the timeout so one defer per session.
*/
destroyPreview(shouldReset: boolean = true) {
if (shouldReset) {
this.clearLoadingIndicatorDelayTimeout();
}
const { onPreviewDestroy } = this.props;
if (this.preview) {
this.preview.destroy();
Expand All @@ -350,6 +368,27 @@ class ContentPreview extends React.PureComponent<Props, State> {
}
}

clearLoadingIndicatorDelayTimeout(): void {
if (this.loadingIndicatorDelayTimeoutId) {
clearTimeout(this.loadingIndicatorDelayTimeoutId);
this.loadingIndicatorDelayTimeoutId = undefined;
}
}

getLoadingIndicatorDelayMs(): number {
return Math.max(0, Number(this.props.loadingIndicatorDelayMs) || 0);
}

/**
* Ends the current loading session: clear defer timer, reset session flag, hide loading state.
* Call when preview has loaded, errored, or file fetch failed.
*/
endLoadingSession(): void {
this.clearLoadingIndicatorDelayTimeout();
this.loadingIndicatorShownThisSession = false;
this.setState({ isLoading: false, isDeferringLoading: false });
}

/**
* Destroys api instances with caches
*
Expand All @@ -362,14 +401,30 @@ class ContentPreview extends React.PureComponent<Props, State> {

/**
* Once the component mounts, load Preview assets and fetch file info.
* When loadingIndicatorDelayMs > 0, defer showing the loading spinner.
*
* @return {void}
*/
componentDidMount(): void {
this.loadStylesheet();
this.loadScript();

this.fetchFile(this.state.currentFileId);
const { currentFileId } = this.state;
if (!currentFileId) {
this.focusPreview();
return;
}

const delayMs = this.getLoadingIndicatorDelayMs();
if (delayMs > 0) {
this.loadingIndicatorDelayTimeoutId = setTimeout(() => {
this.loadingIndicatorDelayTimeoutId = undefined;
this.loadingIndicatorShownThisSession = true;
this.setState({ isLoading: true, isDeferringLoading: false });
}, delayMs);
}

this.fetchFile(currentFileId);
this.focusPreview();
}

Expand Down Expand Up @@ -402,14 +457,26 @@ class ContentPreview extends React.PureComponent<Props, State> {
features?.advancedContentInsights,
);
const haveExperiencesChanged = prevPreviewExperiences !== previewExperiences;
const delayMs = this.getLoadingIndicatorDelayMs();

if (hasFileIdChanged) {
this.destroyPreview();
this.setState({ isLoading: true, selectedVersion: undefined });
this.loadingIndicatorShownThisSession = false;
if (!currentFileId) {
this.endLoadingSession();
} else if (delayMs > 0) {
this.setState({ isLoading: false, isDeferringLoading: true, selectedVersion: undefined });
this.loadingIndicatorDelayTimeoutId = setTimeout(() => {
this.loadingIndicatorDelayTimeoutId = undefined;
this.loadingIndicatorShownThisSession = true;
this.setState({ isLoading: true, isDeferringLoading: false });
}, delayMs);
} else {
this.setState({ isLoading: true, selectedVersion: undefined });
}
this.fetchFile(currentFileId);
} else if (this.shouldLoadPreview(prevState)) {
this.destroyPreview(false);
this.setState({ isLoading: true });
this.loadPreview();
} else if (hasTokenChanged) {
this.updatePreviewToken();
Expand Down Expand Up @@ -629,8 +696,8 @@ class ContentPreview extends React.PureComponent<Props, State> {
const { code = ERROR_CODE_UNKNOWN } = error;
const { onError } = this.props;

// In case of error, there should be no thumbnail sidebar to account for
this.setState({ isLoading: false, isThumbnailSidebarOpen: false });
this.endLoadingSession();
this.setState({ isThumbnailSidebarOpen: false });

onError(
error,
Expand Down Expand Up @@ -739,7 +806,7 @@ class ContentPreview extends React.PureComponent<Props, State> {

onLoad(loadData);

this.setState({ isLoading: false });
this.endLoadingSession();
this.focusPreview();

if (this.preview && filesToPrefetch.length) {
Expand Down Expand Up @@ -854,6 +921,7 @@ class ContentPreview extends React.PureComponent<Props, State> {
header: 'none',
headerElement: `#${this.id} .bcpr-PreviewHeader`,
experiences: previewExperiences,
loadingIndicatorDelayMs: this.getLoadingIndicatorDelayMs(),
showAnnotations: this.canViewAnnotations(),
showAnnotationsControls,
showDownload: this.canDownload(),
Expand Down Expand Up @@ -934,7 +1002,16 @@ class ContentPreview extends React.PureComponent<Props, State> {
// If the file is watermarked or if its a new file, then update the state
// In this case preview should reload without prompting the user
if (isWatermarked || !isExistingFile) {
this.setState({ ...this.initialState, file });
const delayMs = this.getLoadingIndicatorDelayMs();
const useDeferredLoading = delayMs > 0;
// Set isLoading to false only when we never showed the spinner this session.
// If we already showed it, leave isLoading true so the spinner stays until preview loads or errors (no flicker).
const shouldHideLoading = useDeferredLoading && !this.loadingIndicatorShownThisSession;
this.setState({
...this.initialState,
file,
...(shouldHideLoading ? { isLoading: false } : {}),
});
// $FlowFixMe file version and sha1 should exist at this point
} else if (currentFile.file_version.sha1 !== file.file_version.sha1) {
// If we are already prevewing the file that got updated then show the
Expand All @@ -959,7 +1036,8 @@ class ContentPreview extends React.PureComponent<Props, State> {
code: errorCode,
message: fileError.message,
};
this.setState({ error, file: undefined, isLoading: false });
this.endLoadingSession();
this.setState({ error, file: undefined });
onError(fileError, errorCode, {
error: fileError,
});
Expand Down Expand Up @@ -1369,6 +1447,7 @@ class ContentPreview extends React.PureComponent<Props, State> {
error,
file,
isLoading,
isDeferringLoading,
isReloadNotificationVisible,
isThumbnailSidebarOpen,
selectedVersion,
Expand Down Expand Up @@ -1439,6 +1518,7 @@ class ContentPreview extends React.PureComponent<Props, State> {
<PreviewMask
errorCode={errorCode}
extension={currentExtension}
isDeferringLoading={isDeferringLoading}
isLoading={isLoading}
/>
<PreviewNavigation
Expand Down
34 changes: 26 additions & 8 deletions src/elements/content-preview/PreviewMask.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,35 @@ import './PreviewMask.scss';
export type Props = {
errorCode?: string;
extension?: string;
isDeferringLoading?: boolean;
isLoading?: boolean;
};

export default function PreviewMask({ errorCode, extension, isLoading }: Props): React.ReactElement | null {
if (!errorCode && !isLoading) {
return null;
export default function PreviewMask({
errorCode,
extension,
isDeferringLoading = false,
isLoading,
}: Props): React.ReactElement | null {
if (errorCode) {
return (
<div className="bcpr-PreviewMask">
<PreviewError errorCode={errorCode} />
</div>
);
}

return (
<div className="bcpr-PreviewMask">
{errorCode ? <PreviewError errorCode={errorCode} /> : isLoading && <PreviewLoading extension={extension} />}
</div>
);
if (isDeferringLoading) {
return <div className="bcpr-PreviewMask" />;
}

if (isLoading) {
return (
<div className="bcpr-PreviewMask">
<PreviewLoading extension={extension} />
</div>
);
}

return null;
}
Loading