mobile: tab history

This commit is contained in:
Ammar Ahmed
2024-05-14 15:35:49 +05:00
committed by Abdullah Atta
parent e85f8b60b0
commit c80286b587
25 changed files with 1273 additions and 481 deletions

View File

@@ -97,11 +97,6 @@ const TabItemComponent = (props: {
} }
props.close?.(); props.close?.();
}} }}
onLongPress={() => {
useTabStore.getState().updateTab(props.tab.id, {
previewTab: false
});
}}
> >
<View <View
style={{ style={{
@@ -157,8 +152,7 @@ const TabItemComponent = (props: {
color={props.tab.pinned ? colors.primary.accent : colors.primary.icon} color={props.tab.pinned ? colors.primary.accent : colors.primary.icon}
onPress={() => { onPress={() => {
useTabStore.getState().updateTab(props.tab.id, { useTabStore.getState().updateTab(props.tab.id, {
pinned: !props.tab.pinned, pinned: !props.tab.pinned
previewTab: false
}); });
}} }}
top={0} top={0}

View File

@@ -22,24 +22,22 @@ import {
VirtualizedGrouping, VirtualizedGrouping,
createInternalLink createInternalLink
} from "@notesnook/core"; } from "@notesnook/core";
import type { LinkAttributes } from "@notesnook/editor";
import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme"; import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { TextInput, View } from "react-native"; import { TextInput, View } from "react-native";
import { FlatList } from "react-native-actions-sheet"; import { FlatList } from "react-native-actions-sheet";
import { db } from "../../../common/database"; import { db } from "../../../common/database";
import { useDBItem } from "../../../hooks/use-db-item"; import { useDBItem } from "../../../hooks/use-db-item";
import { editorController } from "../../../screens/editor/tiptap/utils";
import { presentSheet } from "../../../services/event-manager"; import { presentSheet } from "../../../services/event-manager";
import { SIZE } from "../../../utils/size"; import { SIZE } from "../../../utils/size";
import { Button } from "../../ui/button"; import { Button } from "../../ui/button";
import Input from "../../ui/input"; import Input from "../../ui/input";
import { Pressable } from "../../ui/pressable"; import { Pressable } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph"; import Paragraph from "../../ui/typography/paragraph";
import type { LinkAttributes } from "@notesnook/editor";
import {
EditorEvents,
editorController
} from "../../../screens/editor/tiptap/utils";
import { strings } from "@notesnook/intl";
const ListNoteItem = ({ const ListNoteItem = ({
id, id,
@@ -194,7 +192,7 @@ export default function LinkNote(props: {
} }
: undefined : undefined
); );
editorController.current?.postMessage(EditorEvents.resolve, { editorController.current?.postMessage(NativeEvents.resolve, {
data: { data: {
href: link, href: link,
title: selectedNote.title title: selectedNote.title

View File

@@ -221,7 +221,7 @@ const useLockedNoteHandler = () => {
biometryAvailable: !!biometry, biometryAvailable: !!biometry,
biometryEnrolled: !!fingerprint biometryEnrolled: !!fingerprint
}); });
syncTabs(); syncTabs("biometry");
})(); })();
}, [tab?.id]); }, [tab?.id]);

View File

@@ -27,10 +27,10 @@ import WebView from "react-native-webview";
import { useRef } from "react"; import { useRef } from "react";
import { EDITOR_URI } from "./source"; import { EDITOR_URI } from "./source";
import { EditorMessage } from "./tiptap/types"; import { EditorMessage } from "./tiptap/types";
import { EventTypes } from "./tiptap/editor-events"; import { EditorEvents } from "@notesnook/editor-mobile/src/utils/editor-events";
import { Attachment } from "@notesnook/editor"; import { Attachment } from "@notesnook/editor";
import downloadAttachment from "../../common/filesystem/download-attachment"; import downloadAttachment from "../../common/filesystem/download-attachment";
import { EditorEvents } from "./tiptap/utils"; import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events";
import { useThemeColors } from "@notesnook/theme"; import { useThemeColors } from "@notesnook/theme";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets"; import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { db } from "../../common/database"; import { db } from "../../common/database";
@@ -69,11 +69,11 @@ export function ReadonlyEditor(props: {
const data = event.nativeEvent.data; const data = event.nativeEvent.data;
const editorMessage = JSON.parse(data) as EditorMessage<any>; const editorMessage = JSON.parse(data) as EditorMessage<any>;
if (editorMessage.type === EventTypes.logger) { if (editorMessage.type === EditorEvents.logger) {
logger.info("[READONLY EDITOR LOG]", editorMessage.value); logger.info("[READONLY EDITOR LOG]", editorMessage.value);
} }
if (editorMessage.type === EventTypes.readonlyEditorLoaded) { if (editorMessage.type === EditorEvents.readonlyEditorLoaded) {
props.onLoad?.((content: { data: string; id: string }) => { props.onLoad?.((content: { data: string; id: string }) => {
setTimeout(() => { setTimeout(() => {
noteId.current = content.id; noteId.current = content.id;
@@ -86,7 +86,7 @@ export function ReadonlyEditor(props: {
setLoading(false); setLoading(false);
}, 300); }, 300);
}); });
} else if (editorMessage.type === EventTypes.getAttachmentData) { } else if (editorMessage.type === EditorEvents.getAttachmentData) {
const attachment = (editorMessage.value as any).attachment as Attachment; const attachment = (editorMessage.value as any).attachment as Attachment;
downloadAttachment(attachment.hash, true, { downloadAttachment(attachment.hash, true, {
@@ -104,7 +104,7 @@ export function ReadonlyEditor(props: {
); );
editorRef.current?.postMessage( editorRef.current?.postMessage(
JSON.stringify({ JSON.stringify({
type: EditorEvents.attachmentData, type: NativeEvents.attachmentData,
value: { value: {
resolverId: (editorMessage.value as any).resolverId, resolverId: (editorMessage.value as any).resolverId,
data data
@@ -115,7 +115,7 @@ export function ReadonlyEditor(props: {
.catch(() => { .catch(() => {
editorRef.current?.postMessage( editorRef.current?.postMessage(
JSON.stringify({ JSON.stringify({
type: EditorEvents.attachmentData, type: NativeEvents.attachmentData,
data: { data: {
resolverId: (editorMessage.value as any).resolverId, resolverId: (editorMessage.value as any).resolverId,
data: undefined data: undefined

View File

@@ -30,6 +30,7 @@ import { sleep } from "../../../utils/time";
import { Settings } from "./types"; import { Settings } from "./types";
import { useTabStore } from "./use-tab-store"; import { useTabStore } from "./use-tab-store";
import { getResponse, randId, textInput } from "./utils"; import { getResponse, randId, textInput } from "./utils";
import { EditorSessionItem } from "./tab-history";
type Action = { job: string; id: string }; type Action = { job: string; id: string };
@@ -167,9 +168,10 @@ if (typeof statusBar !== "undefined") {
setLoading = async (loading?: boolean, tabId?: number) => { setLoading = async (loading?: boolean, tabId?: number) => {
await this.doAsync(` await this.doAsync(`
const editorController = editorControllers[${ const editorController = editorControllers[${
tabId || useTabStore.getState().currentTab tabId === undefined ? useTabStore.getState().currentTab : tabId
}]; }];
editorController.setLoading(${loading}) editorController.setLoading(${loading})
logger("info", editorController.setLoading);
`); `);
}; };
@@ -353,7 +355,46 @@ editor && editor.commands.insertImage({
response = editorControllers[${tabId}]?.scrollIntoView("${id}") || []; response = editorControllers[${tabId}]?.scrollIntoView("${id}") || [];
`); `);
}; };
//todo add replace image function
newSession = async (sessionId: string, tabId: number, noteId: string) => {
return this.doAsync(`
globalThis.sessions.newSession("${sessionId}", ${tabId}, "${noteId}");
`);
};
getSession = async (id: string): Promise<EditorSessionItem | false> => {
return this.doAsync(`
response = globalThis.sessions.get("${id}");
`);
};
deleteSession = async (id: string) => {
return this.doAsync(`
globalThis.sessions.delete("${id}");
`);
};
deleteSessionsForTabId = async (tabId: number) => {
return this.doAsync(`
globalThis.sessions.deleteForTabId(${tabId});
`);
};
updateSession = async (
id: string,
session: {
tabId: number;
noteId: string;
scrollTop: number;
from: number;
to: number;
sessionId: string;
}
) => {
return this.doAsync(`
globalThis.sessions.updateSession("${id}", ${JSON.stringify(session)});
`);
};
} }
export default Commands; export default Commands;

View File

@@ -21,7 +21,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-var-requires */
import { ItemReference } from "@notesnook/core"; import { ItemReference } from "@notesnook/core";
import type { Attachment } from "@notesnook/editor"; import type { Attachment } from "@notesnook/editor";
import { EditorEvents } from "@notesnook/editor-mobile/src/utils/editor-events";
import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events";
import { getDefaultPresets } from "@notesnook/editor/dist/cjs/toolbar/tool-definitions"; import { getDefaultPresets } from "@notesnook/editor/dist/cjs/toolbar/tool-definitions";
import { strings } from "@notesnook/intl";
import Clipboard from "@react-native-clipboard/clipboard"; import Clipboard from "@react-native-clipboard/clipboard";
import React, { useCallback, useEffect, useRef } from "react"; import React, { useCallback, useEffect, useRef } from "react";
import { import {
@@ -72,11 +75,9 @@ import {
import { openLinkInBrowser } from "../../../utils/functions"; import { openLinkInBrowser } from "../../../utils/functions";
import { tabBarRef } from "../../../utils/global-refs"; import { tabBarRef } from "../../../utils/global-refs";
import { useDragState } from "../../settings/editor/state"; import { useDragState } from "../../settings/editor/state";
import { EventTypes } from "./editor-events";
import { EditorMessage, EditorProps, useEditorType } from "./types"; import { EditorMessage, EditorProps, useEditorType } from "./types";
import { useTabStore } from "./use-tab-store"; import { tabHistory, useTabStore } from "./use-tab-store";
import { EditorEvents, editorState, openInternalLink } from "./utils"; import { editorState, openInternalLink } from "./utils";
import { strings } from "@notesnook/intl";
const publishNote = async () => { const publishNote = async () => {
const user = useUserStore.getState().user; const user = useUserStore.getState().user;
@@ -177,7 +178,7 @@ export const useEditorEvents = (
useEffect(() => { useEffect(() => {
const handleKeyboardDidShow: KeyboardEventListener = () => { const handleKeyboardDidShow: KeyboardEventListener = () => {
editor.commands.keyboardShown(true); editor.commands.keyboardShown(true);
editor.postMessage(EditorEvents.keyboardShown, undefined); editor.postMessage(NativeEvents.keyboardShown, undefined);
}; };
const handleKeyboardDidHide: KeyboardEventListener = () => { const handleKeyboardDidHide: KeyboardEventListener = () => {
editor.commands.keyboardShown(false); editor.commands.keyboardShown(false);
@@ -352,25 +353,25 @@ export const useEditorEvents = (
const editorMessage = JSON.parse(data) as EditorMessage<any>; const editorMessage = JSON.parse(data) as EditorMessage<any>;
if (editorMessage.hasTimeout && editorMessage.resolverId) { if (editorMessage.hasTimeout && editorMessage.resolverId) {
editor.postMessage(EditorEvents.resolve, { editor.postMessage(NativeEvents.resolve, {
data: true, data: true,
resolverId: editorMessage.resolverId resolverId: editorMessage.resolverId
}); });
} }
if (editorMessage.type === EventTypes.load) { if (editorMessage.type === EditorEvents.load) {
DatabaseLogger.log("Editor is ready"); DatabaseLogger.log("Editor is ready");
editor.onLoad(); editor.onLoad();
return; return;
} }
if (editorMessage.type === EventTypes.back) { if (editorMessage.type === EditorEvents.back) {
return onBackPress(); return onBackPress();
} }
if ( if (
editorMessage.sessionId !== editor.sessionId.current && editorMessage.sessionId !== editor.sessionId.current &&
editorMessage.type !== EditorEvents.status editorMessage.type !== NativeEvents.status
) { ) {
return; return;
} }
@@ -380,8 +381,8 @@ export const useEditorEvents = (
.getNoteIdForTab(editorMessage.tabId); .getNoteIdForTab(editorMessage.tabId);
switch (editorMessage.type) { switch (editorMessage.type) {
case EventTypes.content: case EditorEvents.content:
DatabaseLogger.log("EventTypes.content"); DatabaseLogger.log("EditorEvents.content");
editor.saveContent({ editor.saveContent({
type: editorMessage.type, type: editorMessage.type,
content: editorMessage.value.html as string, content: editorMessage.value.html as string,
@@ -391,8 +392,8 @@ export const useEditorEvents = (
pendingChanges: editorMessage.value?.pendingChanges pendingChanges: editorMessage.value?.pendingChanges
}); });
break; break;
case EventTypes.title: case EditorEvents.title:
DatabaseLogger.log("EventTypes.title"); DatabaseLogger.log("EditorEvents.title");
editor.saveContent({ editor.saveContent({
type: editorMessage.type, type: editorMessage.type,
title: editorMessage.value?.title as string, title: editorMessage.value?.title as string,
@@ -402,10 +403,10 @@ export const useEditorEvents = (
pendingChanges: editorMessage.value?.pendingChanges pendingChanges: editorMessage.value?.pendingChanges
}); });
break; break;
case EventTypes.logger: case EditorEvents.logger:
logger.info("[EDITOR LOG]", editorMessage.value); logger.info("[EDITOR LOG]", editorMessage.value);
break; break;
case EventTypes.dbLogger: case EditorEvents.dbLogger:
if (editorMessage.value.error) { if (editorMessage.value.error) {
DatabaseLogger.error( DatabaseLogger.error(
editorMessage.value.error, editorMessage.value.error,
@@ -418,12 +419,12 @@ export const useEditorEvents = (
DatabaseLogger.info("[EDITOR_LOG]" + editorMessage.value.message); DatabaseLogger.info("[EDITOR_LOG]" + editorMessage.value.message);
} }
break; break;
case EventTypes.contentchange: case EditorEvents.contentchange:
editor.onContentChanged(editorMessage.noteId); editor.onContentChanged(editorMessage.noteId);
break; break;
case EventTypes.selection: case EditorEvents.selection:
break; break;
case EventTypes.reminders: case EditorEvents.reminders:
if (!noteId) { if (!noteId) {
ToastManager.show({ ToastManager.show({
heading: strings.createNoteFirst(), heading: strings.createNoteFirst(),
@@ -441,7 +442,7 @@ export const useEditorEvents = (
onAdd: () => ReminderSheet.present(undefined, note, true) onAdd: () => ReminderSheet.present(undefined, note, true)
}); });
break; break;
case EventTypes.newtag: case EditorEvents.newtag:
if (!noteId) { if (!noteId) {
ToastManager.show({ ToastManager.show({
heading: strings.createNoteFirst(), heading: strings.createNoteFirst(),
@@ -451,7 +452,7 @@ export const useEditorEvents = (
} }
ManageTagsSheet.present([noteId]); ManageTagsSheet.present([noteId]);
break; break;
case EventTypes.tag: case EditorEvents.tag:
if (editorMessage.value) { if (editorMessage.value) {
if (!noteId) return; if (!noteId) return;
const note = await db.notes.note(noteId); const note = await db.notes.note(noteId);
@@ -467,7 +468,7 @@ export const useEditorEvents = (
}); });
} }
break; break;
case EventTypes.filepicker: case EditorEvents.filepicker:
editorState().isAwaitingResult = true; editorState().isAwaitingResult = true;
const { pick } = require("./picker").default; const { pick } = require("./picker").default;
pick({ pick({
@@ -479,14 +480,14 @@ export const useEditorEvents = (
editorState().isAwaitingResult = false; editorState().isAwaitingResult = false;
}, 1000); }, 1000);
break; break;
case EventTypes.download: { case EditorEvents.download: {
const downloadAttachment = const downloadAttachment =
require("../../../common/filesystem/download-attachment").default; require("../../../common/filesystem/download-attachment").default;
downloadAttachment((editorMessage.value as Attachment)?.hash, true); downloadAttachment((editorMessage.value as Attachment)?.hash, true);
break; break;
} }
case EventTypes.getAttachmentData: { case EditorEvents.getAttachmentData: {
const attachment = (editorMessage.value as any) const attachment = (editorMessage.value as any)
?.attachment as Attachment; ?.attachment as Attachment;
@@ -506,14 +507,14 @@ export const useEditorEvents = (
!!data, !!data,
editorMessage.resolverId editorMessage.resolverId
); );
editor.postMessage(EditorEvents.resolve, { editor.postMessage(NativeEvents.resolve, {
resolverId: editorMessage.resolverId, resolverId: editorMessage.resolverId,
data data
}); });
}) })
.catch((e) => { .catch((e) => {
DatabaseLogger.error(e); DatabaseLogger.error(e);
editor.postMessage(EditorEvents.resolve, { editor.postMessage(NativeEvents.resolve, {
resolverId: editorMessage.resolverId, resolverId: editorMessage.resolverId,
data: undefined data: undefined
}); });
@@ -522,26 +523,26 @@ export const useEditorEvents = (
break; break;
} }
case EventTypes.pro: case EditorEvents.pro:
if (editor.state.current?.isFocused) { if (editor.state.current?.isFocused) {
editor.state.current.isFocused = true; editor.state.current.isFocused = true;
} }
eSendEvent(eOpenPremiumDialog); eSendEvent(eOpenPremiumDialog);
break; break;
case EventTypes.monograph: case EditorEvents.monograph:
publishNote(); publishNote();
break; break;
case EventTypes.properties: case EditorEvents.properties:
showActionsheet(); showActionsheet();
break; break;
case EventTypes.scroll: case EditorEvents.scroll:
editorState().scrollPosition = editorMessage.value; editorState().scrollPosition = editorMessage.value;
break; break;
case EventTypes.fullscreen: case EditorEvents.fullscreen:
editorState().isFullscreen = true; editorState().isFullscreen = true;
eSendEvent(eOpenFullscreenEditor); eSendEvent(eOpenFullscreenEditor);
break; break;
case EventTypes.link: case EditorEvents.link:
if (editorMessage.value.startsWith("nn://")) { if (editorMessage.value.startsWith("nn://")) {
openInternalLink(editorMessage.value); openInternalLink(editorMessage.value);
console.log( console.log(
@@ -553,7 +554,7 @@ export const useEditorEvents = (
} }
break; break;
case EventTypes.previewAttachment: { case EditorEvents.previewAttachment: {
const hash = (editorMessage.value as Attachment)?.hash; const hash = (editorMessage.value as Attachment)?.hash;
const attachment = await db.attachments?.attachment(hash); const attachment = await db.attachments?.attachment(hash);
if (!attachment) return; if (!attachment) return;
@@ -564,11 +565,11 @@ export const useEditorEvents = (
} }
break; break;
} }
case EventTypes.copyToClipboard: { case EditorEvents.copyToClipboard: {
Clipboard.setString(editorMessage.value as string); Clipboard.setString(editorMessage.value as string);
break; break;
} }
case EventTypes.tabsChanged: { case EditorEvents.tabsChanged: {
// useTabStore.setState({ // useTabStore.setState({
// tabs: (editorMessage.value as any)?.tabs, // tabs: (editorMessage.value as any)?.tabs,
// currentTab: (editorMessage.value as any)?.currentTab // currentTab: (editorMessage.value as any)?.currentTab
@@ -576,14 +577,14 @@ export const useEditorEvents = (
// //
break; break;
} }
case EventTypes.toc: case EditorEvents.toc:
TableOfContents.present(editorMessage.value); TableOfContents.present(editorMessage.value);
break; break;
case EventTypes.showTabs: { case EditorEvents.showTabs: {
EditorTabs.present(); EditorTabs.present();
break; break;
} }
case EventTypes.error: { case EditorEvents.error: {
presentSheet({ presentSheet({
component: ( component: (
<Issue <Issue
@@ -595,7 +596,7 @@ export const useEditorEvents = (
}); });
break; break;
} }
case EventTypes.tabFocused: { case EditorEvents.tabFocused: {
eSendEvent(eEditorTabFocused, editorMessage.tabId); eSendEvent(eEditorTabFocused, editorMessage.tabId);
if ( if (
@@ -630,7 +631,7 @@ export const useEditorEvents = (
break; break;
} }
case EventTypes.createInternalLink: { case EditorEvents.createInternalLink: {
LinkNote.present( LinkNote.present(
editorMessage.value.attributes, editorMessage.value.attributes,
editorMessage.resolverId as string editorMessage.resolverId as string
@@ -638,17 +639,27 @@ export const useEditorEvents = (
break; break;
} }
case EventTypes.unlock: { case EditorEvents.unlock: {
eSendEvent(eUnlockWithPassword, editorMessage.value); eSendEvent(eUnlockWithPassword, editorMessage.value);
break; break;
} }
case EventTypes.unlockWithBiometrics: { case EditorEvents.goBack: {
tabHistory.back();
break;
}
case EditorEvents.goForward: {
tabHistory.forward();
break;
}
case EditorEvents.unlockWithBiometrics: {
eSendEvent(eUnlockWithBiometrics); eSendEvent(eUnlockWithBiometrics);
break; break;
} }
case EventTypes.disableReadonlyMode: { case EditorEvents.disableReadonlyMode: {
const noteId = editorMessage.value; const noteId = editorMessage.value;
if (noteId) { if (noteId) {
await db.notes.readonly(false, noteId); await db.notes.readonly(false, noteId);

View File

@@ -33,7 +33,10 @@ import {
isTrashItem isTrashItem
} from "@notesnook/core"; } from "@notesnook/core";
import { strings } from "@notesnook/intl"; import { strings } from "@notesnook/intl";
import { EditorEvents } from "@notesnook/editor-mobile/src/utils/editor-events";
import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events";
import { useThemeEngineStore } from "@notesnook/theme"; import { useThemeEngineStore } from "@notesnook/theme";
import { Mutex } from "async-mutex";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import WebView from "react-native-webview"; import WebView from "react-native-webview";
import { DatabaseLogger, db } from "../../../common/database"; import { DatabaseLogger, db } from "../../../common/database";
@@ -63,12 +66,10 @@ import { sleep } from "../../../utils/time";
import { unlockVault } from "../../../utils/unlock-vault"; import { unlockVault } from "../../../utils/unlock-vault";
import { onNoteCreated } from "../../notes/common"; import { onNoteCreated } from "../../notes/common";
import Commands from "./commands"; import Commands from "./commands";
import { EventTypes } from "./editor-events";
import { SessionHistory } from "./session-history"; import { SessionHistory } from "./session-history";
import { EditorState, SavePayload } from "./types"; import { EditorState, SavePayload } from "./types";
import { syncTabs, useTabStore } from "./use-tab-store"; import { syncTabs, useTabStore } from "./use-tab-store";
import { import {
EditorEvents,
clearAppState, clearAppState,
defaultState, defaultState,
getAppState, getAppState,
@@ -77,6 +78,8 @@ import {
post post
} from "./utils"; } from "./utils";
const loadNoteMutex = new Mutex();
type NoteWithContent = Note & { type NoteWithContent = Note & {
content?: NoteContent<false>; content?: NoteContent<false>;
}; };
@@ -142,7 +145,7 @@ export const useEditor = (
}, [commands, insets, isDefaultEditor]); }, [commands, insets, isDefaultEditor]);
useEffect(() => { useEffect(() => {
postMessage(EditorEvents.theme, theme); postMessage(NativeEvents.theme, theme);
}, [theme, postMessage]); }, [theme, postMessage]);
useEffect(() => { useEffect(() => {
@@ -201,8 +204,9 @@ export const useEditor = (
saveCount.current = 0; saveCount.current = 0;
loadingState.current = undefined; loadingState.current = undefined;
currentLoadingNoteId.current = undefined;
lock.current = false; lock.current = false;
resetContent && postMessage(EditorEvents.title, "", tabId); resetContent && postMessage(NativeEvents.title, "", tabId);
resetContent && (await commands.clearContent(tabId)); resetContent && (await commands.clearContent(tabId));
resetContent && (await commands.clearTags(tabId)); resetContent && (await commands.clearTags(tabId));
@@ -269,13 +273,6 @@ export const useEditor = (
}; };
} }
// If note is edited, the tab becomes a persistent tab automatically.
if (useTabStore.getState().getTab(tabId)?.previewTab) {
useTabStore.getState().updateTab(tabId, {
previewTab: false
});
}
let saved = false; let saved = false;
setTimeout(() => { setTimeout(() => {
if (saved) return; if (saved) return;
@@ -325,7 +322,7 @@ export const useEditor = (
if (!noteData.title) { if (!noteData.title) {
postMessage( postMessage(
EditorEvents.title, NativeEvents.title,
currentNotes.current[id]?.title, currentNotes.current[id]?.title,
tabId tabId
); );
@@ -385,8 +382,8 @@ export const useEditor = (
id === useTabStore.getState().getCurrentNoteId() && id === useTabStore.getState().getCurrentNoteId() &&
pendingChanges pendingChanges
) { ) {
postMessage(EditorEvents.title, title || note?.title, tabId); postMessage(NativeEvents.title, title || note?.title, tabId);
postMessage(EditorEvents.html, data, tabId); postMessage(NativeEvents.html, data, tabId);
currentNotes.current[id] = note; currentNotes.current[id] = note;
} }
@@ -435,7 +432,7 @@ export const useEditor = (
); );
const loadNote = useCallback( const loadNote = useCallback(
async (event: { (event: {
item?: Note; item?: Note;
forced?: boolean; forced?: boolean;
newNote?: boolean; newNote?: boolean;
@@ -443,8 +440,8 @@ export const useEditor = (
blockId?: string; blockId?: string;
presistTab?: boolean; presistTab?: boolean;
}) => { }) => {
loadNoteMutex.runExclusive(async () => {
if (!event) return; if (!event) return;
if (event.blockId) { if (event.blockId) {
blockIdRef.current = event.blockId; blockIdRef.current = event.blockId;
} }
@@ -461,10 +458,9 @@ export const useEditor = (
state.current.ready = true; state.current.ready = true;
} }
if (event.newNote) { if (event.newNote && !currentLoadingNoteId.current) {
useTabStore.getState().focusEmptyTab();
const tabId = useTabStore.getState().currentTab; const tabId = useTabStore.getState().currentTab;
currentNotes.current && (await reset(tabId)); await reset(tabId, true, true);
setTimeout(() => { setTimeout(() => {
if (state.current?.ready && !state.current.movedAway) if (state.current?.ready && !state.current.movedAway)
commands.focus(tabId); commands.focus(tabId);
@@ -476,6 +472,7 @@ export const useEditor = (
} }
const item = event.item; const item = event.item;
currentLoadingNoteId.current = item.id;
const currentTab = useTabStore const currentTab = useTabStore
.getState() .getState()
@@ -489,57 +486,50 @@ export const useEditor = (
const tabLocked = const tabLocked =
isLockedNote && !(event.item as NoteWithContent).content; isLockedNote && !(event.item as NoteWithContent).content;
// If note was already opened in a tab, focus that tab. let tabId =
if (typeof event.tabId !== "number") { event.tabId || useTabStore.getState().getTabForNote(event.item.id);
if (useTabStore.getState().hasTabForNote(event.item.id)) { if (tabId === undefined) tabId = useTabStore.getState().currentTab;
const tabId = useTabStore.getState().getTabForNote(event.item.id);
if (typeof tabId === "number") { const isOpened =
useTabStore.getState().getTabForNote(event.item.id) === tabId;
if (!isOpened) {
await commands.setLoading(true, tabId);
}
useTabStore.getState().updateTab(tabId, { useTabStore.getState().updateTab(tabId, {
readonly: event.item.readonly || readonly, readonly: event.item.readonly || readonly,
...(isOpened
? {
noteId: event.item.id
}
: {
locked: tabLocked, locked: tabLocked,
noteLocked: isLockedNote noteLocked: isLockedNote,
noteId: event.item.id
})
}); });
useTabStore.getState().focusTab(tabId); useTabStore.getState().focusTab(tabId);
if (lastTabFocused.current !== tabId) {
useTabStore.getState().focusTab(tabId);
}
setTimeout(() => { setTimeout(() => {
if (blockIdRef.current) { if (blockIdRef.current) {
commands.scrollIntoViewById(blockIdRef.current); commands.scrollIntoViewById(blockIdRef.current);
blockIdRef.current = undefined; blockIdRef.current = undefined;
} }
}, 150); }, 150);
}
} else {
if (event.presistTab) {
// Open note in new tab.
useTabStore.getState().newTab({
readonly: event.item.readonly || readonly,
locked: tabLocked,
noteLocked: isLockedNote,
noteId: event.item.id,
previewTab: false
});
} else {
// Otherwise we focus the preview tab or create one to open the note in.
useTabStore.getState().focusPreviewTab(event.item.id, {
readonly: event.item.readonly || readonly,
locked: tabLocked,
noteLocked: isLockedNote
});
}
}
} else {
if (lastTabFocused.current !== event.tabId) {
useTabStore.getState().focusTab(event.tabId);
}
}
const tabId = event.tabId || useTabStore.getState().currentTab;
if (lastTabFocused.current !== tabId) { if (lastTabFocused.current !== tabId) {
// if ((await waitForEvent(eEditorTabFocused, 1000)) !== tabId) { // if ((await waitForEvent(eEditorTabFocused, 1000)) !== tabId) {
// // console.log("tab id did not match after focus in 1000ms");
// return; // return;
// } // }
currentLoadingNoteId.current = item.id; currentLoadingNoteId.current = item.id;
console.log("Waiting for tab to focus");
return; return;
} }
@@ -557,17 +547,17 @@ export const useEditor = (
loadingState.current === currentContents.current[item.id]?.data loadingState.current === currentContents.current[item.id]?.data
) { ) {
// If note is already loading, return. // If note is already loading, return.
console.log("Note is already loading...");
return; return;
} }
if (!state.current.ready) { if (!state.current.ready) {
currentNotes.current[item.id] = item; currentNotes.current[item.id] = item;
currentLoadingNoteId.current = event.item?.id;
return; return;
} }
lastContentChangeTime.current[item.id] = 0; lastContentChangeTime.current[item.id] = item.dateEdited;
currentLoadingNoteId.current = item.id;
currentNotes.current[item.id] = item; currentNotes.current[item.id] = item;
if (!currentNotes.current[item.id]) return; if (!currentNotes.current[item.id]) return;
@@ -576,16 +566,16 @@ export const useEditor = (
await commands.setStatus( await commands.setStatus(
getFormattedDate(item.dateEdited, "date-time"), getFormattedDate(item.dateEdited, "date-time"),
strings.saved(), "Saved",
tabId tabId
); );
await postMessage(EditorEvents.title, item.title, tabId); await postMessage(NativeEvents.title, item.title, tabId);
overlay(false); overlay(false);
loadingState.current = currentContents.current[item.id]?.data; loadingState.current = currentContents.current[item.id]?.data;
await postMessage( await postMessage(
EditorEvents.html, NativeEvents.html,
currentContents.current[item.id]?.data || "", currentContents.current[item.id]?.data || "",
tabId, tabId,
10000 10000
@@ -607,7 +597,8 @@ export const useEditor = (
} }
}, 300); }, 300);
} }
postMessage(EditorEvents.theme, theme); postMessage(NativeEvents.theme, theme);
});
}, },
[ [
commands, commands,
@@ -690,7 +681,7 @@ export const useEditor = (
} }
if (currentNotes.current[noteId]?.title !== note.title) { if (currentNotes.current[noteId]?.title !== note.title) {
postMessage(EditorEvents.title, note.title, tabId); postMessage(NativeEvents.title, note.title, tabId);
} }
commands.setTags(note); commands.setTags(note);
if (currentNotes.current[noteId]?.dateEdited !== note.dateEdited) { if (currentNotes.current[noteId]?.dateEdited !== note.dateEdited) {
@@ -724,7 +715,7 @@ export const useEditor = (
} }
} else { } else {
await postMessage( await postMessage(
EditorEvents.updatehtml, NativeEvents.updatehtml,
decryptedContent.data, decryptedContent.data,
tabId tabId
); );
@@ -736,7 +727,7 @@ export const useEditor = (
return; return;
} }
lastContentChangeTime.current[note.id] = note.dateEdited; lastContentChangeTime.current[note.id] = note.dateEdited;
await postMessage(EditorEvents.updatehtml, _nextContent, tabId); await postMessage(NativeEvents.updatehtml, _nextContent, tabId);
if (!isEncryptedContent(data)) { if (!isEncryptedContent(data)) {
currentContents.current[note.id] = currentContents.current[note.id] =
data as UnencryptedContentItem; data as UnencryptedContentItem;
@@ -810,7 +801,7 @@ export const useEditor = (
lastContentChangeTime.current[noteId] = Date.now(); lastContentChangeTime.current[noteId] = Date.now();
} }
if (type === EventTypes.content && noteId) { if (type === EditorEvents.content && noteId) {
currentContents.current[noteId as string] = { currentContents.current[noteId as string] = {
data: content, data: content,
type: "tiptap", type: "tiptap",
@@ -905,7 +896,7 @@ export const useEditor = (
const onLoad = useCallback(async () => { const onLoad = useCallback(async () => {
setTimeout(() => { setTimeout(() => {
postMessage(EditorEvents.theme, theme); postMessage(NativeEvents.theme, theme);
}); });
commands.setInsets( commands.setInsets(
isDefaultEditor ? insets : { top: 0, left: 0, right: 0, bottom: 0 } isDefaultEditor ? insets : { top: 0, left: 0, right: 0, bottom: 0 }

View File

@@ -18,8 +18,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import create from "zustand"; import create from "zustand";
import { persist, StateStorage } from "zustand/middleware"; import { persist, StateStorage } from "zustand/middleware";
import { db } from "../../../common/database";
import { MMKV } from "../../../common/database/mmkv"; import { MMKV } from "../../../common/database/mmkv";
import { eSendEvent } from "../../../services/event-manager";
import { eOnLoadNote } from "../../../utils/events";
import { editorController } from "./utils"; import { editorController } from "./utils";
import { TabHistory } from "@notesnook/common/dist/utils/tab-history";
class History { class History {
history: number[]; history: number[];
@@ -36,7 +40,7 @@ class History {
this.history.unshift(item); // Add item to the beginning of the array this.history.unshift(item); // Add item to the beginning of the array
useTabStore.setState({ useTabStore.setState({
tabHistory: this.history.slice() history: this.history.slice()
}); });
return true; // Item added successfully return true; // Item added successfully
} }
@@ -48,7 +52,7 @@ class History {
return removedItem; return removedItem;
} }
useTabStore.setState({ useTabStore.setState({
tabHistory: this.history.slice() history: this.history.slice()
}); });
return null; // Invalid index return null; // Invalid index
} }
@@ -59,7 +63,7 @@ class History {
return restoredItem; return restoredItem;
} }
useTabStore.setState({ useTabStore.setState({
tabHistory: this.history.slice() history: this.history.slice()
}); });
return null; // History is empty return null; // History is empty
} }
@@ -99,9 +103,13 @@ export type TabStore = {
focusEmptyTab: () => void; focusEmptyTab: () => void;
getCurrentNoteId: () => string | undefined; getCurrentNoteId: () => string | undefined;
getTab: (tabId: number) => TabItem | undefined; getTab: (tabId: number) => TabItem | undefined;
tabHistory: number[]; history: number[];
biometryAvailable?: boolean; biometryAvailable?: boolean;
biometryEnrolled?: boolean; biometryEnrolled?: boolean;
tabHistory: Record<number, { back_stack: string[]; forward_stack: string[] }>;
canGoBack?: boolean;
canGoForward?: boolean;
sessionId?: string;
}; };
function getId(id: number, tabs: TabItem[]): number { function getId(id: number, tabs: TabItem[]): number {
@@ -112,16 +120,73 @@ function getId(id: number, tabs: TabItem[]): number {
return id; return id;
} }
export function syncTabs() { export function syncTabs(
type: "tabs" | "history" | "biometry" | "all" = "all"
) {
const data: Partial<TabStore> = {};
if (type === "tabs" || type === "all") {
data.tabs = useTabStore.getState().tabs;
data.currentTab = useTabStore.getState().currentTab;
}
if (type === "history" || type === "all") {
data.canGoBack = useTabStore.getState().canGoBack;
data.canGoForward = useTabStore.getState().canGoForward;
data.sessionId = useTabStore.getState().sessionId;
}
if (type === "biometry" || type === "all") {
data.biometryAvailable = useTabStore.getState().biometryAvailable;
data.biometryEnrolled = useTabStore.getState().biometryEnrolled;
}
editorController.current?.commands.doAsync(` editorController.current?.commands.doAsync(`
globalThis.tabStore?.setState({ globalThis.tabStore?.setState(${JSON.stringify(data)});
tabs: ${JSON.stringify(useTabStore.getState().tabs)},
currentTab: ${useTabStore.getState().currentTab},
biometryAvailable: ${useTabStore.getState().biometryAvailable},
biometryEnrolled: ${useTabStore.getState().biometryEnrolled}
});
`); `);
} }
export const tabHistory = new TabHistory({
get() {
return useTabStore.getState();
},
set(state) {
console.log(state, "saving tab history...");
useTabStore.setState({
...state
});
},
getCurrentTab: () => useTabStore.getState().currentTab,
loadSession: async (sessionId: string) => {
const session = await editorController?.current?.commands.getSession(
sessionId
);
console.log("LOADING SESSION FOR ID", sessionId, session);
if (session && session.noteId) {
const note = await db.notes.note(session.noteId);
if (note) {
eSendEvent(eOnLoadNote, {
item: note,
tabId: useTabStore.getState().currentTab
});
return true;
}
return false;
}
return false;
},
newSession: (sessionId, tabId, noteId) => {
editorController?.current?.commands?.newSession(sessionId, tabId, noteId);
},
clearSessionsForTabId: (tabId: number) => {
editorController?.current?.commands?.deleteSessionsForTabId(tabId);
},
getSession: async (sessionId: string) => {
return (
(await editorController?.current?.commands.getSession(sessionId)) ||
undefined
);
},
commit: () => syncTabs("history")
});
export const useTabStore = create<TabStore>( export const useTabStore = create<TabStore>(
persist( persist(
@@ -131,14 +196,19 @@ export const useTabStore = create<TabStore>(
id: 0 id: 0
} }
], ],
tabHistory: [0], tabHistory: {},
history: new History(), history: [0],
currentTab: 0, currentTab: 0,
updateTab: (id: number, options: Omit<Partial<TabItem>, "id">) => { updateTab: (id: number, options: Omit<Partial<TabItem>, "id">) => {
if (!options) return; if (!options) return;
const index = get().tabs.findIndex((t) => t.id === id); const index = get().tabs.findIndex((t) => t.id === id);
if (index == -1) return; if (index == -1) return;
const tabs = [...get().tabs]; const tabs = [...get().tabs];
if (options.noteId) {
tabHistory.add(options.noteId);
}
tabs[index] = { tabs[index] = {
...tabs[index], ...tabs[index],
...options ...options
@@ -147,35 +217,14 @@ export const useTabStore = create<TabStore>(
set({ set({
tabs: tabs tabs: tabs
}); });
syncTabs(); syncTabs("tabs");
}, },
focusPreviewTab: ( focusPreviewTab: (
noteId: string, noteId: string,
options: Omit<Partial<TabItem>, "id" | "noteId"> options: Omit<Partial<TabItem>, "id" | "noteId">
) => { ) => {},
const index = get().tabs.findIndex((t) => t.previewTab);
if (index === -1)
return get().newTab({
noteId,
previewTab: true,
...options
});
const tabs = [...get().tabs];
tabs[index] = {
...tabs[index],
...options,
previewTab: true,
noteId: noteId
};
set({
tabs: tabs
});
get().focusTab(tabs[index].id);
},
removeTab: (id: number) => { removeTab: (id: number) => {
const index = get().tabs.findIndex((t) => t.id === id); const index = get().tabs.findIndex((t) => t.id === id);
if (index > -1) { if (index > -1) {
const isFocused = id === get().currentTab; const isFocused = id === get().currentTab;
const nextTabs = get().tabs.slice(); const nextTabs = get().tabs.slice();
@@ -186,6 +235,7 @@ export const useTabStore = create<TabStore>(
id: 0 id: 0
}); });
} }
tabHistory.clearStackForTab(id);
set({ set({
tabs: nextTabs tabs: nextTabs
}); });
@@ -203,6 +253,11 @@ export const useTabStore = create<TabStore>(
...options ...options
} }
]; ];
if (options?.noteId) {
tabHistory.add(options.noteId);
}
set({ set({
tabs: nextTabs tabs: nextTabs
}); });
@@ -220,7 +275,7 @@ export const useTabStore = create<TabStore>(
set({ set({
tabs: tabs tabs: tabs
}); });
syncTabs(); syncTabs("tabs");
}, },
focusTab: (id: number) => { focusTab: (id: number) => {
@@ -228,6 +283,16 @@ export const useTabStore = create<TabStore>(
set({ set({
currentTab: id currentTab: id
}); });
set({
canGoBack: tabHistory.canGoBack(),
canGoForward: tabHistory.canGoForward(),
sessionId: tabHistory.getCurrentSession()
});
console.log(
tabHistory.canGoBack(),
tabHistory.canGoForward(),
tabHistory.getCurrentSession()
);
syncTabs(); syncTabs();
}, },
getNoteIdForTab: (id: number) => { getNoteIdForTab: (id: number) => {
@@ -253,7 +318,7 @@ export const useTabStore = create<TabStore>(
getStorage: () => MMKV as unknown as StateStorage, getStorage: () => MMKV as unknown as StateStorage,
onRehydrateStorage: () => { onRehydrateStorage: () => {
return (state) => { return (state) => {
history.history = state?.tabHistory.slice() || []; history.history = state?.history || [];
}; };
} }
} }

View File

@@ -31,6 +31,8 @@ import { eOnLoadNote } from "../../../utils/events";
import { NotesnookModule } from "../../../utils/notesnook-module"; import { NotesnookModule } from "../../../utils/notesnook-module";
import { AppState, EditorState, useEditorType } from "./types"; import { AppState, EditorState, useEditorType } from "./types";
import { useTabStore } from "./use-tab-store"; import { useTabStore } from "./use-tab-store";
import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events";
export const textInput = createRef<TextInput>(); export const textInput = createRef<TextInput>();
export const editorController = export const editorController =
createRef<useEditorType>() as MutableRefObject<useEditorType>; createRef<useEditorType>() as MutableRefObject<useEditorType>;
@@ -46,19 +48,6 @@ export function editorState() {
return editorController.current?.state.current || defaultState; return editorController.current?.state.current || defaultState;
} }
export const EditorEvents = {
html: "native:html",
updatehtml: "native:updatehtml",
title: "native:title",
theme: "native:theme",
titleplaceholder: "native:titleplaceholder",
logger: "native:logger",
status: "native:status",
keyboardShown: "native:keyboardShown",
attachmentData: "native:attachment-data",
resolve: "native:resolve"
};
export function randId(prefix: string) { export function randId(prefix: string) {
return Math.random() return Math.random()
.toString(36) .toString(36)
@@ -74,7 +63,7 @@ export async function isEditorLoaded(
sessionId: string, sessionId: string,
tabId: number tabId: number
) { ) {
return await post(ref, sessionId, tabId, EditorEvents.status); return await post(ref, sessionId, tabId, NativeEvents.status);
} }
export async function post<T>( export async function post<T>(

View File

@@ -34596,6 +34596,14 @@
"version": "1.0.1", "version": "1.0.1",
"license": "MIT" "license": "MIT"
}, },
"node_modules/async-mutex": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz",
"integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==",
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/at-least-node": { "node_modules/at-least-node": {
"version": "1.0.0", "version": "1.0.0",
"dev": true, "dev": true,
@@ -43365,6 +43373,7 @@
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.2.0.tgz", "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.2.0.tgz",
"integrity": "sha512-JWD+aQ0lh2gvh4NM3bBM42Kx+XybOxCpgYK7F8ugAlpaTSnWsX+39Z4XkOykGZAHrjwwTZT3x3KxswVWxHPUqA==", "integrity": "sha512-JWD+aQ0lh2gvh4NM3bBM42Kx+XybOxCpgYK7F8ugAlpaTSnWsX+39Z4XkOykGZAHrjwwTZT3x3KxswVWxHPUqA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"react-is": "^18.2.0", "react-is": "^18.2.0",
"react-shallow-renderer": "^16.15.0", "react-shallow-renderer": "^16.15.0",

View File

@@ -0,0 +1,109 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
export type EditorSessionItem = {
tabId: number;
noteId: string;
scrollTop: number;
from: number;
to: number;
id: string;
};
export class EditorSessions extends Map<string, EditorSessionItem> {
timer: NodeJS.Timeout | null = null;
constructor(
public options: {
getGlobalNoteState: () => Record<
string,
{ top: number; from: number; to: number }
>;
}
) {
super();
const savedSessions = localStorage.getItem("editor-sessions");
if (savedSessions) {
const parsed = JSON.parse(savedSessions);
for (const [key, value] of Object.entries(parsed)) {
this.set(key, value as EditorSessionItem);
}
}
}
save() {
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(() => {
localStorage.setItem(
"editor-sessions",
JSON.stringify(Object.fromEntries(this.entries()))
);
}, 1000);
}
get(id: string): EditorSessionItem | undefined {
return super.get(id);
}
set(id: string, session: EditorSessionItem): this {
super.set(id, session);
this.save();
return this;
}
delete(key: string): boolean {
super.delete(key);
this.save();
return true;
}
newSession(
sessionId: string,
tabId: number,
noteId: string
): EditorSessionItem {
const session: EditorSessionItem = {
tabId,
noteId,
scrollTop: 0 || this.options.getGlobalNoteState()?.[noteId]?.top,
from: 0 || this.options.getGlobalNoteState()?.[noteId]?.from,
to: 0 || this.options.getGlobalNoteState()?.[noteId]?.to,
id: sessionId
};
this.set(sessionId, session);
return session;
}
updateSession(id: string, session: Partial<EditorSessionItem>): this {
const existing = this.get(id);
if (existing) {
this.set(id, { ...existing, ...session });
}
return this;
}
deleteForTabId(tabId: number) {
for (const [key, value] of this.entries()) {
if (value.tabId === tabId) {
this.delete(key);
}
}
}
}

View File

@@ -0,0 +1,184 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { EditorSessionItem } from "./editor-sessions";
export type TabState = {
tabHistory: Record<number, { back_stack: string[]; forward_stack: string[] }>;
canGoBack?: boolean;
canGoForward?: boolean;
sessionId?: string;
};
export class TabHistory {
constructor(
public options: {
set: (state: TabState) => void;
get: () => TabState;
getCurrentTab: () => number;
loadSession: (sessionId: string) => Promise<boolean>;
newSession: (sessionId: string, tabId: number, noteId: string) => void;
clearSessionsForTabId: (tabid: number) => void;
getSession: (sessionId: string) => Promise<EditorSessionItem | undefined>;
commit: () => void;
}
) {}
get back_stack() {
return (
this.options
.get()
.tabHistory[this.options.getCurrentTab()]?.back_stack.slice() || []
);
}
set back_stack(value: string[]) {
const currentTab = this.options.getCurrentTab();
const tabHistory = this.options.get().tabHistory;
this.options.set({
canGoBack: value.length > 1,
tabHistory: {
...tabHistory,
[currentTab]: {
...(tabHistory[currentTab] || {}),
back_stack: value
}
}
});
this.options.commit();
}
get forward_stack() {
return (
this.options
.get()
.tabHistory[this.options.getCurrentTab()]?.forward_stack.slice() || []
);
}
set forward_stack(value: string[]) {
const currentTab = this.options.getCurrentTab();
const tabHistory = this.options.get().tabHistory;
this.options.set({
canGoForward: value.length > 1,
tabHistory: {
...tabHistory,
[currentTab]: {
...(tabHistory[currentTab] || {}),
forward_stack: value
}
}
});
this.options.commit();
}
async add(noteId: string) {
const currentItemId = this.back_stack[this.back_stack.length - 1];
const currentSession = currentItemId
? await this.options.getSession(currentItemId)
: undefined;
if (currentSession && currentSession.noteId === noteId) return;
const newSessionId = Math.random()
.toString(36)
.replace("0.", "es-" || "");
const back_stack = this.back_stack;
back_stack.push(newSessionId);
this.options.newSession(newSessionId, this.options.getCurrentTab(), noteId);
this.back_stack = back_stack;
this.forward_stack = [];
}
clearStackForTab(tabId: number) {
this.options.set({
tabHistory: {
...this.options.get().tabHistory,
[tabId]: {
back_stack: [],
forward_stack: []
}
}
});
this.options.clearSessionsForTabId(tabId);
}
async back(): Promise<string | null> {
if (!this.canGoBack()) return null;
const back_stack = this.back_stack;
const forward_stack = this.forward_stack;
const current_item = back_stack.pop();
const next_item = back_stack[back_stack.length - 1];
if (next_item) {
current_item && forward_stack.push(current_item);
this.forward_stack = forward_stack;
this.back_stack = back_stack;
if (await this.options.loadSession(next_item)) {
return next_item;
} else if (this.back_stack.length > 1) {
return this.back();
}
}
return null;
}
async forward(): Promise<string | null> {
if (!this.canGoForward()) return null;
const back_stack = this.back_stack;
const forward_stack = this.forward_stack;
const item = forward_stack.pop();
if (item) {
this.forward_stack = forward_stack;
if (await this.options.loadSession(item)) {
back_stack.push(item);
this.back_stack = back_stack;
return item;
} else if (this.forward_stack.length > 0) {
return this.forward();
}
}
return null;
}
getHistory() {
return {
back: this.back_stack,
forward: this.forward_stack
};
}
getCurrentSession() {
return this.back_stack[this.back_stack.length - 1];
}
canGoBack() {
return this.back_stack.length > 1;
}
canGoForward() {
return this.forward_stack.length > 0;
}
}

View File

@@ -14,6 +14,7 @@
"@lingui/react": "5.1.2", "@lingui/react": "5.1.2",
"@mdi/js": "^7.2.96", "@mdi/js": "^7.2.96",
"@mdi/react": "^1.6.0", "@mdi/react": "^1.6.0",
"@notesnook/common": "file:../common",
"@notesnook/editor": "file:../editor", "@notesnook/editor": "file:../editor",
"@notesnook/intl": "file:../intl", "@notesnook/intl": "file:../intl",
"@notesnook/theme": "file:../theme", "@notesnook/theme": "file:../theme",
@@ -33,6 +34,25 @@
"react-scripts": "^5.0.1" "react-scripts": "^5.0.1"
} }
}, },
"../common": {
"version": "1.0.0",
"license": "GPL-3.0-or-later",
"dependencies": {
"@notesnook/core": "file:../core",
"pathe": "^1.1.2",
"timeago.js": "4.0.2"
},
"devDependencies": {
"@notesnook/core": "file:../core",
"@types/react": "^18.2.39",
"react": "18.2.0",
"vitest": "^1.4.0"
},
"peerDependencies": {
"react": ">=18",
"timeago.js": "4.0.2"
}
},
"../editor": { "../editor": {
"name": "@notesnook/editor", "name": "@notesnook/editor",
"version": "2.1.3", "version": "2.1.3",
@@ -3766,6 +3786,10 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@notesnook/common": {
"resolved": "../common",
"link": true
},
"node_modules/@notesnook/editor": { "node_modules/@notesnook/editor": {
"resolved": "../editor", "resolved": "../editor",
"link": true "link": true
@@ -4459,7 +4483,7 @@
"version": "15.7.11", "version": "15.7.11",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==",
"dev": true "devOptional": true
}, },
"node_modules/@types/q": { "node_modules/@types/q": {
"version": "1.5.8", "version": "1.5.8",
@@ -4483,7 +4507,7 @@
"version": "18.2.39", "version": "18.2.39",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.39.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.39.tgz",
"integrity": "sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA==", "integrity": "sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA==",
"dev": true, "devOptional": true,
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
"@types/scheduler": "*", "@types/scheduler": "*",
@@ -4518,7 +4542,7 @@
"version": "0.16.8", "version": "0.16.8",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
"dev": true "devOptional": true
}, },
"node_modules/@types/semver": { "node_modules/@types/semver": {
"version": "7.5.6", "version": "7.5.6",
@@ -9780,7 +9804,7 @@
"version": "9.0.21", "version": "9.0.21",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
"dev": true, "devOptional": true,
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
"url": "https://opencollective.com/immer" "url": "https://opencollective.com/immer"
@@ -17874,6 +17898,20 @@
"is-typedarray": "^1.0.0" "is-typedarray": "^1.0.0"
} }
}, },
"node_modules/typescript": {
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/unbox-primitive": { "node_modules/unbox-primitive": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",

View File

@@ -18,7 +18,9 @@
"react-freeze": "^1.0.3", "react-freeze": "^1.0.3",
"zustand": "^4.4.7", "zustand": "^4.4.7",
"@lingui/core": "5.1.2", "@lingui/core": "5.1.2",
"@lingui/react": "5.1.2" "@lingui/react": "5.1.2",
"tinycolor2": "1.6.0",
"@notesnook/common": "file:../common"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.37.1", "@playwright/test": "^1.37.1",

View File

@@ -36,6 +36,7 @@ import {
useState useState
} from "react"; } from "react";
import { useEditorController } from "../hooks/useEditorController"; import { useEditorController } from "../hooks/useEditorController";
import { useSafeArea } from "../hooks/useSafeArea";
import { useSettings } from "../hooks/useSettings"; import { useSettings } from "../hooks/useSettings";
import { import {
NoteState, NoteState,
@@ -44,7 +45,8 @@ import {
useTabContext, useTabContext,
useTabStore useTabStore
} from "../hooks/useTabStore"; } from "../hooks/useTabStore";
import { EventTypes, postAsyncWithTimeout, Settings } from "../utils"; import { postAsyncWithTimeout, Settings } from "../utils";
import { EditorEvents } from "../utils/editor-events";
import { pendingSaveRequests } from "../utils/pending-saves"; import { pendingSaveRequests } from "../utils/pending-saves";
import Header from "./header"; import Header from "./header";
import StatusBar from "./statusbar"; import StatusBar from "./statusbar";
@@ -82,6 +84,7 @@ const Tiptap = ({
undo, undo,
redo redo
}); });
const insets = useSafeArea();
tabRef.current = tab; tabRef.current = tab;
valueRef.current = { valueRef.current = {
undo, undo,
@@ -92,7 +95,7 @@ const Tiptap = ({
try { try {
if (!tabRef.current.noteId) return; if (!tabRef.current.noteId) return;
const noteState = const noteState =
state || useTabStore.getState().noteState[tabRef.current.noteId]; state || useTabStore.getState().getNoteState(tabRef.current.noteId);
if (noteState && (noteState.to || noteState.from)) { if (noteState && (noteState.to || noteState.from)) {
const size = editors[tabRef.current.id]?.state.doc.content.size || 0; const size = editors[tabRef.current.id]?.state.doc.content.size || 0;
@@ -124,7 +127,7 @@ const Tiptap = ({
premium: settings.premium premium: settings.premium
}, },
onPermissionDenied: () => { onPermissionDenied: () => {
post(EventTypes.pro, undefined, tabRef.current.id, tab.noteId); post(EditorEvents.pro, undefined, tabRef.current.id, tab.noteId);
} }
}); });
@@ -161,7 +164,7 @@ const Tiptap = ({
) as Promise<string | undefined>; ) as Promise<string | undefined>;
}, },
createInternalLink(attributes) { createInternalLink(attributes) {
return postAsyncWithTimeout(EventTypes.createInternalLink, { return postAsyncWithTimeout(EditorEvents.createInternalLink, {
attributes attributes
}); });
}, },
@@ -305,12 +308,12 @@ const Tiptap = ({
if (isFocusedRef.current) return; if (isFocusedRef.current) return;
if (state.currentTab === tabRef.current.id) { if (state.currentTab === tabRef.current.id) {
isFocusedRef.current = true; isFocusedRef.current = true;
const noteState = tabRef.current.noteId const noteState = tabRef.current?.noteId
? state.noteState[tabRef.current.noteId] ? state.getNoteState(tabRef.current.noteId)
: undefined; : undefined;
post( post(
EventTypes.tabFocused, EditorEvents.tabFocused,
!!globalThis.editorControllers[tabRef.current.id]?.content.current && !!globalThis.editorControllers[tabRef.current.id]?.content.current &&
!editorControllers[tabRef.current.id]?.loading, !editorControllers[tabRef.current.id]?.loading,
tabRef.current.id, tabRef.current.id,
@@ -438,7 +441,9 @@ const Tiptap = ({
display: isFocused ? "flex" : "none", display: isFocused ? "flex" : "none",
flex: 1, flex: 1,
flexDirection: "column", flexDirection: "column",
maxWidth: "100vw" maxWidth: "100vw",
position: "relative",
overflow: "hidden"
}} }}
ref={editorRoot} ref={editorRoot}
onDoubleClick={onClickEmptyArea} onDoubleClick={onClickEmptyArea}
@@ -547,57 +552,36 @@ const Tiptap = ({
</button> </button>
</div> </div>
<div
onScroll={controller.scroll}
ref={containerRef}
style={{
overflowY: controller.loading ? "hidden" : "scroll",
height: "100%",
display: "block",
position: "relative"
}}
>
{settings.noHeader || tab.locked ? null : (
<>
<Tags settings={settings} loading={controller.loading} />
<Title
titlePlaceholder={controller.titlePlaceholder}
readonly={settings.readonly}
controller={controllerRef}
title={controller.title}
fontFamily={settings.fontFamily}
dateFormat={settings.dateFormat}
timeFormat={settings.timeFormat}
loading={controller.loading}
/>
<StatusBar
container={containerRef}
loading={controller.loading}
/>
</>
)}
{controller.loading || tab.locked ? ( {controller.loading || tab.locked ? (
<div <div
style={{ style={{
width: "100%", width: "100%",
height: "100%", height: "100%",
position: "absolute", position: "absolute",
zIndex: 999, zIndex: 800,
backgroundColor: colors.primary.background, backgroundColor: colors.primary.background,
paddingRight: 12,
paddingLeft: 12,
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
alignItems: tab.locked ? "center" : "flex-start", alignItems: tab.locked ? "center" : "flex-start",
justifyContent: tab.locked ? "center" : "flex-start", justifyContent: tab.locked ? "center" : "flex-start",
boxSizing: "border-box", boxSizing: "border-box",
rowGap: 10 rowGap: 10,
marginTop: `${50 + insets.top}px`
}} }}
> >
{tab.locked ? ( {tab.locked ? (
<> <div
style={{
flexDirection: "column",
paddingLeft: 12,
paddingRight: 12,
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
gap: 10
}}
>
<p <p
style={{ style={{
color: colors.primary.paragraph, color: colors.primary.paragraph,
@@ -611,6 +595,52 @@ const Tiptap = ({
> >
{controller.title} {controller.title}
</p> </p>
<p
style={{
color: colors.primary.paragraph,
marginTop: 0,
marginBottom: 0,
userSelect: "none"
}}
>
This note is locked.
</p>
<form
onSubmit={(e) => {
e.preventDefault();
const data = new FormData(e.currentTarget);
const password = data.get("password");
const biometrics = data.get("enrollBiometrics");
post("editor-events:unlock", {
password,
biometrics: biometrics === "on" ? true : false
});
}}
style={{
display: "flex",
flexDirection: "column",
rowGap: 10
}}
>
<input
placeholder="Enter password"
ref={controller.passwordInputRef}
name="password"
type="password"
required
style={{
color: colors.primary.paragraph,
fontSize: 20,
fontWeight: "600",
textAlign: "center",
padding: "0px 20px",
marginBottom: 0,
userSelect: "none"
}}
>
{controller.title}
</p>
<p <p
style={{ style={{
color: colors.primary.paragraph, color: colors.primary.paragraph,
@@ -771,10 +801,139 @@ const Tiptap = ({
width: "94%", width: "94%",
backgroundColor: colors.secondary.background, backgroundColor: colors.secondary.background,
borderRadius: 5, borderRadius: 5,
marginTop: 10 border: `1px solid ${colors.primary.border}`,
paddingLeft: 12,
paddingRight: 12,
fontSize: "1em",
backgroundColor: "transparent",
caretColor: colors.primary.accent,
color: colors.primary.paragraph
}} }}
/> />
<button
style={{
backgroundColor: colors.primary.accent,
borderRadius: 5,
boxSizing: "border-box",
border: "none",
color: colors.static.white,
width: 300,
fontSize: "0.9em",
height: 45,
display: "flex",
alignItems: "center",
justifyContent: "center"
}}
onMouseDown={(e) => {
if (globalThis.keyboardShown) {
e.preventDefault();
}
}}
>
<p
style={{
userSelect: "none"
}}
>
Unlock note
</p>
</button>
{biometryAvailable && !biometryEnrolled ? (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 5
}}
>
<input
type="checkbox"
name="enrollBiometrics"
style={{
accentColor: colors.primary.accent
}}
onMouseDown={(e) => {
if (globalThis.keyboardShown) {
e.preventDefault();
}
}}
/>
<p
style={{
color: colors.primary.paragraph,
marginTop: 0,
marginBottom: 0,
userSelect: "none"
}}
>
Enable biometric unlocking
</p>
</div>
) : null}
</form>
{biometryEnrolled && biometryAvailable ? (
<button
style={{
backgroundColor: "transparent",
borderRadius: 5,
boxSizing: "border-box",
border: "none",
color: colors.primary.accent,
width: 300,
fontSize: "0.9em",
height: 45,
display: "flex",
alignItems: "center",
justifyContent: "center",
columnGap: 5,
userSelect: "none"
}}
onMouseDown={(e) => {
if (globalThis.keyboardShown) {
e.preventDefault();
}
}}
onClick={() => {
post("editor-events:unlock-biometrics");
}}
>
<FingerprintIcon />
<p
style={{
userSelect: "none"
}}
>
Unlock with biometrics
</p>
</button>
) : null}
</div>
) : (
<>
<Tags settings={settings} loading={controller.loading} />
<div
style={{
display: "flex",
flexDirection: "column",
paddingLeft: 12,
paddingRight: 12,
width: "100%",
gap: 10
}}
>
<div
style={{
height: 25,
width: "100%",
backgroundColor: colors.secondary.background,
borderRadius: 5
}}
/>
<div <div
style={{ style={{
flexDirection: "row", flexDirection: "row",
@@ -842,11 +1001,43 @@ const Tiptap = ({
marginTop: 10 marginTop: 10
}} }}
/> />
</div>
</> </>
)} )}
</div> </div>
) : null} ) : null}
<div
onScroll={controller.scroll}
ref={containerRef}
style={{
overflowY: controller.loading ? "hidden" : "scroll",
height: "100%",
display: "block",
position: "relative"
}}
>
{settings.noHeader || tab.locked ? null : (
<>
<Tags settings={settings} loading={controller.loading} />
<Title
titlePlaceholder={controller.titlePlaceholder}
readonly={settings.readonly}
controller={controllerRef}
title={controller.title}
fontFamily={settings.fontFamily}
dateFormat={settings.dateFormat}
timeFormat={settings.timeFormat}
loading={controller.loading}
/>
<StatusBar
container={containerRef}
loading={controller.loading}
/>
</>
)}
<div <div
style={{ style={{
display: tab.locked ? "none" : "block" display: tab.locked ? "none" : "block"

View File

@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { ControlledMenu, MenuItem as MenuItemInner } from "@szhsin/react-menu"; import { ControlledMenu, MenuItem as MenuItemInner } from "@szhsin/react-menu";
import ArrowBackIcon from "mdi-react/ArrowBackIcon"; import ArrowBackIcon from "mdi-react/ArrowBackIcon";
import ArrowForwardIcon from "mdi-react/ArrowForwardIcon";
import ArrowULeftTopIcon from "mdi-react/ArrowULeftTopIcon"; import ArrowULeftTopIcon from "mdi-react/ArrowULeftTopIcon";
import ArrowURightTopIcon from "mdi-react/ArrowURightTopIcon"; import ArrowURightTopIcon from "mdi-react/ArrowURightTopIcon";
import DotsHorizontalIcon from "mdi-react/DotsHorizontalIcon"; import DotsHorizontalIcon from "mdi-react/DotsHorizontalIcon";
@@ -30,7 +31,8 @@ import TableOfContentsIcon from "mdi-react/TableOfContentsIcon";
import React, { useRef, useState } from "react"; import React, { useRef, useState } from "react";
import { useSafeArea } from "../hooks/useSafeArea"; import { useSafeArea } from "../hooks/useSafeArea";
import { useTabContext, useTabStore } from "../hooks/useTabStore"; import { useTabContext, useTabStore } from "../hooks/useTabStore";
import { EventTypes, Settings } from "../utils"; import { Settings } from "../utils";
import { EditorEvents } from "../utils/editor-events";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
import { strings } from "@notesnook/intl"; import { strings } from "@notesnook/intl";
@@ -100,6 +102,10 @@ function Header({
const openedTabsCount = useTabStore((state) => state.tabs.length); const openedTabsCount = useTabStore((state) => state.tabs.length);
const [isOpen, setOpen] = useState(false); const [isOpen, setOpen] = useState(false);
const btnRef = useRef(null); const btnRef = useRef(null);
const [canGoBack, canGoForward] = useTabStore((state) => [
state.canGoBack,
state.canGoForward
]);
return ( return (
<div <div
@@ -131,7 +137,7 @@ function Header({
) : ( ) : (
<Button <Button
onPress={() => { onPress={() => {
post(EventTypes.back, undefined, tab.id, tab.noteId); post(EditorEvents.back, undefined, tab.id, tab.noteId);
}} }}
preventDefault={false} preventDefault={false}
style={{ style={{
@@ -233,7 +239,7 @@ function Header({
{settings.deviceMode !== "mobile" && !settings.fullscreen ? ( {settings.deviceMode !== "mobile" && !settings.fullscreen ? (
<Button <Button
onPress={() => { onPress={() => {
post(EventTypes.fullscreen, undefined, tab.id, tab.noteId); post(EditorEvents.fullscreen, undefined, tab.id, tab.noteId);
}} }}
preventDefault={false} preventDefault={false}
style={{ style={{
@@ -296,7 +302,67 @@ function Header({
<Button <Button
onPress={() => { onPress={() => {
post(EventTypes.showTabs, undefined, tab.id, tab.noteId); editor?.commands.undo();
}}
style={{
borderWidth: 0,
borderRadius: 100,
color: "var(--nn_primary_icon)",
marginRight: 10,
width: 39,
height: 39,
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "relative"
}}
>
<ArrowULeftTopIcon
color={
!hasUndo
? "var(--nn_secondary_border)"
: "var(--nn_primary_icon)"
}
size={25 * settings.fontScale}
style={{
position: "absolute"
}}
/>
</Button>
<Button
onPress={() => {
editor?.commands.redo();
}}
style={{
borderWidth: 0,
borderRadius: 100,
color: "var(--nn_primary_icon)",
marginRight: 10,
width: 39,
height: 39,
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "relative"
}}
>
<ArrowURightTopIcon
color={
!hasRedo
? "var(--nn_secondary_border)"
: "var(--nn_primary_icon)"
}
size={25 * settings.fontScale}
style={{
position: "absolute"
}}
/>
</Button>
<Button
onPress={() => {
post(EditorEvents.showTabs, undefined, tab.id, tab.noteId);
}} }}
preventDefault={false} preventDefault={false}
style={{ style={{
@@ -341,7 +407,7 @@ function Header({
fwdRef={btnRef} fwdRef={btnRef}
onPress={() => { onPress={() => {
if (tab.locked) { if (tab.locked) {
post(EventTypes.properties, undefined, tab.id, tab.noteId); post(EditorEvents.properties, undefined, tab.id, tab.noteId);
} else { } else {
setOpen(!isOpen); setOpen(!isOpen);
} }
@@ -395,7 +461,7 @@ function Header({
switch (e.value) { switch (e.value) {
case "toc": case "toc":
post( post(
EventTypes.toc, EditorEvents.toc,
editorControllers[tab.id]?.getTableOfContents(), editorControllers[tab.id]?.getTableOfContents(),
tab.id, tab.id,
tab.noteId tab.noteId
@@ -406,7 +472,12 @@ function Header({
break; break;
case "properties": case "properties":
logger("info", "post properties..."); logger("info", "post properties...");
post(EventTypes.properties, undefined, tab.id, tab.noteId); post(
EditorEvents.properties,
undefined,
tab.id,
tab.noteId
);
break; break;
default: default:
break; break;
@@ -421,17 +492,85 @@ function Header({
alignItems: "center" alignItems: "center"
}} }}
> >
<MagnifyIcon <Button
size={22 * settings.fontScale} onPress={() => {
color="var(--nn_primary_icon)" post(EditorEvents.goBack, undefined, tab.id, tab.noteId);
/> setOpen(false);
<span }}
style={{ style={{
color: "var(--nn_primary_paragraph)" color: "var(--nn_primary_paragraph)"
}} }}
> >
{strings.search()} <ArrowBackIcon
</span> color={
!canGoBack
? "var(--nn_secondary_border)"
: "var(--nn_primary_icon)"
}
size={25 * settings.fontScale}
style={{
position: "absolute"
}}
/>
</Button>
<Button
onPress={() => {
post(EditorEvents.goForward, undefined, tab.id, tab.noteId);
setOpen(false);
}}
style={{
borderWidth: 0,
borderRadius: 100,
color: "var(--nn_primary_icon)",
marginRight: 10,
width: 39,
height: 39,
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "relative"
}}
>
<ArrowForwardIcon
color={
!canGoForward
? "var(--nn_secondary_border)"
: "var(--nn_primary_icon)"
}
size={25 * settings.fontScale}
style={{
position: "absolute"
}}
/>
</Button>
<Button
onPress={() => {
editor?.commands.startSearch();
setOpen(false);
}}
style={{
borderWidth: 0,
borderRadius: 100,
color: "var(--nn_primary_icon)",
marginRight: 10,
width: 39,
height: 39,
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "relative"
}}
>
<MagnifyIcon
size={28 * settings.fontScale}
style={{
position: "absolute"
}}
color="var(--nn_primary_icon)"
/>
</Button>
</MenuItem> </MenuItem>
<MenuItem <MenuItem

View File

@@ -27,7 +27,8 @@ import {
useState useState
} from "react"; } from "react";
import { useSettings } from "../hooks/useSettings"; import { useSettings } from "../hooks/useSettings";
import { EventTypes, Settings, isReactNative, randId } from "../utils"; import { Settings, isReactNative, randId } from "../utils";
import { EditorEvents } from "../utils/editor-events";
export const ReadonlyEditorProvider = (): JSX.Element => { export const ReadonlyEditorProvider = (): JSX.Element => {
const settings = useSettings(); const settings = useSettings();
@@ -95,7 +96,7 @@ const Tiptap = ({
delete pendingResolvers[resolverId]; delete pendingResolvers[resolverId];
resolve(data); resolve(data);
}; };
post(EventTypes.getAttachmentData, { post(EditorEvents.getAttachmentData, {
attachment, attachment,
resolverId: resolverId resolverId: resolverId
}); });
@@ -142,7 +143,7 @@ const Tiptap = ({
if (isSafari) { if (isSafari) {
root = window; root = window;
} }
post(EventTypes.readonlyEditorLoaded); post(EditorEvents.readonlyEditorLoaded);
const onMessage = (event: any) => { const onMessage = (event: any) => {
if (event?.data?.[0] !== "{") return; if (event?.data?.[0] !== "{") return;

View File

@@ -18,7 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { EventTypes, Settings } from "../utils"; import { Settings } from "../utils";
import { EditorEvents } from "../utils/editor-events";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
import { useTabContext } from "../hooks/useTabStore"; import { useTabContext } from "../hooks/useTabStore";
import { strings } from "@notesnook/intl"; import { strings } from "@notesnook/intl";
@@ -45,7 +46,7 @@ function Tags(props: { settings: Settings; loading?: boolean }): JSX.Element {
editor.commands.blur(); editor.commands.blur();
editorTitles[tab.id]?.current?.blur(); editorTitles[tab.id]?.current?.blur();
} }
post(EventTypes.newtag, undefined, tab.id, tab.noteId); post(EditorEvents.newtag, undefined, tab.id, tab.noteId);
}; };
const fontScale = props.settings?.fontScale || 1; const fontScale = props.settings?.fontScale || 1;
@@ -126,7 +127,7 @@ function Tags(props: { settings: Settings; loading?: boolean }): JSX.Element {
}} }}
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
post(EventTypes.tag, tag, tab.id, tab.noteId); post(EditorEvents.tag, tag, tab.id, tab.noteId);
}} }}
> >
#{tag.alias} #{tag.alias}

View File

@@ -32,7 +32,6 @@ import {
useState useState
} from "react"; } from "react";
import { import {
EventTypes,
getRoot, getRoot,
isReactNative, isReactNative,
post, post,
@@ -40,6 +39,7 @@ import {
saveTheme saveTheme
} from "../utils"; } from "../utils";
import { injectCss, transform } from "../utils/css"; import { injectCss, transform } from "../utils/css";
import { EditorEvents } from "../utils/editor-events";
import { pendingSaveRequests } from "../utils/pending-saves"; import { pendingSaveRequests } from "../utils/pending-saves";
import { useTabContext, useTabStore } from "./useTabStore"; import { useTabContext, useTabStore } from "./useTabStore";
@@ -157,9 +157,13 @@ export function useEditorController({
scroll: null scroll: null
}); });
if (!tabRef.current.noteId && loading) {
setTimeout(() => {
if (!tabRef.current.noteId && loading) { if (!tabRef.current.noteId && loading) {
setLoading(false); setLoading(false);
} }
}, 3000);
}
const selectionChange = useCallback((_editor: Editor) => {}, []); const selectionChange = useCallback((_editor: Editor) => {}, []);
@@ -167,7 +171,7 @@ export function useEditorController({
if (!isReactNative()) return; if (!isReactNative()) return;
const currentSessionId = globalThis.sessionId; const currentSessionId = globalThis.sessionId;
post( post(
EventTypes.contentchange, EditorEvents.contentchange,
undefined, undefined,
tabRef.current.id, tabRef.current.id,
tabRef.current.noteId tabRef.current.noteId
@@ -181,7 +185,7 @@ export function useEditorController({
currentSessionId currentSessionId
]; ];
const pendingTitleIds = await pendingSaveRequests.getPendingTitleIds(); const pendingTitleIds = await pendingSaveRequests.getPendingTitleIds();
postAsyncWithTimeout(EventTypes.title, ...params, 1000) postAsyncWithTimeout(EditorEvents.title, ...params, 1000)
.then(() => { .then(() => {
if (pendingTitleIds.length) { if (pendingTitleIds.length) {
dbLogger( dbLogger(
@@ -230,7 +234,7 @@ export function useEditorController({
} }
const currentSessionId = globalThis.sessionId; const currentSessionId = globalThis.sessionId;
post( post(
EventTypes.contentchange, EditorEvents.contentchange,
undefined, undefined,
tabRef.current.id, tabRef.current.id,
tabRef.current.noteId tabRef.current.noteId
@@ -239,8 +243,6 @@ export function useEditorController({
if (typeof timers.current.change === "number") { if (typeof timers.current.change === "number") {
clearTimeout(timers.current?.change); clearTimeout(timers.current?.change);
} }
timers.current.change = setTimeout(async () => {
htmlContentRef.current = editor.getHTML();
const params = [ const params = [
{ {
@@ -251,9 +253,12 @@ export function useEditorController({
tabRef.current.noteId, tabRef.current.noteId,
currentSessionId currentSessionId
]; ];
timers.current.change = setTimeout(async () => {
htmlContentRef.current = editor.getHTML();
const pendingContentIds = const pendingContentIds =
await pendingSaveRequests.getPendingContentIds(); await pendingSaveRequests.getPendingContentIds();
postAsyncWithTimeout(EventTypes.content, ...params, 5000) postAsyncWithTimeout(EditorEvents.content, ...params, 5000)
.then(() => { .then(() => {
if (pendingContentIds.length) { if (pendingContentIds.length) {
dbLogger( dbLogger(
@@ -284,12 +289,7 @@ export function useEditorController({
} }
}); });
logger( logger("info", "Editor saving content", params[1], params[2]);
"info",
"Editor saving content",
tabRef.current.id,
tabRef.current.noteId
);
}, 300); }, 300);
countWords(5000); countWords(5000);
@@ -343,13 +343,13 @@ export function useEditorController({
switch (type) { switch (type) {
case "native:updatehtml": { case "native:updatehtml": {
htmlContentRef.current = value; htmlContentRef.current = value;
logger("info", "UPDATING NOTE HTML");
if (tabRef.current.id !== useTabStore.getState().currentTab) { if (tabRef.current.id !== useTabStore.getState().currentTab) {
updateTabOnFocus.current = true; updateTabOnFocus.current = true;
} else { } else {
if (!editor) break; if (!editor) break;
const noteState = tabRef.current?.noteId const noteState = tabRef.current?.noteId
? useTabStore.getState().noteState[tabRef.current?.noteId] ? useTabStore.getState().getNoteState(tabRef.current?.noteId)
: null; : null;
const top = scrollTop() || noteState?.top || 0; const top = scrollTop() || noteState?.top || 0;
editor?.commands.setContent(htmlContentRef.current, false, { editor?.commands.setContent(htmlContentRef.current, false, {
@@ -418,12 +418,17 @@ export function useEditorController({
}, [onMessage]); }, [onMessage]);
const openFilePicker = useCallback((type: "image" | "file" | "camera") => { const openFilePicker = useCallback((type: "image" | "file" | "camera") => {
post(EventTypes.filepicker, type, tabRef.current.id, tabRef.current.noteId); post(
EditorEvents.filepicker,
type,
tabRef.current.id,
tabRef.current.noteId
);
}, []); }, []);
const downloadAttachment = useCallback((attachment: Attachment) => { const downloadAttachment = useCallback((attachment: Attachment) => {
post( post(
EventTypes.download, EditorEvents.download,
attachment, attachment,
tabRef.current.id, tabRef.current.id,
tabRef.current.noteId tabRef.current.noteId
@@ -431,23 +436,23 @@ export function useEditorController({
}, []); }, []);
const previewAttachment = useCallback((attachment: Attachment) => { const previewAttachment = useCallback((attachment: Attachment) => {
post( post(
EventTypes.previewAttachment, EditorEvents.previewAttachment,
attachment, attachment,
tabRef.current.id, tabRef.current.id,
tabRef.current.noteId tabRef.current.noteId
); );
}, []); }, []);
const openLink = useCallback((url: string) => { const openLink = useCallback((url: string) => {
post(EventTypes.link, url, tabRef.current.id, tabRef.current.noteId); post(EditorEvents.link, url, tabRef.current.id, tabRef.current.noteId);
return true; return true;
}, []); }, []);
const copyToClipboard = (text: string) => { const copyToClipboard = (text: string) => {
post(EventTypes.copyToClipboard, text); post(EditorEvents.copyToClipboard, text);
}; };
const getAttachmentData = (attachment: Partial<Attachment>) => { const getAttachmentData = (attachment: Partial<Attachment>) => {
return postAsyncWithTimeout(EventTypes.getAttachmentData, { return postAsyncWithTimeout(EditorEvents.getAttachmentData, {
attachment attachment
}); });
}; };

View File

@@ -29,6 +29,9 @@ globalThis.statusBars = {};
export type TabItem = { export type TabItem = {
id: number; id: number;
noteId?: string; noteId?: string;
/**
* @deprecated
*/
previewTab?: boolean; previewTab?: boolean;
readonly?: boolean; readonly?: boolean;
locked?: boolean; locked?: boolean;
@@ -66,6 +69,10 @@ export type TabStore = {
setNoteState: (noteId: string, state: Partial<NoteState>) => void; setNoteState: (noteId: string, state: Partial<NoteState>) => void;
biometryAvailable?: boolean; biometryAvailable?: boolean;
biometryEnrolled?: boolean; biometryEnrolled?: boolean;
canGoBack?: boolean;
canGoForward?: boolean;
sessionId?: string;
getNoteState: (noteId: string) => NoteState | undefined;
}; };
function getId(id: number, tabs: TabItem[]): number { function getId(id: number, tabs: TabItem[]): number {
@@ -80,10 +87,21 @@ export const useTabStore = create(
persist<TabStore>( persist<TabStore>(
(set, get) => ({ (set, get) => ({
noteState: {}, noteState: {},
getNoteState: (noteId: string) => {
const sessionId = get().sessionId;
const session = sessionId ? global.sessions.get(sessionId) : undefined;
if (session?.noteId === noteId) {
return {
top: session.scrollTop,
to: session.to,
from: session.from
};
}
return undefined;
},
tabs: [ tabs: [
{ {
id: 0, id: 0
previewTab: true
} }
], ],
currentTab: 0, currentTab: 0,
@@ -91,6 +109,15 @@ export const useTabStore = create(
setNoteState: (noteId: string, state: Partial<NoteState>) => { setNoteState: (noteId: string, state: Partial<NoteState>) => {
if (editorControllers[get().currentTab]?.loading) return; if (editorControllers[get().currentTab]?.loading) return;
const sessionId = get().sessionId;
if (sessionId) {
globalThis.sessions.updateSession(sessionId, {
from: state.from,
to: state.to,
scrollTop: state.top
});
}
const noteState = { const noteState = {
...get().noteState ...get().noteState
}; };
@@ -127,21 +154,7 @@ export const useTabStore = create(
scrollPosition scrollPosition
}); });
}, },
focusPreviewTab: (noteId: string, options) => { focusPreviewTab: (noteId: string, options) => {},
const index = get().tabs.findIndex((t) => t.previewTab);
if (index == -1) return get().newTab(noteId, true);
const tabs = [...get().tabs];
tabs[index] = {
...tabs[index],
noteId: noteId,
previewTab: true,
...options
};
set({
currentTab: tabs[index].id
});
},
focusEmptyTab: () => { focusEmptyTab: () => {
const index = get().tabs.findIndex((t) => !t.noteId); const index = get().tabs.findIndex((t) => !t.noteId);
if (index == -1) return get().newTab(); if (index == -1) return get().newTab();
@@ -159,8 +172,7 @@ export const useTabStore = create(
...get().tabs, ...get().tabs,
{ {
id: id, id: id,
noteId, noteId
previewTab: previewTab
} }
]; ];
set({ set({

View File

@@ -16,7 +16,8 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
export const EventTypes = {
export const EditorEvents = {
selection: "editor-event:selection", selection: "editor-event:selection",
content: "editor-event:content", content: "editor-event:content",
title: "editor-event:title", title: "editor-event:title",
@@ -49,5 +50,7 @@ export const EventTypes = {
disableReadonlyMode: "editor-events:disable-readonly-mode", disableReadonlyMode: "editor-events:disable-readonly-mode",
readonlyEditorLoaded: "readonlyEditorLoaded", readonlyEditorLoaded: "readonlyEditorLoaded",
error: "editorError", error: "editorError",
dbLogger: "editor-events:dbLogger" dbLogger: "editor-events:dbLogger",
}; goBack: "editor-events:go-back",
goForward: "editor-events:go-forward"
} as const;

View File

@@ -21,6 +21,8 @@ import { Editor, ToolbarGroupDefinition } from "@notesnook/editor";
import { ThemeDefinition } from "@notesnook/theme"; import { ThemeDefinition } from "@notesnook/theme";
import { Dispatch, MutableRefObject, RefObject, SetStateAction } from "react"; import { Dispatch, MutableRefObject, RefObject, SetStateAction } from "react";
import { EditorController } from "../hooks/useEditorController"; import { EditorController } from "../hooks/useEditorController";
import { EditorSessions } from "@notesnook/common/dist/utils/editor-sessions";
import { EditorEvents } from "./editor-events";
globalThis.sessionId = "notesnook-editor"; globalThis.sessionId = "notesnook-editor";
globalThis.pendingResolvers = {}; globalThis.pendingResolvers = {};
@@ -61,6 +63,7 @@ declare global {
}; };
var readonlyEditor: boolean; var readonlyEditor: boolean;
var sessions: EditorSessions;
var statusBars: Record< var statusBars: Record<
number, number,
| React.MutableRefObject<{ | React.MutableRefObject<{
@@ -150,8 +153,8 @@ declare global {
* @param value * @param value
*/ */
function post<T extends keyof typeof EventTypes>( function post<T extends keyof typeof EditorEvents>(
type: (typeof EventTypes)[T], type: (typeof EditorEvents)[T],
value?: unknown, value?: unknown,
tabId?: number, tabId?: number,
noteId?: string, noteId?: string,
@@ -184,44 +187,6 @@ export function getOnMessageListener(callback: () => void) {
}; };
} }
/* eslint-enable no-var */
export const EventTypes = {
selection: "editor-event:selection",
content: "editor-event:content",
title: "editor-event:title",
scroll: "editor-event:scroll",
history: "editor-event:history",
newtag: "editor-event:newtag",
tag: "editor-event:tag",
filepicker: "editor-event:picker",
download: "editor-event:download-attachment",
logger: "native:logger",
back: "editor-event:back",
pro: "editor-event:pro",
monograph: "editor-event:monograph",
properties: "editor-event:properties",
fullscreen: "editor-event:fullscreen",
link: "editor-event:link",
contentchange: "editor-event:content-change",
reminders: "editor-event:reminders",
previewAttachment: "editor-event:preview-attachment",
copyToClipboard: "editor-events:copy-to-clipboard",
getAttachmentData: "editor-events:get-attachment-data",
tabsChanged: "editor-events:tabs-changed",
showTabs: "editor-events:show-tabs",
tabFocused: "editor-events:tab-focused",
toc: "editor-events:toc",
createInternalLink: "editor-events:create-internal-link",
load: "editor-events:load",
unlock: "editor-events:unlock",
unlockWithBiometrics: "editor-events:unlock-biometrics",
disableReadonlyMode: "editor-events:disable-readonly-mode",
readonlyEditorLoaded: "readonlyEditorLoaded",
error: "editorError",
dbLogger: "editor-events:dbLogger"
} as const;
export function randId(prefix: string) { export function randId(prefix: string) {
return Math.random() return Math.random()
.toString(36) .toString(36)
@@ -244,7 +209,7 @@ export function logger(
}) })
.join(" "); .join(" ");
post(EventTypes.logger, `[${type}]: ` + logString); post(EditorEvents.logger, `[${type}]: ` + logString);
} }
export function dbLogger(type: "error" | "log", ...logs: unknown[]): void { export function dbLogger(type: "error" | "log", ...logs: unknown[]): void {
@@ -254,7 +219,7 @@ export function dbLogger(type: "error" | "log", ...logs: unknown[]): void {
}) })
.join(" "); .join(" ");
post(EventTypes.dbLogger, { post(EditorEvents.dbLogger, {
message: `[${type}]: ` + logString, message: `[${type}]: ` + logString,
error: logs[0] instanceof Error ? logs[0] : undefined error: logs[0] instanceof Error ? logs[0] : undefined
}); });
@@ -335,3 +300,10 @@ export function getTheme() {
} }
return undefined; return undefined;
} }
const editorSessions = new EditorSessions({
getGlobalNoteState: () => {
return globalThis.tabStore.getState().noteState;
}
});
globalThis.sessions = editorSessions;

View File

@@ -0,0 +1,32 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
export const NativeEvents = {
html: "native:html",
updatehtml: "native:updatehtml",
title: "native:title",
theme: "native:theme",
titleplaceholder: "native:titleplaceholder",
logger: "native:logger",
status: "native:status",
keyboardShown: "native:keyboardShown",
attachmentData: "native:attachment-data",
resolve: "native:resolve",
session: "native:session"
};

View File

@@ -16,7 +16,8 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { EventTypes, postAsyncWithTimeout, randId } from "."; import { postAsyncWithTimeout, randId } from ".";
import { EditorEvents } from "./editor-events";
class PendingSaveRequests { class PendingSaveRequests {
static TITLES = "pendingTitles"; static TITLES = "pendingTitles";
@@ -118,7 +119,7 @@ class PendingSaveRequests {
this.remove(PendingSaveRequests.TITLES); this.remove(PendingSaveRequests.TITLES);
for (const pending of pendingTitles) { for (const pending of pendingTitles) {
if (pending.params[0]) pending.params[0].pendingChanges = true; if (pending.params[0]) pending.params[0].pendingChanges = true;
await postAsyncWithTimeout(EventTypes.title, ...pending.params, 5000); await postAsyncWithTimeout(EditorEvents.title, ...pending.params, 5000);
} }
}; };
@@ -127,7 +128,11 @@ class PendingSaveRequests {
this.remove(PendingSaveRequests.CONTENT); this.remove(PendingSaveRequests.CONTENT);
for (const pending of pendingContents) { for (const pending of pendingContents) {
if (pending.params[0]) pending.params[0].pendingChanges = true; if (pending.params[0]) pending.params[0].pendingChanges = true;
await postAsyncWithTimeout(EventTypes.content, ...pending.params, 5000); await postAsyncWithTimeout(
EditorEvents.content,
...pending.params,
5000
);
} }
}; };