mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-16 19:57:52 +01:00
mobile: tab history
This commit is contained in:
committed by
Abdullah Atta
parent
e85f8b60b0
commit
c80286b587
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ const useLockedNoteHandler = () => {
|
|||||||
biometryAvailable: !!biometry,
|
biometryAvailable: !!biometry,
|
||||||
biometryEnrolled: !!fingerprint
|
biometryEnrolled: !!fingerprint
|
||||||
});
|
});
|
||||||
syncTabs();
|
syncTabs("biometry");
|
||||||
})();
|
})();
|
||||||
}, [tab?.id]);
|
}, [tab?.id]);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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 || [];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>(
|
||||||
|
|||||||
9
apps/mobile/package-lock.json
generated
9
apps/mobile/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
109
packages/common/src/utils/editor-sessions.ts
Normal file
109
packages/common/src/utils/editor-sessions.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
184
packages/common/src/utils/tab-history.ts
Normal file
184
packages/common/src/utils/tab-history.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
46
packages/editor-mobile/package-lock.json
generated
46
packages/editor-mobile/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
|||||||
32
packages/editor-mobile/src/utils/native-events.ts
Normal file
32
packages/editor-mobile/src/utils/native-events.ts
Normal 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"
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user