mobile: add editor tabs

This commit is contained in:
Ammar Ahmed
2023-12-21 10:14:53 +05:00
committed by Abdullah Atta
parent 815c9eb7de
commit 3f23507a74
37 changed files with 1497 additions and 548 deletions

View File

@@ -104,7 +104,7 @@ const DialogHeader = ({
title={button.title}
icon={button.icon}
type={button.type || "secondary"}
height={25}
height={30}
/>
) : null}
</View>

View File

@@ -394,9 +394,10 @@ export class VaultDialog extends Component {
return;
} else {
await db.vault.add(this.state.note.id);
if (this.state.note.id === editorController.current?.note?.id) {
eSendEvent(eClearEditor);
}
// if (this.state.note.id === editorController.current?.note?.id) {
// eSendEvent(eClearEditor, );
// }
this.close();
ToastManager.show({
message: "Note locked successfully",
@@ -501,9 +502,10 @@ export class VaultDialog extends Component {
}
if (this.state.note?.id) {
await db.vault.add(this.state.note.id);
if (this.state.note.id === editorController.current?.note?.id) {
eSendEvent(eClearEditor);
}
// TODO
// if (this.state.note.id === editorController.current?.note?.id) {
// eSendEvent(eClearEditor);
// }
this.setState({
loading: false
});

View File

@@ -33,11 +33,12 @@ import { notesnook } from "../../../../e2e/test.ids";
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
import NotebookScreen from "../../../screens/notebook";
import { TaggedNotes } from "../../../screens/notes/tagged";
import { useEditorStore } from "../../../stores/use-editor-store";
import useNavigationStore from "../../../stores/use-navigation-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { SIZE } from "../../../utils/size";
import { useTabStore } from "../../../screens/editor/tiptap/use-tab-store";
import { NotebooksWithDateEdited, TagsWithDateEdited } from "@notesnook/common";
import { Properties } from "../../properties";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
@@ -45,7 +46,6 @@ import { ReminderTime } from "../../ui/reminder-time";
import { TimeSince } from "../../ui/time-since";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { NotebooksWithDateEdited, TagsWithDateEdited } from "@notesnook/common";
type NoteItemProps = {
item: Note | BaseTrashItem<Note>;
@@ -73,8 +73,9 @@ const NoteItem = ({
locked,
noOpen = false
}: NoteItemProps) => {
const isEditingNote = useEditorStore(
(state) => state.currentEditingNote === item.id
const isEditingNote = useTabStore(
(state) =>
state.tabs.find((t) => t.id === state.currentTab)?.noteId === item.id
);
const { colors } = useThemeColors();
const compactMode = useIsCompactModeEnabled(

View File

@@ -29,7 +29,6 @@ import {
openVault,
presentSheet
} from "../../../services/event-manager";
import { useEditorStore } from "../../../stores/use-editor-store";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { eOnLoadNote, eShowMergeDialog } from "../../../utils/events";
import { tabBarRef } from "../../../utils/global-refs";
@@ -91,9 +90,6 @@ export const openNote = async (
)
});
} else {
if (note?.readonly) {
useEditorStore.getState().setReadonly(note?.readonly);
}
eSendEvent(eOnLoadNote, {
item: note
});

View File

@@ -17,17 +17,18 @@ 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 { Item } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import useIsSelected from "../../../hooks/use-selected";
import { useEditorStore } from "../../../stores/use-editor-store";
import { Item } from "@notesnook/core";
import { useTabStore } from "../../../screens/editor/tiptap/use-tab-store";
export const Filler = ({ item, color }: { item: Item; color?: string }) => {
const { colors } = useThemeColors();
const isEditingNote = useEditorStore(
(state) => state.currentEditingNote === item.id
const isEditingNote = useTabStore(
(state) =>
state.tabs.find((t) => t.id === state.currentTab)?.noteId === item.id
);
const [selected] = useIsSelected(item);
@@ -41,8 +42,8 @@ export const Filler = ({ item, color }: { item: Item; color?: string }) => {
backgroundColor: colors.selected.background,
borderLeftWidth: 5,
borderLeftColor: isEditingNote
? item.color
? colors.static[item.color]
? color
? color
: colors.selected.accent
: "transparent"
}}

View File

@@ -47,6 +47,7 @@ import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Seperator from "../ui/seperator";
import Paragraph from "../ui/typography/paragraph";
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
const MergeConflicts = () => {
const { colors } = useThemeColors();
@@ -88,10 +89,11 @@ const MergeConflicts = () => {
});
}
Navigation.queueRoutesForUpdate();
if (editorController.current?.note?.id === note.id) {
if (useTabStore.getState().getCurrentNoteId() === note.id) {
// reload the note in editor
eSendEvent(eOnLoadNote, {
item: editorController.current?.note,
item: editorController.current.note.current[note.id],
forced: true
});
}

View File

@@ -22,10 +22,10 @@ import React, { useEffect, useState } from "react";
import { View } from "react-native";
import { db } from "../../common/database";
import Editor from "../../screens/editor";
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
import { editorController } from "../../screens/editor/tiptap/utils";
import { eSendEvent, ToastManager } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { useEditorStore } from "../../stores/use-editor-store";
import { useSelectionStore } from "../../stores/use-selection-store";
import { useTrashStore } from "../../stores/use-trash-store";
import { eCloseSheet, eOnLoadNote } from "../../utils/events";
@@ -58,10 +58,11 @@ export default function NotePreview({ session, content, note }) {
return;
}
await db.noteHistory.restore(session.id);
if (useEditorStore.getState()?.currentEditingNote === session?.noteId) {
if (editorController.current?.note) {
if (useTabStore.getState().hasTabForNote(session?.noteId)) {
const note = editorController.current.note.current[session?.noteId];
if (note) {
eSendEvent(eOnLoadNote, {
item: editorController.current?.note,
item: note,
forced: true
});
}

View File

@@ -0,0 +1,154 @@
/*
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 { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import { useDBItem } from "../../../hooks/use-db-item";
import { presentSheet } from "../../../services/event-manager";
import { SIZE } from "../../../utils/size";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import { PressableButton } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph";
import Heading from "../../ui/typography/heading";
import { useTabStore } from "../../../screens/editor/tiptap/use-tab-store";
type TabItem = {
id: number;
noteId?: string;
};
const TabItemComponent = (props: {
tab: TabItem;
isFocused: boolean;
close?: (ctx?: string | undefined) => void;
}) => {
const { colors } = useThemeColors();
const [item] = useDBItem(props.tab.noteId, "note");
return (
<PressableButton
customStyle={{
alignItems: "center",
justifyContent: "space-between",
flexDirection: "row",
paddingLeft: 12,
height: 45
}}
type={props.isFocused ? "selected" : "transparent"}
onPress={() => {
if (!props.isFocused) {
useTabStore.getState().focusTab(props.tab.id);
}
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "flex-start"
}}
>
<Paragraph
color={
props.isFocused
? colors.selected.paragraph
: colors.primary.paragraph
}
size={SIZE.md}
>
{item?.title || "New note"}
</Paragraph>
</View>
<IconButton
name="close"
size={SIZE.lg}
color={colors.primary.icon}
onPress={() => {
useTabStore.getState().removeTab(props.tab.id);
}}
top={0}
left={0}
right={20}
bottom={0}
/>
</PressableButton>
);
};
export default function EditorTabs({
close
}: {
close?: (ctx?: string | undefined) => void;
}) {
const [tabs, currentTab] = useTabStore((state) => [
state.tabs,
state.currentTab
]);
return (
<View
style={{
paddingHorizontal: 12,
gap: 12
}}
>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
width: "100%",
alignItems: "center"
}}
>
<Heading size={SIZE.lg}>Tabs</Heading>
<Button
onPress={() => {
useTabStore.getState().newTab();
}}
title="New tab"
icon="plus"
style={{
flexDirection: "row",
justifyContent: "flex-start",
borderRadius: 100,
height: 35
}}
iconSize={SIZE.lg}
/>
</View>
{tabs.map((tab) => (
<TabItemComponent
key={tab.id}
tab={tab}
isFocused={tab.id === currentTab}
close={close}
/>
))}
</View>
);
}
EditorTabs.present = () => {
presentSheet({
component: (ref, close, update) => <EditorTabs close={close} />
});
};

View File

@@ -43,6 +43,7 @@ import PublishNoteSheet from "../components/sheets/publish-note";
import { RelationsList } from "../components/sheets/relations-list/index";
import ReminderSheet from "../components/sheets/reminder";
import { useSideBarDraggingStore } from "../components/side-menu/dragging-store";
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
import {
ToastManager,
eSendEvent,
@@ -52,7 +53,6 @@ import {
} from "../services/event-manager";
import Navigation from "../services/navigation";
import Notifications from "../services/notifications";
import { useEditorStore } from "../stores/use-editor-store";
import { useMenuStore } from "../stores/use-menu-store";
import useNavigationStore from "../stores/use-navigation-store";
import { useRelationStore } from "../stores/use-relation-store";
@@ -528,8 +528,12 @@ export const useActions = ({
const currentReadOnly = (item as Note).readonly;
await db.notes.readonly(!currentReadOnly, item?.id);
if (useEditorStore.getState().currentEditingNote === item.id) {
useEditorStore.getState().setReadonly(!currentReadOnly);
if (useTabStore.getState().hasTabForNote(item.id)) {
const tabId = useTabStore.getState().getTabForNote(item.id);
if (!tabId) return;
useTabStore.getState().updateTab(tabId, {
readonly: !currentReadOnly
});
}
Navigation.queueRoutesForUpdate();
close();

View File

@@ -53,6 +53,7 @@ import { MMKV } from "../common/database/mmkv";
import Migrate from "../components/sheets/migrate";
import NewFeature from "../components/sheets/new-feature";
import { Walkthrough } from "../components/walkthroughs";
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
import {
clearAppState,
editorController,
@@ -79,7 +80,6 @@ import SettingsService from "../services/settings";
import Sync from "../services/sync";
import { initAfterSync } from "../stores";
import { useAttachmentStore } from "../stores/use-attachment-store";
import { useEditorStore } from "../stores/use-editor-store";
import { useMessageStore } from "../stores/use-message-store";
import { useSettingStore } from "../stores/use-setting-store";
import { SyncStatus, useUserStore } from "../stores/use-user-store";
@@ -233,7 +233,7 @@ async function checkForShareExtensionLaunchedInBackground() {
}
if (notesAddedFromIntent || shareExtensionOpened) {
const id = useEditorStore.getState().currentEditingNote;
const id = useTabStore.getState().getCurrentNoteId();
const note = id && (await db.notes.note(id));
eSendEvent("webview_reset");
if (note) setTimeout(() => eSendEvent("loadingNote", note), 1);
@@ -246,7 +246,7 @@ async function checkForShareExtensionLaunchedInBackground() {
async function saveEditorState() {
if (editorState().currentlyEditing) {
const id = useEditorStore.getState().currentEditingNote;
const id = useTabStore.getState().getCurrentNoteId();
const note = id ? await db.notes.note(id) : undefined;
const locked = note && (await db.vaults.itemExists(note));
if (locked) return;
@@ -520,7 +520,7 @@ export const useAppEvents = () => {
}
} else {
SettingsService.appEnteredBackground();
const id = useEditorStore.getState().currentEditingNote;
const id = useTabStore.getState().getCurrentNoteId();
const note = id ? await db.notes.note(id) : undefined;
const locked = note && (await db.vaults.itemExists(note));
if (locked && SettingsService.canLockAppInBackground()) {
@@ -538,7 +538,9 @@ export const useAppEvents = () => {
}
if (Platform.OS === "ios") {
editorController.current?.commands.blur();
editorController.current?.commands.blur(
useTabStore.getState().currentTab
);
Keyboard.dismiss();
}
}

View File

@@ -46,6 +46,7 @@ import { FluidTabs } from "../components/tabs";
import useGlobalSafeAreaInsets from "../hooks/use-global-safe-area-insets";
import { useShortcutManager } from "../hooks/use-shortcut-manager";
import { hideAllTooltips } from "../hooks/use-tooltip";
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
import {
clearAppState,
editorController,
@@ -59,7 +60,6 @@ import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../services/event-manager";
import { useEditorStore } from "../stores/use-editor-store";
import { useSettingStore } from "../stores/use-setting-store";
import {
eClearEditor,
@@ -285,7 +285,7 @@ const _TabsHolder = () => {
case "mobile":
if (
!editorState().movedAway &&
useEditorStore.getState().currentEditingNote
useTabStore.getState().getCurrentNoteId()
) {
tabBarRef.current?.goToIndex(2, false);
} else {
@@ -511,6 +511,7 @@ const onChangeTab = async (obj) => {
editorState().movedAway = false;
editorState().isFocused = true;
activateKeepAwake();
console.log(editorState().currentlyEditing, "currentlyEditing...");
if (!editorState().currentlyEditing) {
eSendEvent(eOnLoadNote, {
newNote: true
@@ -522,12 +523,13 @@ const onChangeTab = async (obj) => {
editorState().movedAway = true;
editorState().isFocused = false;
eSendEvent(eClearEditor, "removeHandler");
setTimeout(() => useEditorStore.getState().setSearchReplace(false), 1);
let id = useEditorStore.getState().currentEditingNote;
let note = db.notes.note(id);
let id = useTabStore.getState().getCurrentNoteId();
let note = await db.notes.note(id);
const locked = note && (await db.vaults.itemExists(note));
if (locked) {
eSendEvent(eClearEditor);
useTabStore.getState().updateTab(useTabStore.getState().currentTab, {
locked: true
});
}
}
}

View File

@@ -35,7 +35,6 @@ import { db } from "../../common/database";
import { IconButton } from "../../components/ui/icon-button";
import useKeyboard from "../../hooks/use-keyboard";
import { eSubscribeEvent } from "../../services/event-manager";
import { useEditorStore } from "../../stores/use-editor-store";
import { getElevationStyle } from "../../utils/elevation";
import { openLinkInBrowser } from "../../utils/functions";
import EditorOverlay from "./loading";
@@ -43,6 +42,7 @@ import { EDITOR_URI } from "./source";
import { EditorProps, useEditorType } from "./tiptap/types";
import { useEditor } from "./tiptap/use-editor";
import { useEditorEvents } from "./tiptap/use-editor-events";
import { useTabStore } from "./tiptap/use-tab-store";
import { editorController } from "./tiptap/utils";
const style: ViewStyle = {
@@ -173,15 +173,24 @@ const Editor = React.memo(
export default Editor;
const ReadonlyButton = ({ editor }: { editor: useEditorType }) => {
const readonly = useEditorStore((state) => state.readonly);
const readonly = useTabStore(
(state) => state.tabs.find((t) => t.id === state.currentTab)?.readonly
);
const keyboard = useKeyboard();
const { colors } = useThemeColors();
const onPress = async () => {
if (editor.note.current) {
await db.notes.readonly(false, editor.note.current.id);
editor.note.current = await db.notes?.note(editor.note.current.id);
useEditorStore.getState().setReadonly(false);
const noteId = useTabStore
.getState()
.getNoteIdForTab(useTabStore.getState().currentTab);
if (noteId) {
await db.notes.readonly(!editor.note.current.readonly, noteId);
editor.note.current[noteId] = await db.notes?.note(noteId);
useTabStore.getState().updateTab(useTabStore.getState().currentTab, {
readonly: editor.note.current[noteId as string]?.readonly
});
}
};

View File

@@ -23,13 +23,11 @@ import { View } from "react-native";
import { ProgressBarComponent } from "../../components/ui/svg/lazy";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { useEditorStore } from "../../stores/use-editor-store";
import { useTabStore } from "./tiptap/use-tab-store";
export const ProgressBar = () => {
const { colors } = useThemeColors();
const currentlyEditingNote = useEditorStore(
(state) => state.currentEditingNote
);
const currentlyEditingNote = useTabStore((state) => state.getCurrentNoteId());
const downloading = useAttachmentStore((state) => state.downloading);
const loading = currentlyEditingNote

View File

@@ -28,5 +28,5 @@ const EditorMobileSourceUrl =
* The url should be something like this: http://192.168.100.126:3000/index.html
*/
export const EDITOR_URI = __DEV__
? EditorMobileSourceUrl
? "http://192.168.43.252:3000/index.html"
: EditorMobileSourceUrl;

View File

@@ -28,6 +28,7 @@ import { sleep } from "../../../utils/time";
import { Settings } from "./types";
import { getResponse, randId, textInput } from "./utils";
import { Note } from "@notesnook/core/dist/types";
import { useTabStore } from "./use-tab-store";
type Action = { job: string; id: string };
@@ -41,7 +42,7 @@ async function call(webview: RefObject<WebView | undefined>, action?: Action) {
return response ? response.value : response;
}
const fn = (fn: string) => {
const fn = (fn: string, name?: string) => {
const id = randId("fn_");
return {
job: `(async () => {
@@ -52,7 +53,7 @@ const fn = (fn: string) => {
post("${id}",response);
} catch(e) {
const DEV_MODE = ${__DEV__};
if (DEV_MODE && typeof logger !== "undefined") logger('error', "webview: ", e.message, e.stack);
if (DEV_MODE && typeof logger !== "undefined") logger('error', "webview: ", e.message, e.stack, "${name}");
}
return true;
})();true;`,
@@ -68,67 +69,87 @@ class Commands {
this.previousSettings = null;
}
async doAsync<T>(job: string) {
async doAsync<T>(job: string, name?: string) {
if (!this.ref.current) return false;
return call(this.ref, fn(job)) as Promise<T>;
return call(this.ref, fn(job, name)) as Promise<T>;
}
focus = async () => {
focus = async (tabId: number) => {
console.log("focus");
if (!this.ref.current) return;
if (Platform.OS === "android") {
//this.ref.current?.requestFocus();
setTimeout(async () => {
if (!this.ref) return;
textInput.current?.focus();
await this.doAsync("editor.commands.focus()");
await this.doAsync(`editors[${tabId}]?.commands.focus()`, "focus");
this.ref?.current?.requestFocus();
}, 1);
} else {
await sleep(400);
await this.doAsync("editor.commands.focus()");
await this.doAsync(`editors[${tabId}]?.commands.focus()`, "focus");
}
};
blur = async () =>
await this.doAsync(`
editor && editor.commands.blur();
typeof globalThis.editorTitle !== "undefined" && editorTitle.current && editorTitle.current.blur();
`);
blur = async (tabId: number) =>
await this.doAsync(
`
const editor = editors[${tabId}];
const editorTitle = editorTitles[${tabId}];
editor && editor.commands.blur();
typeof editorTitle !== "undefined" && editorTitle.current && editorTitle.current.blur();
`,
"blur"
);
clearContent = async () => {
clearContent = async (tabId: number) => {
console.log("clearContent");
this.previousSettings = null;
await this.doAsync(
`
if (typeof globalThis.statusBar !== "undefined") {
globalThis.statusBar.current.resetWords();
globalThis.statusBar.current.set({date:"",saved:""});
}
editor.commands.blur();
typeof globalThis.editorTitle !== "undefined" && editorTitle.current && editorTitle.current?.blur();
const editor = editors[${tabId}];
const editorController = editorControllers[${tabId}];
const editorTitle = editorTitles[${tabId}];
const statusBar = statusBars[${tabId}];
editor.commands.blur();
typeof editorTitle !== "undefined" && editorTitle.current && editorTitle.current?.blur();
if (editorController.content) editorController.content.current = null;
editorController.onUpdate();
editorController.setTitle(null);
`
editorController.countWords(0);
if (typeof statusBar !== "undefined") {
statusBar.current.resetWords();
statusBar.current.set({date:"",saved:""});
}`,
"clearContent"
);
};
setSessionId = async (id: string | null) =>
await this.doAsync(`globalThis.sessionId = "${id}";`);
setStatus = async (date: string | undefined, saved: string) =>
setStatus = async (
date: string | undefined,
saved: string,
tabId: number
) => {
console.log("setStatus");
await this.doAsync(
`typeof globalThis.statusBar !== "undefined" && statusBar.current.set({date:"${date}",saved:"${saved}"})`
`
const statusBar = statusBars[${tabId}];
typeof statusBar !== "undefined" && statusBar.current.set({date:"${date}",saved:"${saved}"})`,
"setStatus"
);
};
setPlaceholder = async (placeholder: string) => {
await this.doAsync(`
const element = document.querySelector(".is-editor-empty");
if (element) {
element.setAttribute("data-placeholder","${placeholder}");
}
`);
// await this.doAsync(`
// const element = document.querySelector(".is-editor-empty");
// if (element) {
// element.setAttribute("data-placeholder","${placeholder}");
// }
// `);
};
setInsets = async (insets: EdgeInsets) => {
@@ -173,10 +194,14 @@ editorController.setTitle(null);
setTags = async (note: Note | null | undefined) => {
if (!note) return;
const tabId = useTabStore.getState().getTabForNote(note.id);
const tags = await db.relations.to(note, "tag").resolve();
await this.doAsync(`
if (typeof editorTags !== "undefined" && editorTags.current) {
editorTags.current.setTags(${JSON.stringify(
await this.doAsync(
`
const tags = editorTags[${tabId}];
if (tags && tags.current) {
tags.current.setTags(${JSON.stringify(
tags.map((tag) => ({
title: tag.title,
alias: tag.title,
@@ -185,28 +210,38 @@ editorController.setTitle(null);
}))
)});
}
`);
};
clearTags = async () => {
await this.doAsync(`
if (typeof editorTags !== "undefined" && editorTags.current) {
editorTags.current.setTags([]);
}
`);
};
insertAttachment = async (attachment: Attachment) => {
await this.doAsync(
`editor && editor.commands.insertAttachment(${JSON.stringify(
attachment
)})`
`,
"setTags"
);
};
setAttachmentProgress = async (attachmentProgress: Partial<Attachment>) => {
clearTags = async (tabId: number) => {
await this.doAsync(
`editor && editor.commands.updateAttachment(${JSON.stringify(
`
const tags = editorTags[${tabId}];
logger("info", Object.keys(editorTags), typeof editorTags[0]);
if (tags && tags.current) {
tags.current.setTags([]);
}
`,
"clearTags"
);
};
insertAttachment = async (attachment: Attachment, tabId: number) => {
await this.doAsync(
`const editor = editors[${tabId}];
editor && editor.commands.insertAttachment(${JSON.stringify(attachment)})`
);
};
setAttachmentProgress = async (
attachmentProgress: AttachmentProgress,
tabId: number
) => {
await this.doAsync(
`const editor = editors[${tabId}];
editor && editor.commands.setAttachmentProgress(${JSON.stringify(
attachmentProgress
)}, {
preventUpdate: true,
@@ -220,10 +255,13 @@ editorController.setTitle(null);
insertImage = async (
image: Omit<ImageAttributes, "bloburl"> & {
dataurl: string;
}
},
tabId: number
) => {
await this.doAsync(
`const image = toBlobURL("${image.dataurl}", "${image.hash}");
`const editor = editors[${tabId}];
const image = toBlobURL("${image.dataurl}", "${image.hash}");
editor && editor.commands.insertImage({
...${JSON.stringify({
...image,
@@ -234,22 +272,30 @@ editorController.setTitle(null);
);
};
updateWebclip = async ({ src, hash }: Partial<ImageAttributes>) => {
updateWebclip = async (
{ src, hash }: Partial<ImageAttributes>,
tabId: number
) => {
await this.doAsync(
`editor && editor.commands.updateWebClip(${JSON.stringify({
`const editor = editors[${tabId}];
editor && editor.commands.updateWebClip(${JSON.stringify({
hash
})},${JSON.stringify({ src })})`
);
};
updateImage = async ({
hash,
dataurl
}: Partial<Omit<ImageAttributes, "bloburl">> & {
dataurl: string;
}) => {
updateImage = async (
{
hash,
dataurl
}: Partial<Omit<ImageAttributes, "bloburl">> & {
dataurl: string;
},
tabId: number
) => {
await this.doAsync(
`const image = toBlobURL("${dataurl}", "${hash}");
`const editor = editors[${tabId}];
const image = toBlobURL("${dataurl}", "${hash}");
editor && editor.commands.updateImage(${JSON.stringify({
hash
})}, {

View File

@@ -37,5 +37,8 @@ export const EventTypes = {
reminders: "editor-event:reminders",
previewAttachment: "editor-event:preview-attachment",
copyToClipboard: "editor-events:copy-to-clipboard",
getAttachmentData: "editor-events:get-attachment-data"
getAttachmentData: "editor-events:get-attachment-data",
tabsChanged: "editor-events:tabs-changed",
showTabs: "editor-events:showTabs",
tabFocused: "editor-events:tab-focused"
};

View File

@@ -37,6 +37,9 @@ import { useSettingStore } from "../../../stores/use-setting-store";
import { FILE_SIZE_LIMIT, IMAGE_SIZE_LIMIT } from "../../../utils/constants";
import { eCloseSheet } from "../../../utils/events";
import { editorController, editorState } from "./utils";
import { useSettingStore } from "../../../stores/use-setting-store";
import filesystem from "../../../common/filesystem";
import { useTabStore } from "./use-tab-store";
const showEncryptionSheet = (file) => {
presentSheet({
@@ -52,6 +55,15 @@ const santizeUri = (uri) => {
return uri;
};
/**
* @param {{
* noteId: string,
* tabId: string,
* type: "image" | "camera" | "file"
* reupload: boolean
* hash?: string
* }} fileOptions
*/
const file = async (fileOptions) => {
try {
const options = {
@@ -104,22 +116,33 @@ const file = async (fileOptions) => {
if (!(await attachFile(uri, hash, file.type, file.name, fileOptions)))
return;
if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri);
if (isImage(file.type)) {
editorController.current?.commands.insertImage({
hash: hash,
filename: file.name,
mime: file.type,
size: file.size,
dataurl: await db.attachments.read(hash, "base64"),
title: file.name
});
} else {
editorController.current?.commands.insertAttachment({
hash: hash,
filename: file.name,
mime: file.type,
size: file.size
});
if (
useTabStore.getState().getNoteIdForTab(options.tabId) === options.noteId
) {
if (isImage(file.type)) {
editorController.current?.commands.insertImage(
{
hash: hash,
filename: file.name,
mime: file.type,
size: file.size,
dataurl: await db.attachments.read(hash, "base64"),
title: file.name
},
fileOptions.tabId
);
} else {
editorController.current?.commands.insertAttachment(
{
hash: hash,
filename: file.name,
mime: file.type,
size: file.size
},
fileOptions.tabId
);
}
}
setTimeout(() => {
@@ -136,6 +159,15 @@ const file = async (fileOptions) => {
}
};
/**
* @param {{
* noteId: string,
* tabId: string,
* type: "image" | "camera" | "file"
* reupload: boolean
* hash?: string
* }} options
*/
const camera = async (options) => {
try {
await db.attachments.generateKey();
@@ -181,6 +213,17 @@ const gallery = async (options) => {
}
};
/**
*
* @param {{
* noteId: string,
* tabId: string,
* type: "image" | "camera" | "file"
* reupload: boolean
* hash?: string
* }} options
* @returns
*/
const pick = async (options) => {
if (!PremiumService.get()) {
let user = await db.user.getUser();
@@ -253,17 +296,40 @@ const handleImageResponse = async (response, options) => {
if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri);
editorController.current?.commands.insertImage({
hash: hash,
mime: image.type,
title: fileName,
dataurl: b64,
size: image.fileSize,
filename: fileName
});
if (
useTabStore.getState().getNoteIdForTab(options.tabId) === options.noteId
) {
editorController.current?.commands.insertImage(
{
hash: hash,
mime: image.type,
title: fileName,
dataurl: b64,
size: image.fileSize,
filename: fileName
},
options.tabId
);
}
}
};
/**
*
* @param {*} uri
* @param {*} hash
* @param {*} type
* @param {*} filename
/**
* @param {{
* noteId: string,
* tabId: string,
* type: "image" | "camera" | "file"
* reupload: boolean
* hash?: string
* }} options
* @returns
*/
export async function attachFile(uri, hash, type, filename, options) {
try {
let exists = db.attachments.exists(hash);
@@ -299,10 +365,7 @@ export async function attachFile(uri, hash, type, filename, options) {
} else {
encryptionInfo = { hash: hash };
}
await db.attachments.add(
encryptionInfo,
editorController.current?.note?.id
);
await db.attachments.add(encryptionInfo, options.noteId);
return true;
} catch (e) {

View File

@@ -0,0 +1,36 @@
/*
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 class SessionHistory extends Map {
get(key: any) {
let value = super.get(key);
if (Date.now() - value > 5 * 60 * 1000) {
value = Date.now();
this.set(key, value);
}
return value;
}
newSession(noteId: string) {
const value = Date.now();
this.set(noteId, value);
return value;
}
clearSession(noteId: string) {
this.delete(noteId);
}
}

View File

@@ -66,10 +66,12 @@ export type EditorProps = {
onChange?: (html: string) => void;
};
export type EditorMessage = {
export type EditorMessage<T> = {
sessionId: string;
value: unknown;
value: T;
type: string;
noteId: string;
tabId: number;
};
export type SavePayload = {
@@ -77,9 +79,9 @@ export type SavePayload = {
id?: string;
data?: string;
type?: "tiptap";
sessionId?: string | null;
sessionHistoryId?: number;
ignoreEdit: boolean;
tabId: number;
};
export type AppState = {

View File

@@ -35,6 +35,7 @@ import {
import { WebViewMessageEvent } from "react-native-webview";
import { db } from "../../../common/database";
import downloadAttachment from "../../../common/filesystem/download-attachment";
import EditorTabs from "../../../components/sheets/editor-tabs";
import ManageTagsSheet from "../../../components/sheets/manage-tags";
import { RelationsList } from "../../../components/sheets/relations-list";
import ReminderSheet from "../../../components/sheets/reminder";
@@ -66,9 +67,10 @@ import { tabBarRef } from "../../../utils/global-refs";
import { useDragState } from "../../settings/editor/state";
import { EventTypes } from "./editor-events";
import { EditorMessage, EditorProps, useEditorType } from "./types";
import { useTabStore } from "./use-tab-store";
import { EditorEvents, editorState } from "./utils";
const publishNote = async (editor: useEditorType) => {
const publishNote = async () => {
const user = useUserStore.getState().user;
if (!user) {
ToastManager.show({
@@ -91,9 +93,12 @@ const publishNote = async (editor: useEditorType) => {
});
return;
}
const currentNote = editor?.note?.current;
if (currentNote?.id) {
const note = await db.notes?.note(currentNote.id);
const noteId = useTabStore
.getState()
.getNoteIdForTab(useTabStore.getState().currentTab);
if (noteId) {
const note = await db.notes?.note(noteId);
const locked = note && (await db.vaults.itemExists(note));
if (locked) {
ToastManager.show({
@@ -110,11 +115,12 @@ const publishNote = async (editor: useEditorType) => {
}
};
const showActionsheet = async (editor: useEditorType) => {
const currentNote = editor?.note?.current;
if (currentNote?.id) {
const note = await db.notes?.note(currentNote.id);
const showActionsheet = async () => {
const noteId = useTabStore
.getState()
.getNoteIdForTab(useTabStore.getState().currentTab);
if (noteId) {
const note = await db.notes?.note(noteId);
if (editorState().isFocused || editorState().isFocused) {
editorState().isFocused = true;
}
@@ -145,7 +151,6 @@ export const useEditorEvents = (
]);
const handleBack = useRef<NativeEventSubscription>();
const readonly = useEditorStore((state) => state.readonly);
const isPremium = useUserStore((state) => state.premium);
const { fontScale } = useWindowDimensions();
@@ -193,7 +198,7 @@ export const useEditorEvents = (
deviceMode: deviceMode || "mobile",
fullscreen: fullscreen || false,
premium: isPremium,
readonly: readonly || editorPropReadonly,
readonly: false,
tools: tools || getDefaultPresets().default,
noHeader: noHeader,
noToolbar: noToolbar,
@@ -212,13 +217,11 @@ export const useEditorEvents = (
}, [
fullscreen,
isPremium,
readonly,
editor.loading,
deviceMode,
tools,
editor.commands,
doubleSpacedLines,
editorPropReadonly,
noHeader,
noToolbar,
corsProxy,
@@ -238,7 +241,7 @@ export const useEditorEvents = (
return;
}
editorState().currentlyEditing = false;
editor.reset();
// editor.reset(); Notes remain open.
setTimeout(async () => {
if (deviceMode !== "mobile" && fullscreen) {
if (fullscreen) {
@@ -334,23 +337,7 @@ export const useEditorEvents = (
const onMessage = useCallback(
async (event: WebViewMessageEvent) => {
const data = event.nativeEvent.data;
const editorMessage = JSON.parse(data) as EditorMessage;
if (editorMessage.type === EventTypes.content) {
editor.saveContent({
type: editorMessage.type,
content: (editorMessage.value as ContentMessage).html,
ignoreEdit: (editorMessage.value as ContentMessage).ignoreEdit,
forSessionId: editorMessage.sessionId
});
} else if (editorMessage.type === EventTypes.title) {
editor.saveContent({
type: editorMessage.type,
title: editorMessage.value as string,
forSessionId: editorMessage.sessionId,
ignoreEdit: false
});
}
const editorMessage = JSON.parse(data) as EditorMessage<any>;
if (editorMessage.type === EventTypes.back) {
return onBackPress();
@@ -363,7 +350,29 @@ export const useEditorEvents = (
return;
}
const noteId = useTabStore
.getState()
.getNoteIdForTab(editorMessage.tabId);
switch (editorMessage.type) {
case EventTypes.content:
editor.saveContent({
type: editorMessage.type,
content: editorMessage.value as string,
noteId: noteId,
tabId: editorMessage.tabId,
ignoreEdit: (editorMessage.value as ContentMessage).ignoreEdit
});
break;
case EventTypes.title:
editor.saveContent({
type: editorMessage.type,
title: editorMessage.value as string,
noteId: noteId,
tabId: editorMessage.tabId,
ignoreEdit: false
});
break;
case EventTypes.logger:
logger.info("[WEBVIEW LOG]", editorMessage.value);
break;
@@ -373,41 +382,45 @@ export const useEditorEvents = (
case EventTypes.selection:
break;
case EventTypes.reminders:
if (!editor.note.current) {
if (!noteId) {
ToastManager.show({
heading: "Create a note first to add a reminder",
type: "success"
});
return;
}
const note = await db.notes.note(noteId);
if (!note) return;
RelationsList.present({
reference: editor.note.current as any,
reference: note as any,
referenceType: "reminder",
relationType: "from",
title: "Reminders",
onAdd: () =>
ReminderSheet.present(undefined, editor.note.current as any, true)
onAdd: () => ReminderSheet.present(undefined, note, true)
});
break;
case EventTypes.newtag:
if (!editor.note.current) {
if (!noteId) {
ToastManager.show({
heading: "Create a note first to add a tag",
type: "success"
});
return;
}
ManageTagsSheet.present([editor.note.current?.id]);
ManageTagsSheet.present([noteId]);
break;
case EventTypes.tag:
if (editorMessage.value) {
if (!editor.note.current) return;
if (!noteId) return;
const note = await db.notes.note(noteId);
if (!note) return;
db.relations
.unlink(editorMessage.value as ItemReference, editor.note.current)
.unlink(editorMessage.value as ItemReference, note)
.then(async () => {
useTagStore.getState().refresh();
useRelationStore.getState().update();
await editor.commands.setTags(editor.note.current);
await editor.commands.setTags(note);
Navigation.queueRoutesForUpdate();
});
}
@@ -415,7 +428,11 @@ export const useEditorEvents = (
case EventTypes.filepicker:
editorState().isAwaitingResult = true;
const { pick } = require("./picker.js").default;
pick({ type: editorMessage.value });
pick({
type: editorMessage.value,
noteId: noteId,
tabId: editorMessage.tabId
});
setTimeout(() => {
editorState().isAwaitingResult = false;
}, 1000);
@@ -472,10 +489,10 @@ export const useEditorEvents = (
eSendEvent(eOpenPremiumDialog);
break;
case EventTypes.monograph:
publishNote(editor);
publishNote();
break;
case EventTypes.properties:
showActionsheet(editor);
showActionsheet();
break;
case EventTypes.fullscreen:
editorState().isFullscreen = true;
@@ -494,13 +511,41 @@ export const useEditorEvents = (
} else {
eSendEvent("PDFPreview", attachment);
}
break;
}
case EventTypes.copyToClipboard: {
Clipboard.setString(editorMessage.value as string);
break;
}
case EventTypes.tabsChanged: {
useTabStore.setState({
tabs: (editorMessage.value as any)?.tabs,
currentTab: (editorMessage.value as any)?.currentTab
});
console.log("tabs updated...");
break;
}
case EventTypes.showTabs: {
EditorTabs.present();
break;
}
case EventTypes.tabFocused: {
// Reload the note
const note = await db.notes.note(editorMessage.noteId);
if (note) {
eSendEvent(eOnLoadNote, {
item: note,
forced: true
});
}
// TODO
// Handle any updates that occured in an note while the tab was not focused.
// If editor has no content, reload the note, because it might be an app reload
// or the tab was destroyed in background...
// Maybe cache changes, something like pendingUpdates list.
// Do proper cleanup when a tab is destroyed though.
break;
}
default:
break;

View File

@@ -47,14 +47,15 @@ import Navigation from "../../../services/navigation";
import Notifications from "../../../services/notifications";
import SettingsService from "../../../services/settings";
import { TipManager } from "../../../services/tip-manager";
import { useEditorStore } from "../../../stores/use-editor-store";
import { useSettingStore } from "../../../stores/use-setting-store";
import { useTagStore } from "../../../stores/use-tag-store";
import { eClearEditor, eOnLoadNote } from "../../../utils/events";
import { tabBarRef } from "../../../utils/global-refs";
import { onNoteCreated } from "../../notes/common";
import Commands from "./commands";
import { SessionHistory } from "./session-history";
import { EditorState, SavePayload } from "./types";
import { useTabStore } from "./use-tab-store";
import {
EditorEvents,
clearAppState,
@@ -62,32 +63,50 @@ import {
getAppState,
isContentInvalid,
isEditorLoaded,
makeSessionId,
post
} from "./utils";
// Keep a fixed session id, dont' change it when a new note is opened, session id can stay the same always I think once the app is opened. DONE
// Editor will save any note content & title is recieved. and dispatch update to relavant tab always.
// Editor keeps track of what tab is opened and which note is currently focused by keeping a synced zustand store with editor. DONE
// the useEditor hook can recieve save messages for different notes at a time. DONE
// When a note is created, the useEditor hook must immediately notify the editor with the note id and set the note id in the editor tabs store
// so further changes will go into that note. DONE
// Events sent to editor have the tab id value added to ensure the correct tab will recieve and return events only.
// The useEditorEvents hook can manage events from different tabs at the same time as long as the attached session id matches. DONE
// useEditor hook will keep historySessionId for different notes instead of a single note. DONE
export const useEditor = (
editorId = "",
readonly?: boolean,
onChange?: (html: string) => void
) => {
const theme = useThemeEngineStore((state) => state.theme);
const [loading, setLoading] = useState(false);
const sessionIdRef = useRef(makeSessionId());
const sessionIdRef = useRef("notesnook-editor");
const editorRef = useRef<WebView>(null);
const currentNote = useRef<
| (Note & {
content?: NoteContent<false> & {
isPreview?: boolean;
};
})
| null
>();
const currentContent = useRef<Partial<UnencryptedContentItem> | null>();
const currentNotes = useRef<
Record<
string,
| (Note & {
content?: NoteContent<false> & {
isPreview?: boolean;
};
})
| null
| undefined
>
>({});
const currentContents = useRef<
Record<string, Partial<UnencryptedContentItem> | null>
>({});
const timers = useRef<{ [name: string]: NodeJS.Timeout }>({});
const commands = useMemo(() => new Commands(editorRef), [editorRef]);
const sessionHistoryId = useRef<number>();
const editorSessionHistory = useMemo(() => new SessionHistory(), []);
const state = useRef<Partial<EditorState>>(defaultState);
const placeholderTip = useRef(TipManager.placeholderTip());
const tags = useTagStore((state) => state.items);
@@ -96,11 +115,18 @@ export const useEditor = (
const saveCount = useRef(0);
const lastContentChangeTime = useRef<number>(0);
const lock = useRef(false);
const lockedSessionId = useRef<string>();
const currentLoadingNoteId = useRef<string>();
const loadingState = useRef<string>();
const postMessage = useCallback(
async <T>(type: string, data: T, waitFor = 300) =>
await post(editorRef, sessionIdRef.current, type, data, waitFor),
async <T>(type: string, data: T, tabId?: number, waitFor = 300) =>
await post(
editorRef,
sessionIdRef.current,
typeof tabId !== "number" ? useTabStore.getState().currentTab : tabId,
type,
data,
waitFor
),
[sessionIdRef]
);
@@ -115,14 +141,16 @@ export const useEditor = (
}, [theme, postMessage]);
useEffect(() => {
commands.setTags(currentNote.current);
for (const id in currentNotes.current) {
commands.setTags(currentNotes.current[id]);
}
}, [commands, tags]);
const overlay = useCallback(
(show: boolean, data = { type: "new" }) => {
eSendEvent(
"loadingNote" + editorId,
show ? data || currentNote.current : false
show ? data || currentNotes.current : false
);
},
[editorId]
@@ -144,32 +172,31 @@ export const useEditor = (
);
const reset = useCallback(
async (resetState = true, resetContent = true) => {
currentNote.current?.id && db.fs().cancel(currentNote.current.id);
currentNote.current = null;
currentContent.current = null;
sessionHistoryId.current = undefined;
async (tabId: number, resetState = true, resetContent = true) => {
const noteId = useTabStore.getState().getNoteIdForTab(tabId);
if (noteId) {
currentNotes.current?.id && db.fs().cancel(noteId, "download");
currentNotes.current[noteId] = null;
currentContents.current[noteId] = null;
editorSessionHistory.clearSession(noteId);
}
clearTimeout(timers.current["loading-images" + noteId]);
saveCount.current = 0;
loadingState.current = undefined;
lock.current = false;
useEditorStore.getState().setReadonly(false);
resetContent && postMessage(EditorEvents.title, "");
resetContent && postMessage(EditorEvents.title, "", tabId);
lastContentChangeTime.current = 0;
resetContent && (await commands.clearContent());
resetContent && (await commands.clearTags());
resetContent && (await commands.clearContent(tabId));
resetContent && (await commands.clearTags(tabId));
if (resetState) {
const newSessionId = makeSessionId();
sessionIdRef.current = newSessionId;
await commands.setSessionId(newSessionId);
isDefaultEditor &&
useEditorStore.getState().setCurrentlyEditingNote(null);
placeholderTip.current = TipManager.placeholderTip();
await commands.setPlaceholder(placeholderTip.current);
}
useTabStore.getState().updateTab(tabId, {
noteId: undefined
});
},
[commands, isDefaultEditor, postMessage]
[commands, editorSessionHistory, postMessage]
);
const saveNote = useCallback(
@@ -178,32 +205,24 @@ export const useEditor = (
id,
data,
type,
sessionId: currentSessionId,
ignoreEdit,
sessionHistoryId: currentSessionHistoryId,
ignoreEdit
tabId
}: SavePayload) => {
if (
readonly ||
useEditorStore.getState().readonly ||
currentNote.current?.readonly
)
return;
if (currentNotes.current[id as string]?.readonly) return;
try {
if (id && !(await db.notes?.note(id))) {
isDefaultEditor &&
useEditorStore.getState().setCurrentlyEditingNote(null);
await reset();
await reset(tabId);
return;
}
let note = id ? await db.notes?.note(id) : undefined;
const locked = note && (await db.vaults.itemExists(note));
if (note?.conflicted) return;
if (isContentInvalid(data)) {
if (isContentInvalid(data) && id) {
// Create a new history session if recieved empty or invalid content
// To ensure that history is preserved for correct content.
sessionHistoryId.current = Date.now();
currentSessionHistoryId = sessionHistoryId.current;
currentSessionHistoryId = editorSessionHistory.newSession(id);
}
const noteData: Partial<Note> & {
@@ -211,9 +230,7 @@ export const useEditor = (
content?: NoteContent<false>;
} = {
id,
sessionId: isContentInvalid(data)
? undefined
: (currentSessionHistoryId as any)
sessionId: `${currentSessionHistoryId}`
};
noteData.title = title;
@@ -229,10 +246,25 @@ export const useEditor = (
type: type as ContentType
};
}
// If note is edited, the tab becomes a persistent tab automatically.
useTabStore.getState().updateTab(tabId, {
previewTab: false
});
if (!locked) {
id = await db.notes?.add(noteData);
if (!note && id) {
currentNote.current = await db.notes?.note(id);
useTabStore.getState().updateTab(tabId, {
noteId: id
});
editorSessionHistory.newSession(id);
if (id) {
currentNotes.current[id] = await db.notes?.note(id);
}
const defaultNotebook = db.settings.getDefaultNotebook();
if (!state.current.onNoteCreated && defaultNotebook) {
onNoteCreated(id, {
@@ -244,25 +276,14 @@ export const useEditor = (
}
if (!noteData.title) {
postMessage(EditorEvents.title, currentNote.current?.title);
postMessage(
EditorEvents.title,
currentNotes.current?.title,
tabId
);
}
}
if (
useEditorStore.getState().currentEditingNote !== id &&
isDefaultEditor &&
state.current.currentlyEditing
) {
setTimeout(() => {
if (
(currentNote.current?.id && currentNote.current?.id !== id) ||
!state.current.currentlyEditing
)
return;
id && useEditorStore.getState().setCurrentlyEditingNote(id);
});
}
if (Notifications.isNotePinned(id as string)) {
Notifications.pinNote(id as string);
}
@@ -271,19 +292,21 @@ export const useEditor = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await db.vault?.save(noteData as any);
}
if (id && sessionIdRef.current === currentSessionId) {
if (id && useTabStore.getState().getTabForNote(id) === tabId) {
note = (await db.notes?.note(id)) as Note;
await commands.setStatus(
getFormattedDate(note.dateEdited, "date-time"),
"Saved"
"Saved",
tabId
);
lastContentChangeTime.current = note.dateEdited;
if (
saveCount.current < 2 ||
currentNote.current?.title !== note.title ||
currentNote.current?.headline?.slice(0, 200) !==
currentNotes.current[id]?.title !== note.title ||
currentNotes.current[id]?.headline?.slice(0, 200) !==
note.headline?.slice(0, 200)
) {
Navigation.queueRoutesForUpdate();
@@ -297,7 +320,7 @@ export const useEditor = (
DatabaseLogger.error(e as Error);
}
},
[commands, isDefaultEditor, postMessage, readonly, reset]
[commands, editorSessionHistory, postMessage, reset]
);
const loadContent = useCallback(
@@ -306,13 +329,13 @@ export const useEditor = (
content?: NoteContent<false>;
}
) => {
currentNote.current = note;
currentNotes.current[note.id] = note;
const locked = note && (await db.vaults.itemExists(note));
if ((locked || note.content) && note.content?.data) {
currentContent.current = {
currentContents.current[note.id] = {
data: note.content?.data,
type: note.content?.type || "tiptap",
noteId: currentNote.current?.id as string
noteId: note.id
};
} else if (note.contentId) {
const rawContent = await db.content?.get(note.contentId);
@@ -321,7 +344,7 @@ export const useEditor = (
!isDeleted(rawContent) &&
isUnencryptedContent(rawContent)
) {
currentContent.current = {
currentContents.current[note.id] = {
data: rawContent.data,
type: rawContent.type
};
@@ -334,93 +357,105 @@ export const useEditor = (
const loadNote = useCallback(
async (event: { item?: Note; forced?: boolean; newNote?: boolean }) => {
state.current.currentlyEditing = true;
const editorState = useEditorStore.getState();
if (
!state.current.ready &&
(await isEditorLoaded(editorRef, sessionIdRef.current))
(await isEditorLoaded(
editorRef,
sessionIdRef.current,
useTabStore.getState().currentTab
))
) {
state.current.ready = true;
}
if (event.newNote) {
currentNote.current && (await reset());
const nextSessionId = makeSessionId(event.item?.id);
sessionIdRef.current = nextSessionId;
sessionHistoryId.current = Date.now();
await commands.setSessionId(nextSessionId);
if (state.current?.ready) await commands.focus();
lastContentChangeTime.current = 0;
useEditorStore.getState().setReadonly(false);
useTabStore.getState().focusEmptyTab();
const tabId = useTabStore.getState().currentTab;
currentNotes.current && (await reset(tabId));
setTimeout(() => {
if (state.current?.ready) commands.focus(tabId);
lastContentChangeTime.current = 0;
});
} else {
if (!event.item) return;
const item = event.item;
if (!event.forced && currentNote.current?.id === item.id) return;
// If note was already opened in a tab, focus that tab.
if (useTabStore.getState().hasTabForNote(event.item.id)) {
const tabId = useTabStore.getState().getTabForNote(event.item.id);
if (typeof tabId === "number") {
useTabStore.getState().focusTab(tabId);
}
}
// Otherwise we focus the preview tab or create one to open the note in.
useTabStore.getState().focusPreviewTab(event.item.id, {
readonly: event.item.readonly,
locked: false
});
const tabId = useTabStore.getState().currentTab;
// If note is already loaded and forced reload is not requested, return.
if (!event.forced && currentNotes.current[item.id]) return;
state.current.movedAway = false;
state.current.currentlyEditing = true;
if (currentNote.current?.id !== item.id) {
currentNote.current && (await reset(false, false));
isDefaultEditor && editorState.setCurrentlyEditingNote(item.id);
if (!currentNotes.current[item.id]) {
// Reset current tab if note isn't already loaded.
currentNotes.current && (await reset(tabId, false, false));
}
await loadContent(item);
if (
currentNote.current?.id === item.id &&
currentNotes.current[item.id] &&
loadingState.current &&
currentContent.current?.data &&
loadingState.current === currentContent.current?.data
currentContents.current?.data &&
loadingState.current === currentContents.current?.data
) {
return;
}
if (
!currentContent.current?.data ||
currentContent.current?.data.length < 50000
) {
if (state.current.ready) overlay(false);
} else {
overlay(true);
}
if (!state.current.ready) {
currentNote.current = item;
currentNotes.current[item.id] = item;
return;
}
lastContentChangeTime.current = item.dateEdited;
const nextSessionId = makeSessionId(item.id);
sessionIdRef.current = nextSessionId;
lockedSessionId.current = nextSessionId;
sessionHistoryId.current = Date.now();
await commands.setSessionId(nextSessionId);
currentNote.current = item;
currentLoadingNoteId.current = item.id;
currentNotes.current[item.id] = item;
if (!currentNotes.current[item.id]) return;
editorSessionHistory.newSession(item.id);
await commands.setStatus(
getFormattedDate(item.dateEdited, "date-time"),
"Saved"
);
await postMessage(EditorEvents.title, item.title);
loadingState.current = currentContent.current?.data;
await postMessage(
EditorEvents.html,
currentContent.current?.data || "",
10000
"Saved",
tabId
);
await postMessage(EditorEvents.title, item.title, tabId);
loadingState.current = currentContents.current[item.id]?.data;
if (currentContents.current?.data) {
await postMessage(
EditorEvents.html,
currentContents.current?.data,
tabId,
10000
);
}
loadingState.current = undefined;
useEditorStore.getState().setReadonly(item.readonly);
await commands.setTags(currentNote.current);
await commands.setTags(item);
commands.setSettings();
setTimeout(() => {
if (lockedSessionId.current === nextSessionId) {
lockedSessionId.current = undefined;
if (currentLoadingNoteId.current === event.item?.id) {
currentLoadingNoteId.current = undefined;
}
}, 300);
overlay(false);
}
},
[commands, isDefaultEditor, loadContent, overlay, postMessage, reset]
[commands, editorSessionHistory, loadContent, postMessage, reset]
);
const lockNoteWithVault = useCallback((note: Note) => {
@@ -441,55 +476,56 @@ export const useEditor = (
if (!data) return;
const noteId = data.type === "tiptap" ? data.noteId : data.id;
if (!currentNote.current || noteId !== currentNote.current.id) return;
if (!useTabStore.getState().hasTabForNote(noteId)) return;
const tabId = useTabStore.getState().getTabForNote(noteId);
const isContentEncrypted =
typeof (data as ContentItem)?.data === "object";
const note = await db.notes?.note(currentNote.current?.id);
const note = await db.notes?.note(noteId);
if (lastContentChangeTime.current >= (data as Note).dateEdited) return;
lock.current = true;
if (data.type === "tiptap" && note) {
const locked = await db.vaults.itemExists(currentNote.current);
// Handle this case where note was locked on another device and synced.
const locked = await db.vaults.itemExists(
currentNotes.current[note.id]
);
if (!locked && isContentEncrypted) {
lockNoteWithVault(note);
} else if (locked && isEncryptedContent(data)) {
const decryptedContent = await db.vault?.decryptContent(
data,
currentNote?.current?.id
);
const decryptedContent = await db.vault?.decryptContent(data, noteId);
if (!decryptedContent) {
lockNoteWithVault(note);
} else {
await postMessage(EditorEvents.updatehtml, decryptedContent.data);
currentContent.current = decryptedContent;
await postMessage(
EditorEvents.updatehtml,
decryptedContent.data,
tabId
);
currentContents.current[note.id] = decryptedContent;
}
} else {
const _nextContent = data.data;
if (_nextContent === currentContent.current?.data) return;
if (_nextContent === currentContents.current?.data) return;
lastContentChangeTime.current = note.dateEdited;
await postMessage(EditorEvents.updatehtml, _nextContent);
await postMessage(EditorEvents.updatehtml, _nextContent, tabId);
if (!isEncryptedContent(data)) {
currentContent.current = data as UnencryptedContentItem;
currentContents.current[note.id] = data as UnencryptedContentItem;
}
}
} else {
if (data.type !== "note") return;
const note = data;
if (note.title !== currentNote.current.title) {
postMessage(EditorEvents.title, note.title);
}
if (note.tags !== currentNote.current.tags) {
await commands.setTags(note);
}
await commands.setStatus(
if (!note) return;
postMessage(EditorEvents.title, note.title, tabId);
commands.setTags(note);
commands.setStatus(
getFormattedDate(note.dateEdited, "date-time"),
"Saved"
"Saved",
tabId as number
);
}
lock.current = false;
},
[lockNoteWithVault, postMessage, commands]
@@ -513,55 +549,44 @@ export const useEditor = (
content,
type,
forSessionId,
ignoreEdit
ignoreEdit,
noteId,
tabId
}: {
noteId?: string;
title?: string;
content?: string;
type: string;
forSessionId: string;
ignoreEdit: boolean;
tabId: number;
}) => {
if (lock.current || lockedSessionId.current === forSessionId) return;
if (lock.current || currentLoadingNoteId.current === noteId) return;
lastContentChangeTime.current = Date.now();
if (
sessionHistoryId.current &&
Date.now() - sessionHistoryId.current > 5 * 60 * 1000
) {
sessionHistoryId.current = Date.now();
}
if (type === EditorEvents.content) {
currentContent.current = {
if (type === EditorEvents.content && noteId) {
currentContents.current[noteId as string] = {
data: content,
type: "tiptap",
noteId: currentNote.current?.id as string
noteId: noteId as string
};
}
const noteIdFromSessionId =
!forSessionId || forSessionId.startsWith("session")
? null
: forSessionId.split("_")[0];
const noteId = noteIdFromSessionId || currentNote.current?.id;
const params: SavePayload = {
title,
data: content,
type: "tiptap",
sessionId: forSessionId,
id: noteId,
sessionHistoryId: sessionHistoryId.current,
ignoreEdit
ignoreEdit,
sessionHistoryId: noteId ? editorSessionHistory.get(noteId) : undefined,
tabId: tabId
};
withTimer(
noteId || "newnote",
() => {
if (
currentNote.current &&
!params.id &&
params.sessionId === forSessionId
) {
params.id = currentNote.current?.id;
if (!params.id) {
params.id = useTabStore.getState().getNoteIdForTab(tabId);
}
if (onChange && params.data) {
onChange(params.data);
@@ -572,13 +597,12 @@ export const useEditor = (
ignoreEdit ? 0 : 150
);
},
[withTimer, onChange, saveNote]
[editorSessionHistory, withTimer, onChange, saveNote]
);
const restoreEditorState = useCallback(async () => {
const appState = getAppState();
if (!appState) return;
overlay(true, appState.note);
state.current.isRestoringState = true;
state.current.currentlyEditing = true;
state.current.movedAway = false;
@@ -604,7 +628,7 @@ export const useEditor = (
}
clearAppState();
state.current.isRestoringState = false;
}, [loadNote, overlay]);
}, [loadNote]);
useEffect(() => {
eSubscribeEvent(eOnLoadNote + editorId, loadNote);
@@ -622,7 +646,13 @@ export const useEditor = (
}, [loading]);
const onReady = useCallback(async () => {
if (!(await isEditorLoaded(editorRef, sessionIdRef.current))) {
if (
!(await isEditorLoaded(
editorRef,
sessionIdRef.current,
useTabStore.getState().currentTab
))
) {
eSendEvent("webview_reset", "onReady");
return false;
} else {
@@ -632,23 +662,24 @@ export const useEditor = (
}, [isDefaultEditor, restoreEditorState]);
const onLoad = useCallback(async () => {
if (currentNote.current) overlay(true);
if (currentNotes.current) overlay(true);
clearTimeout(timers.current["editor:loaded"]);
timers.current["editor:loaded"] = setTimeout(async () => {
postMessage(EditorEvents.theme, theme);
commands.setInsets(
isDefaultEditor ? insets : { top: 0, left: 0, right: 0, bottom: 0 }
);
await commands.setSessionId(sessionIdRef.current);
await commands.setSettings();
timers.current["editor:loaded"] = setTimeout(async () => {
if (!state.current.ready && (await onReady())) {
state.current.ready = true;
}
if (currentNote.current) {
loadNote({ ...currentNote.current, forced: true });
} else {
await commands.setPlaceholder(placeholderTip.current);
overlay(false);
const noteId = useTabStore.getState().getCurrentNoteId();
if (noteId && currentNotes.current[noteId]) {
loadNote({ ...currentNotes.current[noteId], forced: true });
}
});
});
@@ -672,7 +703,7 @@ export const useEditor = (
setLoading,
state,
sessionId: sessionIdRef,
note: currentNote,
note: currentNotes,
onReady,
saveContent,
onContentChanged,

View File

@@ -0,0 +1,140 @@
/*
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 create from "zustand";
import { editorController } from "./utils";
export type TabItem = {
id: number;
noteId?: string;
previewTab?: boolean;
readonly?: boolean;
locked?: boolean;
};
export type TabStore = {
tabs: TabItem[];
currentTab: number;
updateTab: (id: number, options: Omit<Partial<TabItem>, "id">) => void;
focusPreviewTab: (
noteId: string,
options: Omit<Partial<TabItem>, "id">
) => void;
removeTab: (index: number) => void;
moveTab: (index: number, toIndex: number) => void;
newTab: (noteId?: string) => void;
focusTab: (id: number) => void;
getNoteIdForTab: (id: number) => string | undefined;
getTabForNote: (noteId: string) => number | undefined;
hasTabForNote: (noteId: string) => boolean;
focusEmptyTab: () => void;
getCurrentNoteId: () => string | undefined;
};
export const useTabStore = create<TabStore>((set, get) => ({
tabs: [
{
id: 0
}
],
currentTab: 0,
updateTab: (id: number, options: Omit<Partial<TabItem>, "id">) => {
if (!options) return;
const index = get().tabs.findIndex((t) => t.id === id);
if (index == -1) return;
const tabs = [...get().tabs];
tabs[index] = {
...tabs[index],
...options
};
set({
tabs: tabs
});
editorController.current?.commands.doAsync(`
globalThis.tabStore.getState().updateTab(${id}, ${JSON.stringify(options)});
`);
},
focusPreviewTab: (noteId: string, options: Omit<Partial<TabItem>, "id">) => {
const index = get().tabs.findIndex((t) => t.previewTab);
if (index === -1) return get().newTab(noteId);
const tabs = [...get().tabs];
tabs[index] = {
...tabs[index],
noteId: noteId,
...options
};
set({
currentTab: tabs[index].id
});
editorController.current?.commands.doAsync(`
globalThis.tabStore.getState().focusPreviewTab(${
noteId ? `"${noteId}"` : ""
}, ${JSON.stringify(options || {})});
`);
},
removeTab: (index: number) => {
editorController.current?.commands.doAsync(`
globalThis.tabStore.getState().removeTab(${index});
`);
},
newTab: (noteId?: string) => {
editorController.current?.commands.doAsync(`
globalThis.tabStore.getState().newTab(${noteId ? `"${noteId}"` : ""});
`);
},
focusEmptyTab: () => {
const index = get().tabs.findIndex((t) => !t.noteId);
if (index === -1) return get().newTab();
const tabs = [...get().tabs];
tabs[index] = {
...tabs[index]
};
set({
currentTab: tabs[index].id
});
editorController.current?.commands.doAsync(`
globalThis.tabStore.getState().focusEmptyTab();
`);
},
moveTab: (index: number, toIndex: number) => {
editorController.current?.commands.doAsync(`
globalThis.tabStore.getState().moveTab(${index}, ${toIndex});
`);
},
focusTab: (id: number) => {
editorController.current?.commands.doAsync(`
globalThis.tabStore.getState().focusTab(${id});
`);
},
getNoteIdForTab: (id: number) => {
return get().tabs.find((t) => t.id === id)?.noteId;
},
hasTabForNote: (noteId: string) => {
return typeof get().tabs.find((t) => t.noteId === noteId)?.id === "number";
},
getTabForNote: (noteId: string) => {
return get().tabs.find((t) => t.noteId === noteId)?.id;
},
getCurrentNoteId: () => {
return get().tabs.find((t) => t.id === get().currentTab)?.noteId;
}
}));

View File

@@ -65,14 +65,16 @@ export function makeSessionId(id?: string) {
export async function isEditorLoaded(
ref: RefObject<WebView>,
sessionId: string
sessionId: string,
tabId: number
) {
return await post(ref, sessionId, EditorEvents.status);
return await post(ref, sessionId, tabId, EditorEvents.status);
}
export async function post<T>(
ref: RefObject<WebView>,
sessionId: string,
tabId: number,
type: string,
value: T | null = null,
waitFor = 300
@@ -84,7 +86,8 @@ export async function post<T>(
const message = {
type,
value,
sessionId: sessionId
sessionId: sessionId,
tabId
};
setImmediate(() => ref.current?.postMessage(JSON.stringify(message)));
const response = await getResponse(type, waitFor);

View File

@@ -27,7 +27,7 @@ import { useTagStore } from "../../stores/use-tag-store";
import { eOnLoadNote, eOnNotebookUpdated } from "../../utils/events";
import { openLinkInBrowser } from "../../utils/functions";
import { tabBarRef } from "../../utils/global-refs";
import { editorController, editorState } from "../editor/tiptap/utils";
import { editorState } from "../editor/tiptap/utils";
export const PLACEHOLDER_DATA = {
title: "Your notes",
@@ -52,11 +52,9 @@ export function openMonographsWebpage() {
export function openEditor() {
if (!DDS.isTab) {
if (editorController.current?.note) {
eSendEvent(eOnLoadNote, { newNote: true });
editorState().currentlyEditing = true;
editorState().movedAway = false;
}
eSendEvent(eOnLoadNote, { newNote: true });
editorState().currentlyEditing = true;
editorState().movedAway = false;
tabBarRef.current?.goToPage(1);
} else {
eSendEvent(eOnLoadNote, { newNote: true });

View File

@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import create from "zustand";
import { editorController } from "../screens/editor/tiptap/utils";
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
export type AttachmentGroupProgress = {
total: number;
@@ -66,10 +67,13 @@ export const useAttachmentStore = create<AttachmentStore>((set, get) => ({
remove: (hash) => {
const progress = get().progress;
if (!progress) return;
editorController.current?.commands.setAttachmentProgress({
hash: hash,
progress: 100
});
editorController.current?.commands.setAttachmentProgress(
{
hash: hash,
progress: 100
},
useTabStore.getState().currentTab
);
progress[hash] = null;
set({ progress: { ...progress } });
},
@@ -80,11 +84,14 @@ export const useAttachmentStore = create<AttachmentStore>((set, get) => ({
const progressPercentage =
type === "upload" ? sent / total : recieved / total;
editorController.current?.commands.setAttachmentProgress({
hash: hash,
//@ts-ignore
progress: Math.round(Math.max(progressPercentage * 100, 0))
});
editorController.current?.commands.setAttachmentProgress(
{
hash: hash,
//@ts-ignore
progress: Math.round(Math.max(progressPercentage * 100, 0))
},
useTabStore.getState().currentTab
);
set({ progress: { ...progress } });
},
encryptionProgress: 0,

View File

@@ -18,15 +18,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import create, { State } from "zustand";
import { eSubscribeEvent, eUnSubscribeEvent } from "../services/event-manager";
export interface EditorStore extends State {
currentEditingNote: string | null;
setCurrentlyEditingNote: (note: string | null) => void;
sessionId: string | null;
setSessionId: (sessionId: string | null) => void;
searchReplace: boolean;
setSearchReplace: (searchReplace: boolean) => void;
searchSelection: string | null;
readonly: boolean;
setReadonly: (readonly: boolean) => void;
@@ -39,34 +36,9 @@ export const useEditorStore = create<EditorStore>((set, get) => ({
setSessionId: (sessionId) => {
set({ sessionId });
},
searchReplace: false,
searchSelection: null,
readonly: false,
setReadonly: (readonly) => {
set({ readonly: readonly });
},
setSearchReplace: (searchReplace) => {
if (!searchReplace) {
set({ searchSelection: null, searchReplace: false });
return;
}
const func = (value: string) => {
eUnSubscribeEvent("selectionvalue", func);
if (!value && get().searchReplace) {
// endSearch();
return;
}
set({ searchSelection: value, searchReplace: true });
};
eSubscribeEvent("selectionvalue", func);
// tiny.call(
// EditorWebView,
// `(function() {
// if (editor) {
// reactNativeEventHandler('selectionvalue',editor.selection.getContent());
// }
// })();`
// );
}
}));

View File

@@ -19,6 +19,8 @@
"mdi-react": "9.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-freeze": "^1.0.3",
"tinycolor2": "1.6.0",
"zustand": "^4.4.7"
},
"devDependencies": {
@@ -4358,7 +4360,7 @@
"version": "15.7.11",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==",
"dev": true
"devOptional": true
},
"node_modules/@types/q": {
"version": "1.5.8",
@@ -4382,7 +4384,7 @@
"version": "18.2.39",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.39.tgz",
"integrity": "sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA==",
"dev": true,
"devOptional": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -4417,7 +4419,7 @@
"version": "0.16.8",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
"dev": true
"devOptional": true
},
"node_modules/@types/semver": {
"version": "7.5.6",
@@ -9713,7 +9715,7 @@
"version": "9.0.21",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
"dev": true,
"devOptional": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
@@ -15627,6 +15629,17 @@
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==",
"dev": true
},
"node_modules/react-freeze": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.3.tgz",
"integrity": "sha512-ZnXwLQnGzrDpHBHiC56TXFXvmolPeMjTn1UOm610M4EXGzbEDR7oOIyS2ZiItgbs6eZc4oU/a0hpk8PrcKvv5g==",
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=17.0.0"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -17496,6 +17509,11 @@
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
"dev": true
},
"node_modules/tinycolor2": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="
},
"node_modules/tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@@ -17758,6 +17776,20 @@
"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": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",

View File

@@ -13,6 +13,8 @@
"mdi-react": "9.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-freeze": "^1.0.3",
"tinycolor2": "1.6.0",
"zustand": "^4.4.7"
},
"devDependencies": {

View File

@@ -17,17 +17,19 @@ 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 { Global, css } from "@emotion/react";
import {
ScopedThemeProvider,
themeToCSS,
useThemeEngineStore
} from "@notesnook/theme";
import { useEffect, useMemo } from "react";
import { Freeze } from "react-freeze";
import "./App.css";
import Tiptap from "./components/editor";
import { TabContext, useTabStore } from "./hooks/useTabStore";
import { EmotionEditorTheme } from "./theme-factory";
import { Global, css } from "@emotion/react";
import { useMemo } from "react";
import { getTheme } from "./utils";
import { EventTypes, getTheme } from "./utils";
const currentTheme = getTheme();
if (currentTheme) {
@@ -35,11 +37,29 @@ if (currentTheme) {
}
function App(): JSX.Element {
const tabs = useTabStore((state) => state.tabs);
const currentTab = useTabStore((state) => state.currentTab);
useEffect(() => {
post(EventTypes.tabsChanged, {
tabs: tabs,
currentTab: currentTab
});
}, [tabs, currentTab]);
logger("info", "opened tabs count", tabs);
return (
<ScopedThemeProvider value="base">
<EmotionEditorTheme>
<GlobalStyles />
<Tiptap />
{tabs.map((tab) => (
<TabContext.Provider key={tab.id} value={tab}>
<Freeze freeze={currentTab !== tab.id}>
<Tiptap />
</Freeze>
</TabContext.Provider>
))}
</EmotionEditorTheme>
</ScopedThemeProvider>
);

View File

@@ -25,6 +25,7 @@ import {
usePermissionHandler,
useTiptap
} from "@notesnook/editor";
import { toBlobURL } from "@notesnook/editor/dist/utils/downloader";
import { useThemeColors } from "@notesnook/theme";
import {
forwardRef,
@@ -36,17 +37,19 @@ import {
} from "react";
import { useEditorController } from "../hooks/useEditorController";
import { useSettings } from "../hooks/useSettings";
import { TabStore, useTabContext, useTabStore } from "../hooks/useTabStore";
import { EmotionEditorToolbarTheme } from "../theme-factory";
import { EventTypes, Settings } from "../utils";
import Header from "./header";
import StatusBar from "./statusbar";
import Tags from "./tags";
import Title from "./title";
import { toBlobURL } from "@notesnook/editor/dist/utils/downloader";
globalThis.toBlobURL = toBlobURL as typeof globalThis.toBlobURL;
const Tiptap = ({ settings }: { settings: Settings }) => {
const tab = useTabContext();
const isFocused = useTabStore((state) => state.currentTab === tab?.id);
const [tick, setTick] = useState(0);
const contentRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
@@ -56,45 +59,48 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
premium: settings.premium
},
onPermissionDenied: () => {
post(EventTypes.pro);
post(EventTypes.pro, undefined, tab.id, tab.noteId);
}
});
const _editor = useTiptap(
{
onUpdate: ({ editor, transaction }) => {
global.editorController.contentChange(
globalThis.editorControllers[tab.id]?.contentChange(
editor as Editor,
transaction.getMeta("ignoreEdit")
);
},
onOpenAttachmentPicker: (editor, type) => {
global.editorController.openFilePicker(type);
globalThis.editorControllers[tab.id]?.openFilePicker(type);
return true;
},
onDownloadAttachment: (editor, attachment) => {
global.editorController.downloadAttachment(attachment);
globalThis.editorControllers[tab.id]?.downloadAttachment(attachment);
return true;
},
onPreviewAttachment(editor, attachment) {
global.editorController.previewAttachment(attachment);
globalThis.editorControllers[tab.id]?.previewAttachment(attachment);
return true;
},
getAttachmentData(attachment) {
return global.editorController.getAttachmentData(attachment);
return globalThis.editorControllers[tab.id]?.getAttachmentData(
attachment
) as Promise<string | undefined>;
},
element: !layout ? undefined : contentRef.current || undefined,
editable: !settings.readonly,
editorProps: {
editable: () => !settings.readonly
},
content: global.editorController?.content?.current,
content: globalThis.editorControllers[tab.id]?.content?.current,
isMobile: true,
doubleSpacedLines: settings.doubleSpacedLines,
onOpenLink: (url) => {
return global.editorController.openLink(url);
return globalThis.editorControllers[tab.id]?.openLink(url) || true;
},
copyToClipboard: (text) => {
globalThis.editorController.copyToClipboard(text);
globalThis.editorControllers[tab.id]?.copyToClipboard(text);
},
downloadOptions: {
corsHost: settings.corsProxy
@@ -118,17 +124,45 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
left: 0,
top: 0
});
globalThis.editorController.setTitlePlaceholder("Note title");
}, []);
globalThis.editorControllers[tab.id]?.setTitlePlaceholder("Note title");
}, [tab.id]);
const controller = useEditorController(update);
const controllerRef = useRef(controller);
globalThis.editorController = controller;
globalThis.editor = _editor;
globalThis.editorControllers[tab.id] = controller;
globalThis.editors[tab.id] = _editor;
useLayoutEffect(() => {
setLayout(true);
}, []);
const updateScrollPosition = (state: TabStore) => {
if (state.currentTab === tab.id) {
const position = state.scrollPosition[tab?.id];
if (position) {
containerRef.current?.scrollTo({
left: 0,
top: position,
behavior: "auto"
});
}
post(
EventTypes.tabFocused,
!!globalThis.editorControllers[tab.id]?.content.current,
tab.id,
tab.noteId
);
}
};
updateScrollPosition(useTabStore.getState());
const unsub = useTabStore.subscribe((state) => {
updateScrollPosition(state);
});
return () => {
unsub();
};
}, [tab.id, tab.noteId]);
const onClickEmptyArea: React.MouseEventHandler<HTMLDivElement> = useCallback(
(event) => {
@@ -149,15 +183,17 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
return;
}
const firstChild = globalThis.editor?.state.doc.firstChild;
const editor = editors[tab.id];
const firstChild = editor?.state.doc.firstChild;
const isParagraph = firstChild?.type.name === "paragraph";
const isFirstChildEmpty =
!firstChild?.textContent || firstChild?.textContent?.length === 0;
if (isParagraph && isFirstChildEmpty) {
globalThis.editor?.commands.focus("end");
editor?.commands.focus("end");
return;
}
globalThis.editor
editor
?.chain()
.insertContentAt(0, "<p></p>", {
updateSelection: true
@@ -166,33 +202,35 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
.run();
}
},
[]
[tab.id]
);
const onClickBottomArea = useCallback(() => {
const docSize = globalThis.editor?.state.doc.content.size;
const editor = editors[tab.id];
const docSize = editor?.state.doc.content.size;
if (!docSize) return;
const lastChild = globalThis.editor?.state.doc.lastChild;
const lastChild = editor?.state.doc.lastChild;
const isParagraph = lastChild?.type.name === "paragraph";
const isLastChildEmpty =
!lastChild?.textContent || lastChild?.textContent?.length === 0;
if (isParagraph && isLastChildEmpty) {
globalThis.editor?.commands.focus("end");
editor?.commands.focus("end");
return;
}
globalThis.editor
editor
?.chain()
.insertContentAt(docSize - 1, "<p></p>", {
updateSelection: true
})
.focus("end")
.run();
}, []);
}, [tab.id]);
return (
<>
<div
style={{
display: "flex",
display: isFocused ? "flex" : "none",
flex: 1,
flexDirection: "column",
maxWidth: "100vw"

View File

@@ -18,14 +18,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import ArrowBackIcon from "mdi-react/ArrowBackIcon";
import CrownIcon from "mdi-react/CrownIcon";
import DotsHorizontalIcon from "mdi-react/DotsHorizontalIcon";
import ArrowULeftTopIcon from "mdi-react/ArrowULeftTopIcon";
import ArrowURightTopIcon from "mdi-react/ArrowURightTopIcon";
import CrownIcon from "mdi-react/CrownIcon";
import DotsHorizontalIcon from "mdi-react/DotsHorizontalIcon";
import FullscreenIcon from "mdi-react/FullscreenIcon";
import MagnifyIcon from "mdi-react/MagnifyIcon";
import React from "react";
import { useSafeArea } from "../hooks/useSafeArea";
import { useTabContext, useTabStore } from "../hooks/useTabStore";
import { EventTypes, Settings } from "../utils";
import styles from "./styles.module.css";
@@ -65,7 +66,11 @@ function Header({
hasUndo: boolean;
hasRedo: boolean;
}): JSX.Element {
const tab = useTabContext();
const editor = editors[tab.id];
const insets = useSafeArea();
const openedTabsCount = useTabStore((state) => state.tabs.length);
return (
<div
style={{
@@ -95,7 +100,7 @@ function Header({
) : (
<Button
onPress={() => {
post(EventTypes.back);
post(EventTypes.back, undefined, tab.id, tab.noteId);
}}
preventDefault={false}
style={{
@@ -113,7 +118,7 @@ function Header({
}}
>
<ArrowBackIcon
size={27 * settings.fontScale}
size={28 * settings.fontScale}
style={{
position: "absolute"
}}
@@ -235,7 +240,7 @@ function Header({
}}
>
<MagnifyIcon
size={25 * settings.fontScale}
size={28 * settings.fontScale}
style={{
position: "absolute"
}}
@@ -246,7 +251,7 @@ function Header({
{settings.deviceMode !== "mobile" && !settings.fullscreen ? (
<Button
onPress={() => {
post(EventTypes.fullscreen);
post(EventTypes.fullscreen, undefined, tab.id, tab.noteId);
}}
preventDefault={false}
style={{
@@ -274,7 +279,46 @@ function Header({
<Button
onPress={() => {
post(EventTypes.properties);
post(EventTypes.showTabs, undefined, tab.id, tab.noteId);
}}
preventDefault={false}
style={{
borderWidth: 0,
borderRadius: 100,
color: "var(--nn_primary_icon)",
marginRight: 12,
width: 39,
height: 39,
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "relative"
}}
>
<div
style={{
border: "2.5px solid var(--nn_primary_icon)",
width: 20 * settings.fontScale,
height: 20 * settings.fontScale,
borderRadius: 5,
display: "flex",
justifyContent: "center",
alignItems: "center"
}}
>
<p
style={{
fontSize: 15 * settings.fontScale
}}
>
{openedTabsCount}
</p>
</div>
</Button>
<Button
onPress={() => {
post(EventTypes.properties, undefined, tab.id, tab.noteId);
}}
preventDefault={false}
style={{

View File

@@ -19,12 +19,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { RefObject, useEffect, useRef, useState } from "react";
import { getTotalWords, Editor } from "@notesnook/editor";
import { useTabContext } from "../hooks/useTabStore";
function StatusBar({ container }: { container: RefObject<HTMLDivElement> }) {
const [status, setStatus] = useState({
date: "",
saved: ""
});
const tab = useTabContext();
const [sticky, setSticky] = useState(false);
const stickyRef = useRef(false);
const prevScroll = useRef(0);
@@ -34,6 +36,7 @@ function StatusBar({ container }: { container: RefObject<HTMLDivElement> }) {
const statusBar = useRef({
set: setStatus,
updateWords: () => {
const editor = editors[tab.id];
const words = getTotalWords(editor as Editor) + " words";
if (currentWords.current === words) return;
setWords(words);
@@ -43,7 +46,13 @@ function StatusBar({ container }: { container: RefObject<HTMLDivElement> }) {
setWords(`0 words`);
}
});
globalThis.statusBar = statusBar;
useEffect(() => {
globalThis.statusBars[tab.id] = statusBar;
return () => {
globalThis.statusBars[tab.id] = undefined;
};
}, [tab.id, statusBar]);
const onScroll = React.useCallback((event: Event) => {
const currentOffset = (event.target as HTMLElement)?.scrollTop;

View File

@@ -17,26 +17,34 @@ 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 React, { useRef, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { EventTypes, Settings } from "../utils";
import styles from "./styles.module.css";
import { useTabContext } from "../hooks/useTabStore";
function Tags(props: { settings: Settings }): JSX.Element {
const [tags, setTags] = useState<
{ title: string; alias: string; id: string; type: "tag" }[]
>([]);
const editorTags = useRef({
const tagsRef = useRef({
setTags: setTags
});
const tab = useTabContext();
global.editorTags = editorTags;
useEffect(() => {
globalThis.editorTags[tab.id] = tagsRef;
return () => {
globalThis.editorTags[tab.id] = undefined;
};
}, [tab.id, tagsRef]);
const openManageTagsSheet = () => {
const editor = editors[tab.id];
if (editor?.isFocused) {
editor.commands.blur();
editorTitle.current?.blur();
editorTitles[tab.id]?.current?.blur();
}
post(EventTypes.newtag);
post(EventTypes.newtag, undefined, tab.id, tab.noteId);
};
const fontScale = props.settings?.fontScale || 1;
@@ -116,7 +124,7 @@ function Tags(props: { settings: Settings }): JSX.Element {
}}
onClick={(e) => {
e.preventDefault();
post(EventTypes.tag, tag);
post(EventTypes.tag, tag, tab.id, tab.noteId);
}}
>
#{tag.alias}

View File

@@ -22,6 +22,7 @@ import React, { RefObject, useCallback, useEffect, useRef } from "react";
import { EditorController } from "../hooks/useEditorController";
import styles from "./styles.module.css";
import { replaceDateTime } from "@notesnook/editor/dist/extensions/date-time";
import { useTabContext } from "../hooks/useTabStore";
function Title({
controller,
title,
@@ -39,10 +40,10 @@ function Title({
dateFormat: string;
timeFormat: string;
}) {
const tab = useTabContext();
const titleRef = useRef<HTMLTextAreaElement>(null);
const titleSizeDiv = useRef<HTMLDivElement>(null);
const emitUpdate = useRef(true);
global.editorTitle = titleRef;
const resizeTextarea = useCallback(() => {
if (!titleSizeDiv.current || !titleRef.current) return;
@@ -65,6 +66,13 @@ function Title({
};
}, [resizeTextarea, title]);
useEffect(() => {
globalThis.editorTitles[tab.id] = titleRef;
return () => {
globalThis.editorTitles[tab.id] = undefined;
};
}, [tab.id, titleRef]);
return (
<>
<div
@@ -121,6 +129,7 @@ function Title({
resizeTextarea();
}}
onKeyDown={(e) => {
const editor = editors[tab.id];
if (e.key === "Enter") {
e.preventDefault();
e.stopPropagation();

View File

@@ -32,6 +32,8 @@ import {
} from "react";
import { EventTypes, isReactNative, post, randId, saveTheme } from "../utils";
import { injectCss, transform } from "../utils/css";
import { useTabContext, useTabStore } from "./useTabStore";
type Attachment = {
hash: string;
filename: string;
@@ -105,6 +107,7 @@ export type EditorController = {
};
export function useEditorController(update: () => void): EditorController {
const tab = useTabContext();
const setTheme = useThemeEngineStore((store) => store.setTheme);
const { colors } = useThemeColors("editor");
const [title, setTitle] = useState("");
@@ -118,20 +121,26 @@ export function useEditorController(update: () => void): EditorController {
const selectionChange = useCallback((_editor: Editor) => {}, []);
const titleChange = useCallback((title: string) => {
post(EventTypes.contentchange);
post(EventTypes.title, title);
}, []);
const titleChange = useCallback(
(title: string) => {
post(EventTypes.contentchange, undefined, tab.id, tab.noteId);
post(EventTypes.title, title, tab.id, tab.noteId);
},
[tab.id, tab.noteId]
);
const countWords = useCallback((ms = 300) => {
if (typeof timers.current.wordCounter === "number")
clearTimeout(timers.current.wordCounter);
timers.current.wordCounter = setTimeout(() => {
console.time("wordCounter");
statusBar?.current?.updateWords();
console.timeEnd("wordCounter");
}, ms);
}, []);
const countWords = useCallback(
(ms = 300) => {
if (typeof timers.current.wordCounter === "number")
clearTimeout(timers.current.wordCounter);
timers.current.wordCounter = setTimeout(() => {
console.time("wordCounter");
statusBars[tab.id]?.current?.updateWords();
console.timeEnd("wordCounter");
}, ms);
},
[tab.id]
);
useEffect(() => {
injectCss(transform(colors));
@@ -145,29 +154,33 @@ export function useEditorController(update: () => void): EditorController {
if (typeof timers.current.change === "number") {
clearTimeout(timers.current?.change);
}
timers.current.change = setTimeout(
() => {
htmlContentRef.current = editor.getHTML();
post(
EventTypes.content,
{
html: htmlContentRef.current,
ignoreEdit: ignoreEdit
},
currentSessionId
);
},
ignoreEdit ? 0 : 300
);
timers.current.change = setTimeout(() => {
htmlContentRef.current = editor.getHTML();
post(
EventTypes.content,
{
html: htmlContentRef.current,
ignoreEdit: ignoreEdit
},
tab.id,
tab.noteId,
currentSessionId
);
}, 300);
countWords(5000);
},
[countWords]
[countWords, tab.id, tab.noteId]
);
const scroll = useCallback(
(_event: React.UIEvent<HTMLDivElement, UIEvent>) => {},
[]
(_event: React.UIEvent<HTMLDivElement, UIEvent>) => {
if (!tab) return;
useTabStore
.getState()
.setScrollPosition(tab.id, _event.currentTarget.scrollTop);
},
[tab]
);
const onUpdate = useCallback(() => {
@@ -177,19 +190,34 @@ export function useEditorController(update: () => void): EditorController {
const onMessage = useCallback(
(event: Event & { data?: string }) => {
if (event?.data?.[0] !== "{") return;
const message = JSON.parse(event.data);
const type = message.type;
const value = message.value;
global.sessionId = message.sessionId;
if (message.tabId !== tab.id) {
logger("info", "tab id not matched");
return;
}
logger(
"info",
"webview message for tab",
message.type,
tab.id,
message.tabId
);
const editor = editors[tab.id];
switch (type) {
case "native:updatehtml": {
htmlContentRef.current = value;
if (!editor) break;
const { from, to } = editor.state.selection;
editor?.commands.setContent(htmlContentRef.current, false, {
preserveWhitespace: true
});
editor.commands.setTextSelection({
from,
to
@@ -198,6 +226,7 @@ export function useEditorController(update: () => void): EditorController {
break;
}
case "native:html":
// logger("info", "loading html", htmlContentRef.current);
htmlContentRef.current = value;
update();
countWords();
@@ -232,7 +261,7 @@ export function useEditorController(update: () => void): EditorController {
}
post(type); // Notify that message was delivered successfully.
},
[update, countWords, setTheme]
[tab, update, countWords, setTheme]
);
useEffect(() => {
@@ -249,20 +278,32 @@ export function useEditorController(update: () => void): EditorController {
};
}, [onMessage]);
const openFilePicker = useCallback((type: "image" | "file" | "camera") => {
post(EventTypes.filepicker, type);
}, []);
const openFilePicker = useCallback(
(type: "image" | "file" | "camera") => {
post(EventTypes.filepicker, type, tab.id, tab.noteId);
},
[tab.id, tab.noteId]
);
const downloadAttachment = useCallback((attachment: Attachment) => {
post(EventTypes.download, attachment);
}, []);
const previewAttachment = useCallback((attachment: Attachment) => {
post(EventTypes.previewAttachment, attachment);
}, []);
const openLink = useCallback((url: string) => {
post(EventTypes.link, url);
return true;
}, []);
const downloadAttachment = useCallback(
(attachment: Attachment) => {
post(EventTypes.download, attachment, tab.id, tab.noteId);
},
[tab.id, tab.noteId]
);
const previewAttachment = useCallback(
(attachment: Attachment) => {
post(EventTypes.previewAttachment, attachment, tab.id, tab.noteId);
},
[tab.id, tab.noteId]
);
const openLink = useCallback(
(url: string) => {
post(EventTypes.link, url, tab.id, tab.noteId);
return true;
},
[tab.id, tab.noteId]
);
const copyToClipboard = (text: string) => {
post(EventTypes.copyToClipboard, text);

View File

@@ -0,0 +1,206 @@
/*
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 { createContext, useContext } from "react";
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
globalThis.editorControllers = {};
globalThis.editors = {};
global.editorTags = {};
global.editorTitles = {};
global.statusBars = {};
export type TabItem = {
id: number;
noteId?: string;
previewTab?: boolean;
readonly?: boolean;
};
export type TabStore = {
tabs: TabItem[];
currentTab: number;
scrollPosition: Record<number, number>;
updateTab: (id: number, options: Omit<Partial<TabItem>, "id">) => void;
removeTab: (index: number) => void;
moveTab: (index: number, toIndex: number) => void;
newTab: (noteId?: string) => void;
focusTab: (id: number) => void;
setScrollPosition: (id: number, position: number) => void;
getNoteIdForTab: (id: number) => string | undefined;
getTabForNote: (noteId: string) => number | undefined;
hasTabForNote: (noteId: string) => boolean;
focusEmptyTab: () => void;
focusPreviewTab: (
noteId: string,
options: Omit<Partial<TabItem>, "id">
) => void;
};
function getId(id: number, tabs: TabItem[]): number {
const exists = tabs.find((t) => t.id === id);
if (exists) {
return getId(id + 1, tabs);
}
return id;
}
export const useTabStore = create(
persist<TabStore>(
(set, get) => ({
tabs: [
{
id: 0,
previewTab: true
}
],
currentTab: 0,
scrollPosition: {},
updateTab: (id: number, options: Omit<Partial<TabItem>, "id">) => {
const index = get().tabs.findIndex((t) => t.id === id);
if (index == -1) return;
const tabs = [...get().tabs];
tabs[index] = {
...tabs[index],
...options
};
set({
tabs: tabs
});
},
removeTab: (index: number) => {
const tab = get().tabs.findIndex((t) => t.id === index);
if (tab > -1) {
const isFocused = get().tabs[tab].id === get().currentTab;
const nextTabs = get().tabs.slice();
nextTabs.splice(tab, 1);
if (nextTabs.length === 0) {
nextTabs.push({
id: 0
});
}
const scrollPosition = { ...get().scrollPosition };
if (scrollPosition[get().tabs[tab].id]) {
delete scrollPosition[get().tabs[tab].id];
}
globalThis.editorControllers[get().tabs[tab].id] = undefined;
set({
tabs: nextTabs,
currentTab: isFocused
? nextTabs[nextTabs.length - 1].id
: get().currentTab,
scrollPosition
});
}
},
focusPreviewTab: (noteId: string, options) => {
const index = get().tabs.findIndex((t) => t.previewTab);
if (index == -1) return get().newTab(noteId);
const tabs = [...get().tabs];
tabs[index] = {
...tabs[index],
noteId: noteId,
...options
};
set({
currentTab: tabs[index].id
});
},
focusEmptyTab: () => {
const index = get().tabs.findIndex((t) => !t.noteId);
if (index == -1) return get().newTab();
const tabs = [...get().tabs];
tabs[index] = {
...tabs[index]
};
set({
currentTab: tabs[index].id
});
},
newTab: (noteId?: string) => {
const id = getId(get().tabs.length, get().tabs);
const nextTabs = [
...get().tabs,
{
id: id,
noteId
}
];
set({
tabs: nextTabs,
currentTab: id
});
},
moveTab: (index: number, toIndex: number) => {
const tabs = get().tabs.slice();
tabs.splice(toIndex, 0, tabs.slice(index, 1)[0]);
set({
tabs: tabs
});
},
focusTab: (id: number) => {
set({
currentTab: id
});
},
setScrollPosition: (id: number, position: number) => {
set({
scrollPosition: {
...get().scrollPosition,
[id]: position
}
});
},
getNoteIdForTab: (id: number) => {
return get().tabs.find((t) => t.id === id)?.noteId;
},
hasTabForNote: (noteId: string) => {
return (
typeof get().tabs.find((t) => t.noteId === noteId)?.id === "number"
);
},
getTabForNote: (noteId: string) => {
return get().tabs.find((t) => t.noteId === noteId)?.id;
}
}),
{
name: "tab-storage",
storage: createJSONStorage(() => localStorage)
}
)
);
globalThis.tabStore = useTabStore;
export const TabContext = createContext<TabItem>({
id: 0
});
export const useTabContext = () => {
const tab = useContext(TabContext);
return tab;
};

View File

@@ -17,11 +17,12 @@ 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 { ToolbarGroupDefinition } from "@notesnook/editor";
import { Editor } from "@notesnook/editor";
import { Dispatch, MutableRefObject, RefObject, SetStateAction } from "react";
import { useEditorController } from "../hooks/useEditorController";
import { Editor, ToolbarGroupDefinition } from "@notesnook/editor";
import { ThemeDefinition } from "@notesnook/theme";
import { Dispatch, MutableRefObject, RefObject, SetStateAction } from "react";
import { EditorController } from "../hooks/useEditorController";
globalThis.sessionId = "notesnook-editor";
globalThis.pendingResolvers = {};
export function randId(prefix: string) {
@@ -60,15 +61,20 @@ declare global {
var pendingResolvers: {
[key: string]: (value: any) => void;
};
var statusBar: React.MutableRefObject<{
set: React.Dispatch<
React.SetStateAction<{
date: string;
saved: string;
var statusBars: Record<
number,
| React.MutableRefObject<{
set: React.Dispatch<
React.SetStateAction<{
date: string;
saved: string;
}>
>;
updateWords: () => void;
resetWords: () => void;
}>
>;
updateWords: () => void;
}>;
| undefined
>;
var __PLATFORM__: "ios" | "android";
var readonly: boolean;
var noToolbar: boolean;
@@ -78,14 +84,16 @@ declare global {
* Id of current session
*/
var sessionId: string;
var tabStore: any;
/**
* Current tiptap instance
* Current tiptap editors
*/
var editor: Editor | null;
var editors: Record<number, Editor | null>;
/**
* Current editor controller
* Current editor controllers
*/
var editorController: ReturnType<typeof useEditorController>;
var editorControllers: Record<number, EditorController | undefined>;
var settingsController: {
update: (settings: Settings) => void;
@@ -113,17 +121,21 @@ declare global {
>;
};
var editorTitle: RefObject<HTMLTextAreaElement>;
var editorTitles: Record<number, RefObject<HTMLTextAreaElement> | undefined>;
/**
* Global ref to manage tags in editor.
*/
var editorTags: MutableRefObject<{
setTags: React.Dispatch<
React.SetStateAction<
{ title: string; alias: string; id: string; type: "tag" }[]
>
>;
}>;
var editorTags: Record<
number,
| MutableRefObject<{
setTags: React.Dispatch<
React.SetStateAction<
{ title: string; alias: string; id: string; type: "tag" }[]
>
>;
}>
| undefined
>;
function logger(type: "info" | "warn" | "error", ...logs: unknown[]): void;
/**
@@ -134,7 +146,10 @@ declare global {
function post<T extends keyof typeof EventTypes>(
type: (typeof EventTypes)[T],
value?: unknown
value?: unknown,
tabId?: number,
noteId?: string,
sessionId?: string
): void;
interface Window {
/**
@@ -168,7 +183,10 @@ export const EventTypes = {
reminders: "editor-event:reminders",
previewAttachment: "editor-event:preview-attachment",
copyToClipboard: "editor-events:copy-to-clipboard",
getAttachmentData: "editor-events:get-attachment-data"
getAttachmentData: "editor-events:get-attachment-data",
tabsChanged: "editor-events:tabs-changed",
showTabs: "editor-events:show-tabs",
tabFocused: "editor-events:tab-focused"
} as const;
export function isReactNative(): boolean {
@@ -191,6 +209,8 @@ export function logger(
export function post<T extends keyof typeof EventTypes>(
type: (typeof EventTypes)[T],
value?: unknown,
tabId?: number,
noteId?: string,
sessionId?: string
): void {
if (isReactNative()) {
@@ -198,7 +218,9 @@ export function post<T extends keyof typeof EventTypes>(
JSON.stringify({
type,
value: value,
sessionId: sessionId || globalThis.sessionId
sessionId: sessionId || globalThis.sessionId,
tabId,
noteId
})
);
} else {