Skip to content

Commit fe68be7

Browse files
authored
Merge pull request #412 from BloomBooks/BL15692_FailBookNav
fix: Handle book navigation failure more gracefully (BL-15692)
2 parents ed09ff6 + 0eb5cb3 commit fe68be7

File tree

2 files changed

+82
-21
lines changed

2 files changed

+82
-21
lines changed

src/bloom-player-controls.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
BloomPlayerControls wraps BloomPlayerCore and adds just enough controls to preview the
33
book inside of the Bloom:Publish:Android screen.
44
*/
5-
import { BloomPlayerCore } from "./bloom-player-core";
5+
import { BloomPlayerCore, ForceShowAppBar } from "./bloom-player-core";
66
import * as ReactDOM from "react-dom";
77
import {
88
informHostOfBackAction,
@@ -214,6 +214,20 @@ export const BloomPlayerControls: React.FunctionComponent<BloomPlayerProps> = (
214214
props.initiallyShowAppBar,
215215
);
216216

217+
// If showAppBar is already true, we still may have to force a render to update the state of the controls.
218+
const [, forceRerender] = useState(0);
219+
useEffect(() => {
220+
const onForceShowAppBar = () => {
221+
setShowAppBar(true);
222+
forceRerender((n) => n + 1);
223+
};
224+
225+
ForceShowAppBar.subscribe(onForceShowAppBar);
226+
return () => {
227+
ForceShowAppBar.unsubscribe(onForceShowAppBar);
228+
};
229+
}, []);
230+
217231
const [pageNumberControlPos, setPageNumberControlPos] = useState(
218232
props.startPage ?? 0,
219233
);

src/bloom-player-core.tsx

Lines changed: 67 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ import {
2929
} from "./videoRecordingSupport";
3030
import LangData from "./langData";
3131

32+
// Used to force the app bar (part of the controls) to be displayed,
33+
// e.g. when we need to force the back button to appear when there is an error loading a book.
34+
export const ForceShowAppBar = new LiteEvent<void>();
35+
3236
// See related comments in controlBar.tsx
3337
import IconButton from "@material-ui/core/IconButton";
3438
import ArrowBack from "@material-ui/icons/ArrowBackIosRounded";
@@ -422,7 +426,12 @@ export class BloomPlayerCore extends React.Component<IProps, IPlayerState> {
422426
}
423427
// called by bloom-player-controls
424428
public HandleBackButtonClickedIfHavePlayerHistory(): boolean {
425-
const backLocation = tryPopPlayerHistory(this.bookInfo.bookInstanceId);
429+
// When a linked book fails to load, bookInfo.bookInstanceId may still be the previous book.
430+
// If our current URL is a /book/{id}/index.htm-style URL, prefer that for navigation history.
431+
const currentBookIdForHistory =
432+
this.getBookIdFromBookUrlForHistory(this.state.bookUrl) ||
433+
this.bookInfo.bookInstanceId;
434+
const backLocation = tryPopPlayerHistory(currentBookIdForHistory);
426435
if (backLocation) {
427436
this.navigate(backLocation.bookUrl, backLocation.pageId);
428437
return true;
@@ -460,6 +469,22 @@ export class BloomPlayerCore extends React.Component<IProps, IPlayerState> {
460469
}
461470
}
462471

472+
private getBookIdFromBookUrlForHistory(
473+
bookUrl: string | undefined,
474+
): string | undefined {
475+
if (!bookUrl) {
476+
return undefined;
477+
}
478+
try {
479+
const pathname = new URL(bookUrl, window.location.href).pathname;
480+
// Expected format from navigation.ts: /book/{guid}/index.htm
481+
const match = pathname.match(/^\/book\/([^/]+)\/index\.htm$/i);
482+
return match ? match[1] : undefined;
483+
} catch {
484+
return undefined;
485+
}
486+
}
487+
463488
private handleWindowFocus() {
464489
const readDuration = localStorage.getItem(kLocalStorageDurationKey);
465490
const savedBookUrl = localStorage.getItem(kLocalStorageBookUrlKey);
@@ -585,8 +610,13 @@ export class BloomPlayerCore extends React.Component<IProps, IPlayerState> {
585610
// This happens in Bloom Editor preview, where a render of some outer component does not
586611
// yet have the URL. There's nothing useful we can do.
587612
if (!this.props.url) return;
588-
if (this.state.loadFailed) {
589-
return; // otherwise we'll just be stuck in here forever trying to load
613+
// If a load failed, we normally stop trying to reload (otherwise we can loop forever).
614+
// But we still want to allow navigation (e.g. Back) to a different URL to recover.
615+
if (
616+
this.state.loadFailed &&
617+
prevState.bookUrl === this.state.bookUrl
618+
) {
619+
return;
590620
}
591621
// also one-time setup; only the first time through
592622
this.initializeMedia();
@@ -888,31 +918,48 @@ export class BloomPlayerCore extends React.Component<IProps, IPlayerState> {
888918

889919
private HandleLoadingError(axiosError: any) {
890920
const errorMessage = axiosError.message as string;
891-
// Note: intentionally no bothering to add this to the l10n load, at this time.
921+
// Note: intentionally not bothering to add this to the l10n load, at this time.
892922
let msg = `<p>There was a problem displaying this book: ${errorMessage}<p>`; // just show the raw thing
893-
// If it's a file:/// url, we're probably on an Android, and something
894-
// went wrong with unpacking it, or it is corrupt. This typically results in an error
895-
// message that is NOT a 404 but IS reported, unhelpfully, as a "Network Error" (BL-7813).
896-
// A file:/// url can't possibly be a network error, so the "part of the book is missing"
897-
// error is at least a little more helpful, maybe to the user, and certainly to a developer
898-
// trying to fix the problem, especially if the user reports the problem URL.
899-
const localFileMissing =
900-
axiosError.config &&
901-
axiosError.config.url &&
902-
axiosError.config.url.startsWith("file://");
903-
if (localFileMissing || axiosError.message.indexOf("404") >= 0) {
904-
msg = "<p>This book (or some part of it) was not found.<p>";
905-
if (axiosError.config && axiosError.config.url) {
906-
msg += `<p class='errorDetails'>${htmlEncode(
907-
axiosError.config.url,
908-
)}</p>`;
923+
924+
const isBookUrlLoadFailure =
925+
!!axiosError.config?.url &&
926+
new URL(
927+
axiosError.config.url,
928+
window.location.href,
929+
).pathname.startsWith("/book/");
930+
931+
if (isBookUrlLoadFailure) {
932+
msg = "<p>We could not find that book.</p>";
933+
} else {
934+
// If it's a file:/// url, we're probably on an Android, and something
935+
// went wrong with unpacking it, or it is corrupt. This typically results in an error
936+
// message that is NOT a 404 but IS reported, unhelpfully, as a "Network Error" (BL-7813).
937+
// A file:/// url can't possibly be a network error, so the "part of the book is missing"
938+
// error is at least a little more helpful, maybe to the user, and certainly to a developer
939+
// trying to fix the problem, especially if the user reports the problem URL.
940+
const localFileMissing =
941+
axiosError.config &&
942+
axiosError.config.url &&
943+
axiosError.config.url.startsWith("file://");
944+
if (localFileMissing || axiosError.message.indexOf("404") >= 0) {
945+
msg = "<p>This book (or some part of it) was not found.<p>";
946+
if (axiosError.config && axiosError.config.url) {
947+
msg += `<p class='errorDetails'>${htmlEncode(
948+
axiosError.config.url,
949+
)}</p>`;
950+
}
909951
}
910952
}
953+
911954
this.setState({
912955
isLoading: false,
913956
loadFailed: true,
914957
loadErrorHtml: msg,
915958
});
959+
960+
if (isBookUrlLoadFailure) {
961+
ForceShowAppBar.raise();
962+
}
916963
}
917964

918965
private finishUpCalled = false;

0 commit comments

Comments
 (0)