Skip to content
Merged
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
4 changes: 2 additions & 2 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"expo": {
"name": "OpenNotes",
"slug": "open-notes",
"version": "1.3.3",
"version": "1.3.4",
"scheme": "opennotes",
"orientation": "default",
"icon": "./assets/icon.png",
Expand All @@ -16,7 +16,7 @@
"ios": {
"bundleIdentifier": "com.builderpro.opennotes",
"icon": "./assets/icon.png",
"buildNumber": "16",
"buildNumber": "17",
"supportsTablet": true,
"usesIcloudStorage": true,
"entitlements": {
Expand Down
99 changes: 64 additions & 35 deletions app/note/[id].tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {useOverlayHistory} from '../../src/components/editor/history/useOverlayHistory';
import React, {
useCallback,
useEffect,
Expand All @@ -23,8 +24,8 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useFocusEffect, useLocalSearchParams, useRouter } from 'expo-router';
import * as Haptics from 'expo-haptics';
import {
InfiniteInkCanvas,
type InfiniteInkCanvasRef,
ContinuousDocumentCanvas as InfiniteInkCanvas,
type ContinuousDocumentCanvasRef,
type InkTextBox,
type InsertedElement,
type NotebookPage,
Expand Down Expand Up @@ -61,6 +62,7 @@ import {
} from '../../src/services/imageInsertStorage';
import { exportNotebookAsPdf } from '../../src/services/exportService';
import { recordSuccessfulNoteSave } from '../../src/services/lifecycleService';
import { duplicateAssetPosition } from '../../src/utils/continuousAssets';
import { textBoxId, insertedElementId } from '../../src/utils/id';
import { spacing } from '../../src/theme/spacing';
import { t } from '../../src/i18n';
Expand Down Expand Up @@ -135,7 +137,7 @@ export default function NoteScreen() {
const { width: viewportWidth, height: viewportHeight } = useWindowDimensions();
const { id } = useLocalSearchParams<{ id: string }>();

const canvasRef = useRef<InfiniteInkCanvasRef | null>(null);
const canvasRef = useRef<ContinuousDocumentCanvasRef | null>(null);
const pendingBodyRef = useRef<SerializedNotebookData | null>(null);
const canvasReadyRef = useRef(false);
const isMountedRef = useRef(true);
Expand All @@ -148,13 +150,20 @@ export default function NoteScreen() {

const [metadata, setMetadata] = useState<NoteMetadata | null>(null);
const [enginePages, setEnginePages] = useState<NotebookPage[]>([]);
const enginePagesRef = useRef(enginePages);
const updateEnginePages = useCallback((pages: NotebookPage[]) => {
enginePagesRef.current = pages;
setEnginePages(pages);
}, []);
const flushHistoryRef = useRef<() => Promise<void>>(async () => {});
const [overlayMap, setOverlayMap] = useState<Map<string, PageOverlayState>>(
() => new Map(),
);
const overlayMapRef = useRef(overlayMap);
useEffect(() => {
overlayMapRef.current = overlayMap;
}, [overlayMap]);
const replaceOverlayMap = useCallback((next: Map<string, PageOverlayState>) => {
overlayMapRef.current = next;
setOverlayMap(next);
}, []);

const [currentPageIndex, setCurrentPageIndex] = useState(0);
const [autosaveStatus, setAutosaveStatus] = useState<AutosaveStatus>('idle');
Expand Down Expand Up @@ -203,6 +212,7 @@ export default function NoteScreen() {

const persistMerged = useCallback(async () => {
if (!id || !canvasRef.current) return;
await flushHistoryRef.current();
const canvasData = await canvasRef.current.getNotebookData();
const overlay = overlayMapRef.current;
const mergedPages = mergeStoredPreviews(canvasData.pages).map((page) => {
Expand Down Expand Up @@ -237,6 +247,13 @@ export default function NoteScreen() {
enabled: Boolean(id) && autosaveEnabled && bodyReady,
});

const history = useOverlayHistory({fileId: id, canvasRef,
readPages: () => enginePagesRef.current.map(page => ({...page, ...overlayMapRef.current.get(page.id)})),
writePages: pages => replaceOverlayMap(new Map(pages.map(page => [page.id, overlayFromPage(page)]))),
onChange: scheduleAutosave,
});
flushHistoryRef.current = history.flushOverlayHistory;

useEffect(() => {
isMountedRef.current = true;
return () => {
Expand Down Expand Up @@ -276,23 +293,32 @@ export default function NoteScreen() {
});
}, [fingerDrawingEnabled]);

const handleCanvasLoadError = useCallback((error: unknown) => {
if (__DEV__) console.warn('[NoteScreen] native notebook load failed', error);
if (!isMountedRef.current) return;
canvasReadyRef.current = false;
pendingBodyRef.current = null;
setBodyReady(false);
setBodyLoadFailed(true);
}, []);

const applyNotebookData = useCallback(
(notebookData: SerializedNotebookData) => {
rememberPagePreviews(notebookData.pages);
setEnginePages(mergeStoredPreviews(notebookData.pages));
updateEnginePages(mergeStoredPreviews(notebookData.pages));
const initialMap = new Map<string, PageOverlayState>();
for (const page of notebookData.pages) {
initialMap.set(page.id, overlayFromPage(page));
}
setOverlayMap(initialMap);
replaceOverlayMap(initialMap);
setBodyReady(true);
if (canvasReadyRef.current) {
void canvasRef.current?.loadNotebookData(notebookData);
if (canvasReadyRef.current && canvasRef.current) {
void canvasRef.current.loadNotebookData(notebookData).catch(handleCanvasLoadError);
} else {
pendingBodyRef.current = notebookData;
}
},
[mergeStoredPreviews, rememberPagePreviews],
[mergeStoredPreviews, rememberPagePreviews, handleCanvasLoadError],
);

// Initial load
Expand All @@ -302,7 +328,7 @@ export default function NoteScreen() {
storedPreviewByPageIdRef.current = new Map();
setBodyReady(false);
setBodyLoadFailed(false);
setEnginePages([]);
updateEnginePages([]);
setCurrentPageIndex(0);
setLoading(true);
(async () => {
Expand Down Expand Up @@ -357,17 +383,18 @@ export default function NoteScreen() {
const pending = pendingBodyRef.current;
if (pending) {
pendingBodyRef.current = null;
void canvasRef.current?.loadNotebookData(pending);
void canvasRef.current?.loadNotebookData(pending).catch(handleCanvasLoadError);
}
}, []);
}, [handleCanvasLoadError]);

const handlePagesChange = useCallback((next: NotebookPage[]) => {
if (!isMountedRef.current || exitRef.current === 'navigating') return;
rememberPagePreviews(next);
const pagesWithPreviews = mergeStoredPreviews(next);
setEnginePages(pagesWithPreviews);
updateEnginePages(pagesWithPreviews);
// Make sure every page id has an overlay slot; drop overlays for removed pages.
setOverlayMap((prev) => {
{
const prev = overlayMapRef.current;
let changed = false;
const nextMap = new Map(prev);
const idsInPages = new Set(pagesWithPreviews.map((p) => p.id));
Expand All @@ -383,8 +410,8 @@ export default function NoteScreen() {
changed = true;
}
}
return changed ? nextMap : prev;
});
if (changed) replaceOverlayMap(nextMap);
}
}, [mergeStoredPreviews, rememberPagePreviews]);

const handleDrawingChange = useCallback(() => {
Expand Down Expand Up @@ -459,13 +486,13 @@ export default function NoteScreen() {
);

const handleUndo = useCallback(() => {
canvasRef.current?.undo();
history.undoHistory();
scheduleAutosave();
}, [scheduleAutosave]);
}, [scheduleAutosave, history.undoHistory]);
const handleRedo = useCallback(() => {
canvasRef.current?.redo();
history.redoHistory();
scheduleAutosave();
}, [scheduleAutosave]);
}, [scheduleAutosave, history.redoHistory]);

const handleJumpToPage = useCallback(
(pageIndex: number) => {
Expand Down Expand Up @@ -496,18 +523,13 @@ export default function NoteScreen() {
pageIndex: number,
updater: (state: PageOverlayState) => PageOverlayState,
) => {
setOverlayMap((prev) => {
const page = enginePages[pageIndex];
if (!page) return prev;
const current = prev.get(page.id) ?? emptyOverlay();
const next = updater(current);
const nextMap = new Map(prev);
nextMap.set(page.id, next);
return nextMap;
});
const pageId = enginePagesRef.current[pageIndex]?.id;
if (!pageId) return;
history.setOverlayPages(pages => pages.map(page => page.id === pageId
? {...page, ...updater(overlayFromPage(page))} : page));
scheduleAutosave();
},
[enginePages, scheduleAutosave],
[history.setOverlayPages, scheduleAutosave],
);

const applyInsertedImage = useCallback(
Expand Down Expand Up @@ -644,12 +666,15 @@ export default function NoteScreen() {

const now = new Date().toISOString();
const nextId = insertedElementId();
const maxZIndex = Math.max(0, ...state.insertedElements.map((el) => el.zIndex ?? 0));
const maxZIndex = Array.from(overlayMapRef.current.values()).reduce(
(highest, overlay) => overlay.insertedElements.reduce(
(value, asset) => Math.max(value, asset.zIndex ?? 0), highest,
), 0,
);
const duplicated: InsertedElement = {
...element,
id: nextId,
x: Math.max(0, Math.min(PAGE_WIDTH - (element.width ?? 80), element.x + 20)),
y: Math.max(0, Math.min(PAGE_HEIGHT - (element.height ?? 80), element.y + 20)),
...duplicateAssetPosition(element, pageIndex, PAGE_WIDTH, PAGE_HEIGHT, enginePages.length),
zIndex: maxZIndex + 1,
createdAt: now,
updatedAt: now,
Expand Down Expand Up @@ -696,6 +721,7 @@ export default function NoteScreen() {
setIsExporting(true);
try {
await flushNow();
await flushHistoryRef.current();
const canvasData = await canvasRef.current.getNotebookData();
const overlay = overlayMapRef.current;
const mergedPages = mergeStoredPreviews(canvasData.pages).map((page) => {
Expand Down Expand Up @@ -908,7 +934,10 @@ export default function NoteScreen() {
pdfBackgroundBaseUri={metadata?.pdfUri ?? undefined}
fingerDrawingEnabled={fingerDrawingEnabled}
onReady={handleCanvasReady}
onError={handleCanvasLoadError}
onDrawingChange={handleDrawingChange}
onExternalEdit={payload => history.handleHistoryDrawingChange({nativeEvent: {externalEdit: {payload}}})}
onHistoryAction={historyAction => history.handleHistoryDrawingChange({nativeEvent: {historyAction}})}
onCurrentPageChange={handleCurrentPageChange}
onPagesChange={handlePagesChange}
onPencilDoubleTap={handlePencilDoubleTap}
Expand Down
Loading
Loading