@@ -261,76 +258,9 @@ function SourceBarHeader({
);
}
-function ConnectionState({
- state,
- directAccess,
-}: {
- readonly state: ReaderConnectionState;
- readonly directAccess: ReaderDirectAccessState;
-}): JSX.Element | null {
- const { t } = useLingui();
- const { snapshot, working, problem, request } = directAccess;
- const descriptionId = useId();
+function ConnectionState({ state }: { readonly state: ReaderConnectionState }): JSX.Element | null {
useMdbaseMarkBusy(state === "syncing" && "stream");
- if (state === null) {
- return null;
- }
- if (snapshot?.authority !== "connector" || state !== "connected") {
- return
;
- }
- if (snapshot.route === "direct") {
- return (
-
-
- Direct connection
-
-
- );
- }
- if (working || snapshot.status === "checking") {
- return (
-
- Connecting…
-
- );
- }
- if (snapshot.status === "disabled") {
- return
;
- }
- const denied = snapshot.status === "denied";
- const title =
- problem ??
- (denied
- ? translate({
- message:
- "Local network access is blocked. Allow it in your browser’s site settings, then retry.",
- })
- : translate({
- message:
- "Request local network access and connect directly to the mdbase relay on this computer.",
- }));
- // The visible words name the button; the explanation is its description.
- return (
- <>
-
-
- {title}
-
- >
- );
+ return state === null ? null :
;
}
/** saved_on_device: the SDK keeps changes on this device until it can sync them. */
diff --git a/apps/reader/src/ReaderSaveStatus.test.tsx b/apps/reader/src/ReaderSaveStatus.test.tsx
index 2d9f4ca4..c392b21e 100644
--- a/apps/reader/src/ReaderSaveStatus.test.tsx
+++ b/apps/reader/src/ReaderSaveStatus.test.tsx
@@ -17,30 +17,35 @@ afterEach(() => {
vi.useRealTimers();
});
-it("offers a reload for old pending writes even while syncing", async () => {
+it("offers a reload only when the SDK marks pending writes as long", () => {
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
- vi.useFakeTimers();
const host = document.createElement("div");
document.body.append(host);
- const mdbase = {
- subscribe: () => () => undefined,
- getSnapshot: () => snapshot,
- } as unknown as Mdbase;
- const snapshot = {
- phase: { kind: "starting" },
+ let changed: () => void = () => undefined;
+ let snapshot = {
sync: {
+ state: "saving",
+ pendingFor: "short",
writes: {
pending: [{ mutationId: "stalled", records: [], createdAt: Date.now() }],
rejected: [],
},
},
};
+ const mdbase = {
+ subscribe: (listener: () => undefined) => {
+ changed = listener;
+ return () => undefined;
+ },
+ getSnapshot: () => snapshot,
+ } as unknown as Mdbase;
root = createRoot(host);
act(() => root.render(
));
expect(host.querySelector(".reader-save-details > button")).toBeNull();
- await act(() => vi.advanceTimersByTime(29_999));
- expect(host.querySelector(".reader-save-details > button")).toBeNull();
- await act(() => vi.advanceTimersByTime(2));
+ act(() => {
+ snapshot = { ...snapshot, sync: { ...snapshot.sync, pendingFor: "long" } };
+ changed();
+ });
expect(host.querySelector(".reader-save-details > button")?.textContent).toContain(
"Reload Reader",
);
@@ -101,15 +106,17 @@ it("shows the actual pending change and drops the warning only when the ledger d
expect(host.querySelector("details")).toBeNull();
});
-it("stays calm offline and starts the stalled timer only on reconnect", async () => {
+it("keeps offline writes calm and follows the SDK's pending age after reconnect", () => {
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
- vi.useFakeTimers();
const host = document.createElement("div");
document.body.append(host);
- const snapshot = {
- phase: { kind: "starting" },
+ let changed: () => void = () => undefined;
+ let snapshot = {
saveWarning: "storage_may_be_evicted",
sync: {
+ connection: "offline",
+ state: "saved_on_device",
+ pendingFor: "long",
writes: {
pending: [{ mutationId: "offline", records: [], createdAt: Date.now() - 60_000 }],
rejected: [],
@@ -117,82 +124,34 @@ it("stays calm offline and starts the stalled timer only on reconnect", async ()
},
};
const mdbase = {
- subscribe: () => () => undefined,
+ subscribe: (listener: () => undefined) => {
+ changed = listener;
+ return () => undefined;
+ },
getSnapshot: () => snapshot,
} as unknown as Mdbase;
root = createRoot(host);
- act(() => root.render(
));
- await act(() => vi.advanceTimersByTime(60_000));
+ act(() => root.render(
));
expect(host.textContent).toContain("still pending on this device");
expect(host.textContent).not.toContain("may remove");
expect(host.querySelector(".reader-save-details > button")).toBeNull();
- act(() => root.render(
));
- await act(() => vi.advanceTimersByTime(0));
- expect(host.querySelector(".reader-save-details > button")).toBeNull();
- await act(() => vi.advanceTimersByTime(29_999));
+ act(() => {
+ snapshot = {
+ ...snapshot,
+ sync: { ...snapshot.sync, connection: "online", state: "saving", pendingFor: "short" },
+ };
+ changed();
+ });
expect(host.querySelector(".reader-save-details > button")).toBeNull();
- await act(() => vi.advanceTimersByTime(1));
+ act(() => {
+ snapshot = { ...snapshot, sync: { ...snapshot.sync, pendingFor: "long" } };
+ changed();
+ });
expect(host.querySelector(".reader-save-details > button")?.textContent).toContain(
"Reload Reader",
);
});
-it.each(["saved_on_device", "saving"])(
- "keeps SDK-offline %s writes calm while the browser is online",
- async (saveState) => {
- vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
- vi.useFakeTimers();
- expect(navigator.onLine).toBe(true);
- const host = document.createElement("div");
- document.body.append(host);
- let changed: () => void = () => undefined;
- let snapshot = {
- phase: { kind: "starting" },
- saveWarning: "storage_may_be_evicted",
- sync: {
- connection: "offline",
- writes: {
- saveState,
- pending: [{ mutationId: "offline-sdk", records: [], createdAt: Date.now() }],
- rejected: [],
- },
- },
- };
- const mdbase = {
- subscribe: (listener: () => void) => {
- changed = listener;
- return () => undefined;
- },
- getSnapshot: () => snapshot,
- } as unknown as Mdbase;
- root = createRoot(host);
- act(() => root.render(
));
- for (let reconnect = 0; reconnect < 2; reconnect += 1) {
- await act(() => vi.advanceTimersByTime(60_000));
- expect(host.textContent).toContain("still pending on this device");
- expect(host.textContent).not.toContain("may remove");
- expect(host.querySelector(".reader-save-details > button")).toBeNull();
- act(() => {
- snapshot = { ...snapshot, sync: { ...snapshot.sync, connection: "online" } };
- changed();
- });
- await act(() => vi.advanceTimersByTime(0));
- expect(host.querySelector(".reader-save-details > button")).toBeNull();
- await act(() => vi.advanceTimersByTime(29_999));
- expect(host.querySelector(".reader-save-details > button")).toBeNull();
- await act(() => vi.advanceTimersByTime(1));
- expect(host.querySelector(".reader-save-details > button")?.textContent).toContain(
- "Reload Reader",
- );
- act(() => {
- snapshot = { ...snapshot, sync: { ...snapshot.sync, connection: "offline" } };
- changed();
- });
- expect(host.querySelector(".reader-save-details > button")).toBeNull();
- }
- },
-);
-
it.each([
[0, 1, "The collection refused these changes."],
[1, 1, "The collection refused some changes. Other changes are still pending on this device."],
diff --git a/apps/reader/src/ReaderSaveStatus.tsx b/apps/reader/src/ReaderSaveStatus.tsx
index cf3c35b9..d192d1c4 100644
--- a/apps/reader/src/ReaderSaveStatus.tsx
+++ b/apps/reader/src/ReaderSaveStatus.tsx
@@ -4,7 +4,6 @@ import { useCallback, useContext, useSyncExternalStore, type JSX } from "react";
import { Menu } from "./Menu.js";
import { sdkOffline, sdkSyncIncidents } from "./sdk-connection-state.js";
import { SourceLibraryContext } from "./SourceLibraryContext.js";
-import { useSustained } from "./use-sustained.js";
import type { Mdbase, MdbaseSnapshot } from "@mdbase-reader/connect/native";
@@ -12,7 +11,7 @@ const noop = (): void => undefined;
export function ReaderSaveStatus({
mdbase,
- online: browserOnline,
+ online: _browserOnline,
}: {
readonly mdbase: Mdbase | null;
readonly online: boolean;
@@ -27,11 +26,11 @@ export function ReaderSaveStatus({
() => mdbase?.getSnapshot() ?? null,
() => null,
);
- const online = browserOnline && !sdkOffline(snapshot);
+ const online = !sdkOffline(snapshot);
const { pending, rejected } = snapshot?.sync?.writes ?? { pending: [], rejected: [] };
const incidents = sdkSyncIncidents(snapshot);
const needsAttention = rejected.length + incidents.length > 0;
- const stalled = useSustained(online && Boolean(pending.length), 30_000);
+ const stalled = snapshot?.sync?.pendingFor === "long";
if (!pending.length && !needsAttention) {
return null;
}
diff --git a/apps/reader/src/ReaderWorkspace.tsx b/apps/reader/src/ReaderWorkspace.tsx
index c0176647..dab29e3e 100644
--- a/apps/reader/src/ReaderWorkspace.tsx
+++ b/apps/reader/src/ReaderWorkspace.tsx
@@ -49,7 +49,6 @@ export function ReaderWorkspace({
gateway={gateway}
initialSourceId={requestedSourceId(location.href, opened.collectionId)}
initialAnnotation={requestedAnnotation(location.href, opened.collectionId)}
- {...(opened.directAccess ? { directAccess: opened.directAccess } : {})}
saveFile={(name, blob) => readerPlatform.saveFile(name, blob)}
pickSourceFile={() =>
readerPlatform.pickFile([
diff --git a/apps/reader/src/ReaderWorkspaceView.tsx b/apps/reader/src/ReaderWorkspaceView.tsx
index 8c779fd8..78c205e3 100644
--- a/apps/reader/src/ReaderWorkspaceView.tsx
+++ b/apps/reader/src/ReaderWorkspaceView.tsx
@@ -48,7 +48,6 @@ import type {
} from "./source-workspace-layout.js";
import type { AnnotationComposerController } from "./use-annotation-composer.js";
import type { BibliographyExportController } from "./use-bibliography-export.js";
-import type { ReaderDirectAccessState } from "./use-direct-access.js";
import type { DocumentAttachmentController } from "./use-document-attachment.js";
import type { MdbaseLibraryViewsController } from "./use-mdbase-library-views.js";
import type { ReaderWorkspaceController } from "./use-reader-workspace.js";
@@ -85,7 +84,6 @@ export interface ReaderWorkspaceViewModel {
readonly onSurfaceChange: (sessionId: string, surface: ReadingSurface | null) => void;
readonly surfaces: ReadonlyMap
;
readonly deploymentUpdateAvailable: boolean;
- readonly directAccess: ReaderDirectAccessState;
readonly theme: ThemePreference;
readonly changeTheme: (theme: ThemePreference) => void;
readonly focusMode: boolean;
@@ -256,7 +254,6 @@ export function ReaderWorkspaceView({
collectionName={library.collectionName}
beforeCollectionSwitch={beforeCollectionSwitch}
connectionState={connectionState}
- directAccess={model.directAccess}
theme={model.theme}
onChangeTheme={model.changeTheme}
onOpenCommands={() => model.setCommandsOpen(true)}
diff --git a/apps/reader/src/SourceNoteEditor.tsx b/apps/reader/src/SourceNoteEditor.tsx
index aab8beb8..4d625c44 100644
--- a/apps/reader/src/SourceNoteEditor.tsx
+++ b/apps/reader/src/SourceNoteEditor.tsx
@@ -8,6 +8,7 @@ import { lazy, Suspense, useContext, useMemo, useState, type JSX, type ReactNode
import { annotationWikiCandidate, annotationWikiPath } from "./annotation-wiki-candidates.js";
import { DraftRecoveryNotice } from "./DraftRecoveryNotice.js";
import { CodeIcon, LinkIcon, ListIcon, QuoteIcon } from "./icons.js";
+import { useReaderMdbase } from "./reader-collections.js";
import {
sourceNoteCitationCandidates,
sourceNoteWikiCandidates,
@@ -46,6 +47,7 @@ export function SourceNoteEditor({
onOpenSourceView,
}: SourceNoteEditorProps): JSX.Element {
useLocale();
+ const mdbase = useReaderMdbase();
const [insertion, setInsertion] = useState(null);
const [formatting, setFormatting] = useState(null);
const librarySources = useContext(SourceLibraryContext);
@@ -83,7 +85,7 @@ export function SourceNoteEditor({
);
}
- if (!workspace.draftReady) {
+ if (!workspace.draftReady && mdbase) {
return (