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} title={button.title}
icon={button.icon} icon={button.icon}
type={button.type || "secondary"} type={button.type || "secondary"}
height={25} height={30}
/> />
) : null} ) : null}
</View> </View>

View File

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

View File

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

View File

@@ -29,7 +29,6 @@ import {
openVault, openVault,
presentSheet presentSheet
} from "../../../services/event-manager"; } from "../../../services/event-manager";
import { useEditorStore } from "../../../stores/use-editor-store";
import { useSelectionStore } from "../../../stores/use-selection-store"; import { useSelectionStore } from "../../../stores/use-selection-store";
import { eOnLoadNote, eShowMergeDialog } from "../../../utils/events"; import { eOnLoadNote, eShowMergeDialog } from "../../../utils/events";
import { tabBarRef } from "../../../utils/global-refs"; import { tabBarRef } from "../../../utils/global-refs";
@@ -91,9 +90,6 @@ export const openNote = async (
) )
}); });
} else { } else {
if (note?.readonly) {
useEditorStore.getState().setReadonly(note?.readonly);
}
eSendEvent(eOnLoadNote, { eSendEvent(eOnLoadNote, {
item: note 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/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { Item } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme"; import { useThemeColors } from "@notesnook/theme";
import React from "react"; import React from "react";
import { View } from "react-native"; import { View } from "react-native";
import useIsSelected from "../../../hooks/use-selected"; import useIsSelected from "../../../hooks/use-selected";
import { useEditorStore } from "../../../stores/use-editor-store"; import { useTabStore } from "../../../screens/editor/tiptap/use-tab-store";
import { Item } from "@notesnook/core";
export const Filler = ({ item, color }: { item: Item; color?: string }) => { export const Filler = ({ item, color }: { item: Item; color?: string }) => {
const { colors } = useThemeColors(); const { colors } = useThemeColors();
const isEditingNote = useEditorStore( const isEditingNote = useTabStore(
(state) => state.currentEditingNote === item.id (state) =>
state.tabs.find((t) => t.id === state.currentTab)?.noteId === item.id
); );
const [selected] = useIsSelected(item); const [selected] = useIsSelected(item);
@@ -41,8 +42,8 @@ export const Filler = ({ item, color }: { item: Item; color?: string }) => {
backgroundColor: colors.selected.background, backgroundColor: colors.selected.background,
borderLeftWidth: 5, borderLeftWidth: 5,
borderLeftColor: isEditingNote borderLeftColor: isEditingNote
? item.color ? color
? colors.static[item.color] ? color
: colors.selected.accent : colors.selected.accent
: "transparent" : "transparent"
}} }}

View File

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

View File

@@ -22,10 +22,10 @@ import React, { useEffect, useState } from "react";
import { View } from "react-native"; import { View } from "react-native";
import { db } from "../../common/database"; import { db } from "../../common/database";
import Editor from "../../screens/editor"; import Editor from "../../screens/editor";
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
import { editorController } from "../../screens/editor/tiptap/utils"; import { editorController } from "../../screens/editor/tiptap/utils";
import { eSendEvent, ToastManager } from "../../services/event-manager"; import { eSendEvent, ToastManager } from "../../services/event-manager";
import Navigation from "../../services/navigation"; import Navigation from "../../services/navigation";
import { useEditorStore } from "../../stores/use-editor-store";
import { useSelectionStore } from "../../stores/use-selection-store"; import { useSelectionStore } from "../../stores/use-selection-store";
import { useTrashStore } from "../../stores/use-trash-store"; import { useTrashStore } from "../../stores/use-trash-store";
import { eCloseSheet, eOnLoadNote } from "../../utils/events"; import { eCloseSheet, eOnLoadNote } from "../../utils/events";
@@ -58,10 +58,11 @@ export default function NotePreview({ session, content, note }) {
return; return;
} }
await db.noteHistory.restore(session.id); await db.noteHistory.restore(session.id);
if (useEditorStore.getState()?.currentEditingNote === session?.noteId) { if (useTabStore.getState().hasTabForNote(session?.noteId)) {
if (editorController.current?.note) { const note = editorController.current.note.current[session?.noteId];
if (note) {
eSendEvent(eOnLoadNote, { eSendEvent(eOnLoadNote, {
item: editorController.current?.note, item: note,
forced: true 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 { RelationsList } from "../components/sheets/relations-list/index";
import ReminderSheet from "../components/sheets/reminder"; import ReminderSheet from "../components/sheets/reminder";
import { useSideBarDraggingStore } from "../components/side-menu/dragging-store"; import { useSideBarDraggingStore } from "../components/side-menu/dragging-store";
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
import { import {
ToastManager, ToastManager,
eSendEvent, eSendEvent,
@@ -52,7 +53,6 @@ import {
} from "../services/event-manager"; } from "../services/event-manager";
import Navigation from "../services/navigation"; import Navigation from "../services/navigation";
import Notifications from "../services/notifications"; import Notifications from "../services/notifications";
import { useEditorStore } from "../stores/use-editor-store";
import { useMenuStore } from "../stores/use-menu-store"; import { useMenuStore } from "../stores/use-menu-store";
import useNavigationStore from "../stores/use-navigation-store"; import useNavigationStore from "../stores/use-navigation-store";
import { useRelationStore } from "../stores/use-relation-store"; import { useRelationStore } from "../stores/use-relation-store";
@@ -528,8 +528,12 @@ export const useActions = ({
const currentReadOnly = (item as Note).readonly; const currentReadOnly = (item as Note).readonly;
await db.notes.readonly(!currentReadOnly, item?.id); await db.notes.readonly(!currentReadOnly, item?.id);
if (useEditorStore.getState().currentEditingNote === item.id) { if (useTabStore.getState().hasTabForNote(item.id)) {
useEditorStore.getState().setReadonly(!currentReadOnly); const tabId = useTabStore.getState().getTabForNote(item.id);
if (!tabId) return;
useTabStore.getState().updateTab(tabId, {
readonly: !currentReadOnly
});
} }
Navigation.queueRoutesForUpdate(); Navigation.queueRoutesForUpdate();
close(); close();

View File

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

View File

@@ -46,6 +46,7 @@ import { FluidTabs } from "../components/tabs";
import useGlobalSafeAreaInsets from "../hooks/use-global-safe-area-insets"; import useGlobalSafeAreaInsets from "../hooks/use-global-safe-area-insets";
import { useShortcutManager } from "../hooks/use-shortcut-manager"; import { useShortcutManager } from "../hooks/use-shortcut-manager";
import { hideAllTooltips } from "../hooks/use-tooltip"; import { hideAllTooltips } from "../hooks/use-tooltip";
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
import { import {
clearAppState, clearAppState,
editorController, editorController,
@@ -59,7 +60,6 @@ import {
eSubscribeEvent, eSubscribeEvent,
eUnSubscribeEvent eUnSubscribeEvent
} from "../services/event-manager"; } from "../services/event-manager";
import { useEditorStore } from "../stores/use-editor-store";
import { useSettingStore } from "../stores/use-setting-store"; import { useSettingStore } from "../stores/use-setting-store";
import { import {
eClearEditor, eClearEditor,
@@ -285,7 +285,7 @@ const _TabsHolder = () => {
case "mobile": case "mobile":
if ( if (
!editorState().movedAway && !editorState().movedAway &&
useEditorStore.getState().currentEditingNote useTabStore.getState().getCurrentNoteId()
) { ) {
tabBarRef.current?.goToIndex(2, false); tabBarRef.current?.goToIndex(2, false);
} else { } else {
@@ -511,6 +511,7 @@ const onChangeTab = async (obj) => {
editorState().movedAway = false; editorState().movedAway = false;
editorState().isFocused = true; editorState().isFocused = true;
activateKeepAwake(); activateKeepAwake();
console.log(editorState().currentlyEditing, "currentlyEditing...");
if (!editorState().currentlyEditing) { if (!editorState().currentlyEditing) {
eSendEvent(eOnLoadNote, { eSendEvent(eOnLoadNote, {
newNote: true newNote: true
@@ -522,12 +523,13 @@ const onChangeTab = async (obj) => {
editorState().movedAway = true; editorState().movedAway = true;
editorState().isFocused = false; editorState().isFocused = false;
eSendEvent(eClearEditor, "removeHandler"); eSendEvent(eClearEditor, "removeHandler");
setTimeout(() => useEditorStore.getState().setSearchReplace(false), 1); let id = useTabStore.getState().getCurrentNoteId();
let id = useEditorStore.getState().currentEditingNote; let note = await db.notes.note(id);
let note = db.notes.note(id);
const locked = note && (await db.vaults.itemExists(note)); const locked = note && (await db.vaults.itemExists(note));
if (locked) { 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 { IconButton } from "../../components/ui/icon-button";
import useKeyboard from "../../hooks/use-keyboard"; import useKeyboard from "../../hooks/use-keyboard";
import { eSubscribeEvent } from "../../services/event-manager"; import { eSubscribeEvent } from "../../services/event-manager";
import { useEditorStore } from "../../stores/use-editor-store";
import { getElevationStyle } from "../../utils/elevation"; import { getElevationStyle } from "../../utils/elevation";
import { openLinkInBrowser } from "../../utils/functions"; import { openLinkInBrowser } from "../../utils/functions";
import EditorOverlay from "./loading"; import EditorOverlay from "./loading";
@@ -43,6 +42,7 @@ import { EDITOR_URI } from "./source";
import { EditorProps, useEditorType } from "./tiptap/types"; import { EditorProps, useEditorType } from "./tiptap/types";
import { useEditor } from "./tiptap/use-editor"; import { useEditor } from "./tiptap/use-editor";
import { useEditorEvents } from "./tiptap/use-editor-events"; import { useEditorEvents } from "./tiptap/use-editor-events";
import { useTabStore } from "./tiptap/use-tab-store";
import { editorController } from "./tiptap/utils"; import { editorController } from "./tiptap/utils";
const style: ViewStyle = { const style: ViewStyle = {
@@ -173,15 +173,24 @@ const Editor = React.memo(
export default Editor; export default Editor;
const ReadonlyButton = ({ editor }: { editor: useEditorType }) => { 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 keyboard = useKeyboard();
const { colors } = useThemeColors(); const { colors } = useThemeColors();
const onPress = async () => { const onPress = async () => {
if (editor.note.current) { const noteId = useTabStore
await db.notes.readonly(false, editor.note.current.id); .getState()
editor.note.current = await db.notes?.note(editor.note.current.id); .getNoteIdForTab(useTabStore.getState().currentTab);
useEditorStore.getState().setReadonly(false); 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 { ProgressBarComponent } from "../../components/ui/svg/lazy";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets"; import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { useAttachmentStore } from "../../stores/use-attachment-store"; import { useAttachmentStore } from "../../stores/use-attachment-store";
import { useEditorStore } from "../../stores/use-editor-store"; import { useTabStore } from "./tiptap/use-tab-store";
export const ProgressBar = () => { export const ProgressBar = () => {
const { colors } = useThemeColors(); const { colors } = useThemeColors();
const currentlyEditingNote = useEditorStore( const currentlyEditingNote = useTabStore((state) => state.getCurrentNoteId());
(state) => state.currentEditingNote
);
const downloading = useAttachmentStore((state) => state.downloading); const downloading = useAttachmentStore((state) => state.downloading);
const loading = currentlyEditingNote 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 * The url should be something like this: http://192.168.100.126:3000/index.html
*/ */
export const EDITOR_URI = __DEV__ export const EDITOR_URI = __DEV__
? EditorMobileSourceUrl ? "http://192.168.43.252:3000/index.html"
: EditorMobileSourceUrl; : EditorMobileSourceUrl;

View File

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

View File

@@ -37,5 +37,8 @@ export const EventTypes = {
reminders: "editor-event:reminders", reminders: "editor-event:reminders",
previewAttachment: "editor-event:preview-attachment", previewAttachment: "editor-event:preview-attachment",
copyToClipboard: "editor-events:copy-to-clipboard", 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 { FILE_SIZE_LIMIT, IMAGE_SIZE_LIMIT } from "../../../utils/constants";
import { eCloseSheet } from "../../../utils/events"; import { eCloseSheet } from "../../../utils/events";
import { editorController, editorState } from "./utils"; 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) => { const showEncryptionSheet = (file) => {
presentSheet({ presentSheet({
@@ -52,6 +55,15 @@ const santizeUri = (uri) => {
return uri; return uri;
}; };
/**
* @param {{
* noteId: string,
* tabId: string,
* type: "image" | "camera" | "file"
* reupload: boolean
* hash?: string
* }} fileOptions
*/
const file = async (fileOptions) => { const file = async (fileOptions) => {
try { try {
const options = { const options = {
@@ -104,22 +116,33 @@ const file = async (fileOptions) => {
if (!(await attachFile(uri, hash, file.type, file.name, fileOptions))) if (!(await attachFile(uri, hash, file.type, file.name, fileOptions)))
return; return;
if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri); if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri);
if (isImage(file.type)) {
editorController.current?.commands.insertImage({ if (
hash: hash, useTabStore.getState().getNoteIdForTab(options.tabId) === options.noteId
filename: file.name, ) {
mime: file.type, if (isImage(file.type)) {
size: file.size, editorController.current?.commands.insertImage(
dataurl: await db.attachments.read(hash, "base64"), {
title: file.name hash: hash,
}); filename: file.name,
} else { mime: file.type,
editorController.current?.commands.insertAttachment({ size: file.size,
hash: hash, dataurl: await db.attachments.read(hash, "base64"),
filename: file.name, title: file.name
mime: file.type, },
size: file.size fileOptions.tabId
}); );
} else {
editorController.current?.commands.insertAttachment(
{
hash: hash,
filename: file.name,
mime: file.type,
size: file.size
},
fileOptions.tabId
);
}
} }
setTimeout(() => { 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) => { const camera = async (options) => {
try { try {
await db.attachments.generateKey(); 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) => { const pick = async (options) => {
if (!PremiumService.get()) { if (!PremiumService.get()) {
let user = await db.user.getUser(); let user = await db.user.getUser();
@@ -253,17 +296,40 @@ const handleImageResponse = async (response, options) => {
if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri); if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri);
editorController.current?.commands.insertImage({ if (
hash: hash, useTabStore.getState().getNoteIdForTab(options.tabId) === options.noteId
mime: image.type, ) {
title: fileName, editorController.current?.commands.insertImage(
dataurl: b64, {
size: image.fileSize, hash: hash,
filename: fileName 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) { export async function attachFile(uri, hash, type, filename, options) {
try { try {
let exists = db.attachments.exists(hash); let exists = db.attachments.exists(hash);
@@ -299,10 +365,7 @@ export async function attachFile(uri, hash, type, filename, options) {
} else { } else {
encryptionInfo = { hash: hash }; encryptionInfo = { hash: hash };
} }
await db.attachments.add( await db.attachments.add(encryptionInfo, options.noteId);
encryptionInfo,
editorController.current?.note?.id
);
return true; return true;
} catch (e) { } 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; onChange?: (html: string) => void;
}; };
export type EditorMessage = { export type EditorMessage<T> = {
sessionId: string; sessionId: string;
value: unknown; value: T;
type: string; type: string;
noteId: string;
tabId: number;
}; };
export type SavePayload = { export type SavePayload = {
@@ -77,9 +79,9 @@ export type SavePayload = {
id?: string; id?: string;
data?: string; data?: string;
type?: "tiptap"; type?: "tiptap";
sessionId?: string | null;
sessionHistoryId?: number; sessionHistoryId?: number;
ignoreEdit: boolean; ignoreEdit: boolean;
tabId: number;
}; };
export type AppState = { export type AppState = {

View File

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

View File

@@ -47,14 +47,15 @@ import Navigation from "../../../services/navigation";
import Notifications from "../../../services/notifications"; import Notifications from "../../../services/notifications";
import SettingsService from "../../../services/settings"; import SettingsService from "../../../services/settings";
import { TipManager } from "../../../services/tip-manager"; import { TipManager } from "../../../services/tip-manager";
import { useEditorStore } from "../../../stores/use-editor-store";
import { useSettingStore } from "../../../stores/use-setting-store"; import { useSettingStore } from "../../../stores/use-setting-store";
import { useTagStore } from "../../../stores/use-tag-store"; import { useTagStore } from "../../../stores/use-tag-store";
import { eClearEditor, eOnLoadNote } from "../../../utils/events"; import { eClearEditor, eOnLoadNote } from "../../../utils/events";
import { tabBarRef } from "../../../utils/global-refs"; import { tabBarRef } from "../../../utils/global-refs";
import { onNoteCreated } from "../../notes/common"; import { onNoteCreated } from "../../notes/common";
import Commands from "./commands"; import Commands from "./commands";
import { SessionHistory } from "./session-history";
import { EditorState, SavePayload } from "./types"; import { EditorState, SavePayload } from "./types";
import { useTabStore } from "./use-tab-store";
import { import {
EditorEvents, EditorEvents,
clearAppState, clearAppState,
@@ -62,32 +63,50 @@ import {
getAppState, getAppState,
isContentInvalid, isContentInvalid,
isEditorLoaded, isEditorLoaded,
makeSessionId,
post post
} from "./utils"; } 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 = ( export const useEditor = (
editorId = "", editorId = "",
readonly?: boolean, readonly?: boolean,
onChange?: (html: string) => void onChange?: (html: string) => void
) => { ) => {
const theme = useThemeEngineStore((state) => state.theme); const theme = useThemeEngineStore((state) => state.theme);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const sessionIdRef = useRef(makeSessionId()); const sessionIdRef = useRef("notesnook-editor");
const editorRef = useRef<WebView>(null); const editorRef = useRef<WebView>(null);
const currentNote = useRef<
| (Note & { const currentNotes = useRef<
content?: NoteContent<false> & { Record<
isPreview?: boolean; string,
}; | (Note & {
}) content?: NoteContent<false> & {
| null isPreview?: boolean;
>(); };
const currentContent = useRef<Partial<UnencryptedContentItem> | null>(); })
| null
| undefined
>
>({});
const currentContents = useRef<
Record<string, Partial<UnencryptedContentItem> | null>
>({});
const timers = useRef<{ [name: string]: NodeJS.Timeout }>({}); const timers = useRef<{ [name: string]: NodeJS.Timeout }>({});
const commands = useMemo(() => new Commands(editorRef), [editorRef]); const commands = useMemo(() => new Commands(editorRef), [editorRef]);
const sessionHistoryId = useRef<number>(); const editorSessionHistory = useMemo(() => new SessionHistory(), []);
const state = useRef<Partial<EditorState>>(defaultState); const state = useRef<Partial<EditorState>>(defaultState);
const placeholderTip = useRef(TipManager.placeholderTip()); const placeholderTip = useRef(TipManager.placeholderTip());
const tags = useTagStore((state) => state.items); const tags = useTagStore((state) => state.items);
@@ -96,11 +115,18 @@ export const useEditor = (
const saveCount = useRef(0); const saveCount = useRef(0);
const lastContentChangeTime = useRef<number>(0); const lastContentChangeTime = useRef<number>(0);
const lock = useRef(false); const lock = useRef(false);
const lockedSessionId = useRef<string>(); const currentLoadingNoteId = useRef<string>();
const loadingState = useRef<string>(); const loadingState = useRef<string>();
const postMessage = useCallback( const postMessage = useCallback(
async <T>(type: string, data: T, waitFor = 300) => async <T>(type: string, data: T, tabId?: number, waitFor = 300) =>
await post(editorRef, sessionIdRef.current, type, data, waitFor), await post(
editorRef,
sessionIdRef.current,
typeof tabId !== "number" ? useTabStore.getState().currentTab : tabId,
type,
data,
waitFor
),
[sessionIdRef] [sessionIdRef]
); );
@@ -115,14 +141,16 @@ export const useEditor = (
}, [theme, postMessage]); }, [theme, postMessage]);
useEffect(() => { useEffect(() => {
commands.setTags(currentNote.current); for (const id in currentNotes.current) {
commands.setTags(currentNotes.current[id]);
}
}, [commands, tags]); }, [commands, tags]);
const overlay = useCallback( const overlay = useCallback(
(show: boolean, data = { type: "new" }) => { (show: boolean, data = { type: "new" }) => {
eSendEvent( eSendEvent(
"loadingNote" + editorId, "loadingNote" + editorId,
show ? data || currentNote.current : false show ? data || currentNotes.current : false
); );
}, },
[editorId] [editorId]
@@ -144,32 +172,31 @@ export const useEditor = (
); );
const reset = useCallback( const reset = useCallback(
async (resetState = true, resetContent = true) => { async (tabId: number, resetState = true, resetContent = true) => {
currentNote.current?.id && db.fs().cancel(currentNote.current.id); const noteId = useTabStore.getState().getNoteIdForTab(tabId);
currentNote.current = null; if (noteId) {
currentContent.current = null; currentNotes.current?.id && db.fs().cancel(noteId, "download");
sessionHistoryId.current = undefined;
currentNotes.current[noteId] = null;
currentContents.current[noteId] = null;
editorSessionHistory.clearSession(noteId);
}
clearTimeout(timers.current["loading-images" + noteId]);
saveCount.current = 0; saveCount.current = 0;
loadingState.current = undefined; loadingState.current = undefined;
lock.current = false; lock.current = false;
useEditorStore.getState().setReadonly(false); resetContent && postMessage(EditorEvents.title, "", tabId);
resetContent && postMessage(EditorEvents.title, "");
lastContentChangeTime.current = 0; lastContentChangeTime.current = 0;
resetContent && (await commands.clearContent()); resetContent && (await commands.clearContent(tabId));
resetContent && (await commands.clearTags()); resetContent && (await commands.clearTags(tabId));
if (resetState) { useTabStore.getState().updateTab(tabId, {
const newSessionId = makeSessionId(); noteId: undefined
sessionIdRef.current = newSessionId; });
await commands.setSessionId(newSessionId);
isDefaultEditor &&
useEditorStore.getState().setCurrentlyEditingNote(null);
placeholderTip.current = TipManager.placeholderTip();
await commands.setPlaceholder(placeholderTip.current);
}
}, },
[commands, isDefaultEditor, postMessage] [commands, editorSessionHistory, postMessage]
); );
const saveNote = useCallback( const saveNote = useCallback(
@@ -178,32 +205,24 @@ export const useEditor = (
id, id,
data, data,
type, type,
sessionId: currentSessionId, ignoreEdit,
sessionHistoryId: currentSessionHistoryId, sessionHistoryId: currentSessionHistoryId,
ignoreEdit tabId
}: SavePayload) => { }: SavePayload) => {
if ( if (currentNotes.current[id as string]?.readonly) return;
readonly ||
useEditorStore.getState().readonly ||
currentNote.current?.readonly
)
return;
try { try {
if (id && !(await db.notes?.note(id))) { if (id && !(await db.notes?.note(id))) {
isDefaultEditor && await reset(tabId);
useEditorStore.getState().setCurrentlyEditingNote(null);
await reset();
return; return;
} }
let note = id ? await db.notes?.note(id) : undefined; let note = id ? await db.notes?.note(id) : undefined;
const locked = note && (await db.vaults.itemExists(note)); const locked = note && (await db.vaults.itemExists(note));
if (note?.conflicted) return; if (note?.conflicted) return;
if (isContentInvalid(data)) { if (isContentInvalid(data) && id) {
// Create a new history session if recieved empty or invalid content // Create a new history session if recieved empty or invalid content
// To ensure that history is preserved for correct content. // To ensure that history is preserved for correct content.
sessionHistoryId.current = Date.now(); currentSessionHistoryId = editorSessionHistory.newSession(id);
currentSessionHistoryId = sessionHistoryId.current;
} }
const noteData: Partial<Note> & { const noteData: Partial<Note> & {
@@ -211,9 +230,7 @@ export const useEditor = (
content?: NoteContent<false>; content?: NoteContent<false>;
} = { } = {
id, id,
sessionId: isContentInvalid(data) sessionId: `${currentSessionHistoryId}`
? undefined
: (currentSessionHistoryId as any)
}; };
noteData.title = title; noteData.title = title;
@@ -229,10 +246,25 @@ export const useEditor = (
type: type as ContentType type: type as ContentType
}; };
} }
// If note is edited, the tab becomes a persistent tab automatically.
useTabStore.getState().updateTab(tabId, {
previewTab: false
});
if (!locked) { if (!locked) {
id = await db.notes?.add(noteData); id = await db.notes?.add(noteData);
if (!note && id) { 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(); const defaultNotebook = db.settings.getDefaultNotebook();
if (!state.current.onNoteCreated && defaultNotebook) { if (!state.current.onNoteCreated && defaultNotebook) {
onNoteCreated(id, { onNoteCreated(id, {
@@ -244,25 +276,14 @@ export const useEditor = (
} }
if (!noteData.title) { 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)) { if (Notifications.isNotePinned(id as string)) {
Notifications.pinNote(id as string); Notifications.pinNote(id as string);
} }
@@ -271,19 +292,21 @@ export const useEditor = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
await db.vault?.save(noteData as 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; note = (await db.notes?.note(id)) as Note;
await commands.setStatus( await commands.setStatus(
getFormattedDate(note.dateEdited, "date-time"), getFormattedDate(note.dateEdited, "date-time"),
"Saved" "Saved",
tabId
); );
lastContentChangeTime.current = note.dateEdited; lastContentChangeTime.current = note.dateEdited;
if ( if (
saveCount.current < 2 || saveCount.current < 2 ||
currentNote.current?.title !== note.title || currentNotes.current[id]?.title !== note.title ||
currentNote.current?.headline?.slice(0, 200) !== currentNotes.current[id]?.headline?.slice(0, 200) !==
note.headline?.slice(0, 200) note.headline?.slice(0, 200)
) { ) {
Navigation.queueRoutesForUpdate(); Navigation.queueRoutesForUpdate();
@@ -297,7 +320,7 @@ export const useEditor = (
DatabaseLogger.error(e as Error); DatabaseLogger.error(e as Error);
} }
}, },
[commands, isDefaultEditor, postMessage, readonly, reset] [commands, editorSessionHistory, postMessage, reset]
); );
const loadContent = useCallback( const loadContent = useCallback(
@@ -306,13 +329,13 @@ export const useEditor = (
content?: NoteContent<false>; content?: NoteContent<false>;
} }
) => { ) => {
currentNote.current = note; currentNotes.current[note.id] = note;
const locked = note && (await db.vaults.itemExists(note)); const locked = note && (await db.vaults.itemExists(note));
if ((locked || note.content) && note.content?.data) { if ((locked || note.content) && note.content?.data) {
currentContent.current = { currentContents.current[note.id] = {
data: note.content?.data, data: note.content?.data,
type: note.content?.type || "tiptap", type: note.content?.type || "tiptap",
noteId: currentNote.current?.id as string noteId: note.id
}; };
} else if (note.contentId) { } else if (note.contentId) {
const rawContent = await db.content?.get(note.contentId); const rawContent = await db.content?.get(note.contentId);
@@ -321,7 +344,7 @@ export const useEditor = (
!isDeleted(rawContent) && !isDeleted(rawContent) &&
isUnencryptedContent(rawContent) isUnencryptedContent(rawContent)
) { ) {
currentContent.current = { currentContents.current[note.id] = {
data: rawContent.data, data: rawContent.data,
type: rawContent.type type: rawContent.type
}; };
@@ -334,93 +357,105 @@ export const useEditor = (
const loadNote = useCallback( const loadNote = useCallback(
async (event: { item?: Note; forced?: boolean; newNote?: boolean }) => { async (event: { item?: Note; forced?: boolean; newNote?: boolean }) => {
state.current.currentlyEditing = true; state.current.currentlyEditing = true;
const editorState = useEditorStore.getState();
if ( if (
!state.current.ready && !state.current.ready &&
(await isEditorLoaded(editorRef, sessionIdRef.current)) (await isEditorLoaded(
editorRef,
sessionIdRef.current,
useTabStore.getState().currentTab
))
) { ) {
state.current.ready = true; state.current.ready = true;
} }
if (event.newNote) { if (event.newNote) {
currentNote.current && (await reset()); useTabStore.getState().focusEmptyTab();
const nextSessionId = makeSessionId(event.item?.id); const tabId = useTabStore.getState().currentTab;
sessionIdRef.current = nextSessionId; currentNotes.current && (await reset(tabId));
sessionHistoryId.current = Date.now(); setTimeout(() => {
await commands.setSessionId(nextSessionId); if (state.current?.ready) commands.focus(tabId);
if (state.current?.ready) await commands.focus(); lastContentChangeTime.current = 0;
lastContentChangeTime.current = 0; });
useEditorStore.getState().setReadonly(false);
} else { } else {
if (!event.item) return; if (!event.item) return;
const item = event.item; 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.movedAway = false;
state.current.currentlyEditing = true; state.current.currentlyEditing = true;
if (currentNote.current?.id !== item.id) { if (!currentNotes.current[item.id]) {
currentNote.current && (await reset(false, false)); // Reset current tab if note isn't already loaded.
isDefaultEditor && editorState.setCurrentlyEditingNote(item.id); currentNotes.current && (await reset(tabId, false, false));
} }
await loadContent(item); await loadContent(item);
if ( if (
currentNote.current?.id === item.id && currentNotes.current[item.id] &&
loadingState.current && loadingState.current &&
currentContent.current?.data && currentContents.current?.data &&
loadingState.current === currentContent.current?.data loadingState.current === currentContents.current?.data
) { ) {
return; return;
} }
if (
!currentContent.current?.data ||
currentContent.current?.data.length < 50000
) {
if (state.current.ready) overlay(false);
} else {
overlay(true);
}
if (!state.current.ready) { if (!state.current.ready) {
currentNote.current = item; currentNotes.current[item.id] = item;
return; return;
} }
lastContentChangeTime.current = item.dateEdited; lastContentChangeTime.current = item.dateEdited;
const nextSessionId = makeSessionId(item.id); currentLoadingNoteId.current = item.id;
sessionIdRef.current = nextSessionId; currentNotes.current[item.id] = item;
lockedSessionId.current = nextSessionId;
sessionHistoryId.current = Date.now(); if (!currentNotes.current[item.id]) return;
await commands.setSessionId(nextSessionId);
currentNote.current = item; editorSessionHistory.newSession(item.id);
await commands.setStatus( await commands.setStatus(
getFormattedDate(item.dateEdited, "date-time"), getFormattedDate(item.dateEdited, "date-time"),
"Saved" "Saved",
); tabId
await postMessage(EditorEvents.title, item.title);
loadingState.current = currentContent.current?.data;
await postMessage(
EditorEvents.html,
currentContent.current?.data || "",
10000
); );
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; loadingState.current = undefined;
useEditorStore.getState().setReadonly(item.readonly); await commands.setTags(item);
await commands.setTags(currentNote.current);
commands.setSettings(); commands.setSettings();
setTimeout(() => { setTimeout(() => {
if (lockedSessionId.current === nextSessionId) { if (currentLoadingNoteId.current === event.item?.id) {
lockedSessionId.current = undefined; currentLoadingNoteId.current = undefined;
} }
}, 300); }, 300);
overlay(false);
} }
}, },
[commands, isDefaultEditor, loadContent, overlay, postMessage, reset] [commands, editorSessionHistory, loadContent, postMessage, reset]
); );
const lockNoteWithVault = useCallback((note: Note) => { const lockNoteWithVault = useCallback((note: Note) => {
@@ -441,55 +476,56 @@ export const useEditor = (
if (!data) return; if (!data) return;
const noteId = data.type === "tiptap" ? data.noteId : data.id; 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 = const isContentEncrypted =
typeof (data as ContentItem)?.data === "object"; 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; if (lastContentChangeTime.current >= (data as Note).dateEdited) return;
lock.current = true; lock.current = true;
if (data.type === "tiptap" && note) { 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) { if (!locked && isContentEncrypted) {
lockNoteWithVault(note); lockNoteWithVault(note);
} else if (locked && isEncryptedContent(data)) { } else if (locked && isEncryptedContent(data)) {
const decryptedContent = await db.vault?.decryptContent( const decryptedContent = await db.vault?.decryptContent(data, noteId);
data,
currentNote?.current?.id
);
if (!decryptedContent) { if (!decryptedContent) {
lockNoteWithVault(note); lockNoteWithVault(note);
} else { } else {
await postMessage(EditorEvents.updatehtml, decryptedContent.data); await postMessage(
currentContent.current = decryptedContent; EditorEvents.updatehtml,
decryptedContent.data,
tabId
);
currentContents.current[note.id] = decryptedContent;
} }
} else { } else {
const _nextContent = data.data; const _nextContent = data.data;
if (_nextContent === currentContent.current?.data) return; if (_nextContent === currentContents.current?.data) return;
lastContentChangeTime.current = note.dateEdited; lastContentChangeTime.current = note.dateEdited;
await postMessage(EditorEvents.updatehtml, _nextContent); await postMessage(EditorEvents.updatehtml, _nextContent, tabId);
if (!isEncryptedContent(data)) { if (!isEncryptedContent(data)) {
currentContent.current = data as UnencryptedContentItem; currentContents.current[note.id] = data as UnencryptedContentItem;
} }
} }
} else { } else {
if (data.type !== "note") return; if (!note) return;
const note = data; postMessage(EditorEvents.title, note.title, tabId);
if (note.title !== currentNote.current.title) { commands.setTags(note);
postMessage(EditorEvents.title, note.title); commands.setStatus(
}
if (note.tags !== currentNote.current.tags) {
await commands.setTags(note);
}
await commands.setStatus(
getFormattedDate(note.dateEdited, "date-time"), getFormattedDate(note.dateEdited, "date-time"),
"Saved" "Saved",
tabId as number
); );
} }
lock.current = false; lock.current = false;
}, },
[lockNoteWithVault, postMessage, commands] [lockNoteWithVault, postMessage, commands]
@@ -513,55 +549,44 @@ export const useEditor = (
content, content,
type, type,
forSessionId, forSessionId,
ignoreEdit ignoreEdit,
noteId,
tabId
}: { }: {
noteId?: string;
title?: string; title?: string;
content?: string; content?: string;
type: string; type: string;
forSessionId: string; forSessionId: string;
ignoreEdit: boolean; ignoreEdit: boolean;
tabId: number;
}) => { }) => {
if (lock.current || lockedSessionId.current === forSessionId) return; if (lock.current || currentLoadingNoteId.current === noteId) return;
lastContentChangeTime.current = Date.now(); lastContentChangeTime.current = Date.now();
if ( if (type === EditorEvents.content && noteId) {
sessionHistoryId.current && currentContents.current[noteId as string] = {
Date.now() - sessionHistoryId.current > 5 * 60 * 1000
) {
sessionHistoryId.current = Date.now();
}
if (type === EditorEvents.content) {
currentContent.current = {
data: content, data: content,
type: "tiptap", 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 = { const params: SavePayload = {
title, title,
data: content, data: content,
type: "tiptap", type: "tiptap",
sessionId: forSessionId,
id: noteId, id: noteId,
sessionHistoryId: sessionHistoryId.current, ignoreEdit,
ignoreEdit sessionHistoryId: noteId ? editorSessionHistory.get(noteId) : undefined,
tabId: tabId
}; };
withTimer( withTimer(
noteId || "newnote", noteId || "newnote",
() => { () => {
if ( if (!params.id) {
currentNote.current && params.id = useTabStore.getState().getNoteIdForTab(tabId);
!params.id &&
params.sessionId === forSessionId
) {
params.id = currentNote.current?.id;
} }
if (onChange && params.data) { if (onChange && params.data) {
onChange(params.data); onChange(params.data);
@@ -572,13 +597,12 @@ export const useEditor = (
ignoreEdit ? 0 : 150 ignoreEdit ? 0 : 150
); );
}, },
[withTimer, onChange, saveNote] [editorSessionHistory, withTimer, onChange, saveNote]
); );
const restoreEditorState = useCallback(async () => { const restoreEditorState = useCallback(async () => {
const appState = getAppState(); const appState = getAppState();
if (!appState) return; if (!appState) return;
overlay(true, appState.note);
state.current.isRestoringState = true; state.current.isRestoringState = true;
state.current.currentlyEditing = true; state.current.currentlyEditing = true;
state.current.movedAway = false; state.current.movedAway = false;
@@ -604,7 +628,7 @@ export const useEditor = (
} }
clearAppState(); clearAppState();
state.current.isRestoringState = false; state.current.isRestoringState = false;
}, [loadNote, overlay]); }, [loadNote]);
useEffect(() => { useEffect(() => {
eSubscribeEvent(eOnLoadNote + editorId, loadNote); eSubscribeEvent(eOnLoadNote + editorId, loadNote);
@@ -622,7 +646,13 @@ export const useEditor = (
}, [loading]); }, [loading]);
const onReady = useCallback(async () => { const onReady = useCallback(async () => {
if (!(await isEditorLoaded(editorRef, sessionIdRef.current))) { if (
!(await isEditorLoaded(
editorRef,
sessionIdRef.current,
useTabStore.getState().currentTab
))
) {
eSendEvent("webview_reset", "onReady"); eSendEvent("webview_reset", "onReady");
return false; return false;
} else { } else {
@@ -632,23 +662,24 @@ export const useEditor = (
}, [isDefaultEditor, restoreEditorState]); }, [isDefaultEditor, restoreEditorState]);
const onLoad = useCallback(async () => { const onLoad = useCallback(async () => {
if (currentNote.current) overlay(true); if (currentNotes.current) overlay(true);
clearTimeout(timers.current["editor:loaded"]); clearTimeout(timers.current["editor:loaded"]);
timers.current["editor:loaded"] = setTimeout(async () => { timers.current["editor:loaded"] = setTimeout(async () => {
postMessage(EditorEvents.theme, theme); postMessage(EditorEvents.theme, theme);
commands.setInsets( commands.setInsets(
isDefaultEditor ? insets : { top: 0, left: 0, right: 0, bottom: 0 } isDefaultEditor ? insets : { top: 0, left: 0, right: 0, bottom: 0 }
); );
await commands.setSessionId(sessionIdRef.current);
await commands.setSettings(); await commands.setSettings();
timers.current["editor:loaded"] = setTimeout(async () => { timers.current["editor:loaded"] = setTimeout(async () => {
if (!state.current.ready && (await onReady())) { if (!state.current.ready && (await onReady())) {
state.current.ready = true; state.current.ready = true;
} }
if (currentNote.current) { overlay(false);
loadNote({ ...currentNote.current, forced: true });
} else { const noteId = useTabStore.getState().getCurrentNoteId();
await commands.setPlaceholder(placeholderTip.current); if (noteId && currentNotes.current[noteId]) {
loadNote({ ...currentNotes.current[noteId], forced: true });
} }
}); });
}); });
@@ -672,7 +703,7 @@ export const useEditor = (
setLoading, setLoading,
state, state,
sessionId: sessionIdRef, sessionId: sessionIdRef,
note: currentNote, note: currentNotes,
onReady, onReady,
saveContent, saveContent,
onContentChanged, 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( export async function isEditorLoaded(
ref: RefObject<WebView>, 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>( export async function post<T>(
ref: RefObject<WebView>, ref: RefObject<WebView>,
sessionId: string, sessionId: string,
tabId: number,
type: string, type: string,
value: T | null = null, value: T | null = null,
waitFor = 300 waitFor = 300
@@ -84,7 +86,8 @@ export async function post<T>(
const message = { const message = {
type, type,
value, value,
sessionId: sessionId sessionId: sessionId,
tabId
}; };
setImmediate(() => ref.current?.postMessage(JSON.stringify(message))); setImmediate(() => ref.current?.postMessage(JSON.stringify(message)));
const response = await getResponse(type, waitFor); 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 { eOnLoadNote, eOnNotebookUpdated } from "../../utils/events";
import { openLinkInBrowser } from "../../utils/functions"; import { openLinkInBrowser } from "../../utils/functions";
import { tabBarRef } from "../../utils/global-refs"; import { tabBarRef } from "../../utils/global-refs";
import { editorController, editorState } from "../editor/tiptap/utils"; import { editorState } from "../editor/tiptap/utils";
export const PLACEHOLDER_DATA = { export const PLACEHOLDER_DATA = {
title: "Your notes", title: "Your notes",
@@ -52,11 +52,9 @@ export function openMonographsWebpage() {
export function openEditor() { export function openEditor() {
if (!DDS.isTab) { if (!DDS.isTab) {
if (editorController.current?.note) { eSendEvent(eOnLoadNote, { newNote: true });
eSendEvent(eOnLoadNote, { newNote: true }); editorState().currentlyEditing = true;
editorState().currentlyEditing = true; editorState().movedAway = false;
editorState().movedAway = false;
}
tabBarRef.current?.goToPage(1); tabBarRef.current?.goToPage(1);
} else { } else {
eSendEvent(eOnLoadNote, { newNote: true }); 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 create from "zustand";
import { editorController } from "../screens/editor/tiptap/utils"; import { editorController } from "../screens/editor/tiptap/utils";
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
export type AttachmentGroupProgress = { export type AttachmentGroupProgress = {
total: number; total: number;
@@ -66,10 +67,13 @@ export const useAttachmentStore = create<AttachmentStore>((set, get) => ({
remove: (hash) => { remove: (hash) => {
const progress = get().progress; const progress = get().progress;
if (!progress) return; if (!progress) return;
editorController.current?.commands.setAttachmentProgress({ editorController.current?.commands.setAttachmentProgress(
hash: hash, {
progress: 100 hash: hash,
}); progress: 100
},
useTabStore.getState().currentTab
);
progress[hash] = null; progress[hash] = null;
set({ progress: { ...progress } }); set({ progress: { ...progress } });
}, },
@@ -80,11 +84,14 @@ export const useAttachmentStore = create<AttachmentStore>((set, get) => ({
const progressPercentage = const progressPercentage =
type === "upload" ? sent / total : recieved / total; type === "upload" ? sent / total : recieved / total;
editorController.current?.commands.setAttachmentProgress({ editorController.current?.commands.setAttachmentProgress(
hash: hash, {
//@ts-ignore hash: hash,
progress: Math.round(Math.max(progressPercentage * 100, 0)) //@ts-ignore
}); progress: Math.round(Math.max(progressPercentage * 100, 0))
},
useTabStore.getState().currentTab
);
set({ progress: { ...progress } }); set({ progress: { ...progress } });
}, },
encryptionProgress: 0, 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 create, { State } from "zustand";
import { eSubscribeEvent, eUnSubscribeEvent } from "../services/event-manager";
export interface EditorStore extends State { export interface EditorStore extends State {
currentEditingNote: string | null; currentEditingNote: string | null;
setCurrentlyEditingNote: (note: string | null) => void; setCurrentlyEditingNote: (note: string | null) => void;
sessionId: string | null; sessionId: string | null;
setSessionId: (sessionId: string | null) => void; setSessionId: (sessionId: string | null) => void;
searchReplace: boolean;
setSearchReplace: (searchReplace: boolean) => void;
searchSelection: string | null; searchSelection: string | null;
readonly: boolean; readonly: boolean;
setReadonly: (readonly: boolean) => void; setReadonly: (readonly: boolean) => void;
@@ -39,34 +36,9 @@ export const useEditorStore = create<EditorStore>((set, get) => ({
setSessionId: (sessionId) => { setSessionId: (sessionId) => {
set({ sessionId }); set({ sessionId });
}, },
searchReplace: false,
searchSelection: null, searchSelection: null,
readonly: false, readonly: false,
setReadonly: (readonly) => { setReadonly: (readonly) => {
set({ readonly: 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", "mdi-react": "9.1.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-freeze": "^1.0.3",
"tinycolor2": "1.6.0",
"zustand": "^4.4.7" "zustand": "^4.4.7"
}, },
"devDependencies": { "devDependencies": {
@@ -4358,7 +4360,7 @@
"version": "15.7.11", "version": "15.7.11",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==",
"dev": true "devOptional": true
}, },
"node_modules/@types/q": { "node_modules/@types/q": {
"version": "1.5.8", "version": "1.5.8",
@@ -4382,7 +4384,7 @@
"version": "18.2.39", "version": "18.2.39",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.39.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.39.tgz",
"integrity": "sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA==", "integrity": "sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA==",
"dev": true, "devOptional": true,
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
"@types/scheduler": "*", "@types/scheduler": "*",
@@ -4417,7 +4419,7 @@
"version": "0.16.8", "version": "0.16.8",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
"dev": true "devOptional": true
}, },
"node_modules/@types/semver": { "node_modules/@types/semver": {
"version": "7.5.6", "version": "7.5.6",
@@ -9713,7 +9715,7 @@
"version": "9.0.21", "version": "9.0.21",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
"dev": true, "devOptional": true,
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
"url": "https://opencollective.com/immer" "url": "https://opencollective.com/immer"
@@ -15627,6 +15629,17 @@
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==",
"dev": true "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": { "node_modules/react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -17496,6 +17509,11 @@
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
"dev": true "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": { "node_modules/tmpl": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@@ -17758,6 +17776,20 @@
"is-typedarray": "^1.0.0" "is-typedarray": "^1.0.0"
} }
}, },
"node_modules/typescript": {
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/unbox-primitive": { "node_modules/unbox-primitive": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",

View File

@@ -13,6 +13,8 @@
"mdi-react": "9.1.0", "mdi-react": "9.1.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-freeze": "^1.0.3",
"tinycolor2": "1.6.0",
"zustand": "^4.4.7" "zustand": "^4.4.7"
}, },
"devDependencies": { "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/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { Global, css } from "@emotion/react";
import { import {
ScopedThemeProvider, ScopedThemeProvider,
themeToCSS, themeToCSS,
useThemeEngineStore useThemeEngineStore
} from "@notesnook/theme"; } from "@notesnook/theme";
import { useEffect, useMemo } from "react";
import { Freeze } from "react-freeze";
import "./App.css"; import "./App.css";
import Tiptap from "./components/editor"; import Tiptap from "./components/editor";
import { TabContext, useTabStore } from "./hooks/useTabStore";
import { EmotionEditorTheme } from "./theme-factory"; import { EmotionEditorTheme } from "./theme-factory";
import { Global, css } from "@emotion/react"; import { EventTypes, getTheme } from "./utils";
import { useMemo } from "react";
import { getTheme } from "./utils";
const currentTheme = getTheme(); const currentTheme = getTheme();
if (currentTheme) { if (currentTheme) {
@@ -35,11 +37,29 @@ if (currentTheme) {
} }
function App(): JSX.Element { 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 ( return (
<ScopedThemeProvider value="base"> <ScopedThemeProvider value="base">
<EmotionEditorTheme> <EmotionEditorTheme>
<GlobalStyles /> <GlobalStyles />
<Tiptap /> {tabs.map((tab) => (
<TabContext.Provider key={tab.id} value={tab}>
<Freeze freeze={currentTab !== tab.id}>
<Tiptap />
</Freeze>
</TabContext.Provider>
))}
</EmotionEditorTheme> </EmotionEditorTheme>
</ScopedThemeProvider> </ScopedThemeProvider>
); );

View File

@@ -25,6 +25,7 @@ import {
usePermissionHandler, usePermissionHandler,
useTiptap useTiptap
} from "@notesnook/editor"; } from "@notesnook/editor";
import { toBlobURL } from "@notesnook/editor/dist/utils/downloader";
import { useThemeColors } from "@notesnook/theme"; import { useThemeColors } from "@notesnook/theme";
import { import {
forwardRef, forwardRef,
@@ -36,17 +37,19 @@ import {
} from "react"; } from "react";
import { useEditorController } from "../hooks/useEditorController"; import { useEditorController } from "../hooks/useEditorController";
import { useSettings } from "../hooks/useSettings"; import { useSettings } from "../hooks/useSettings";
import { TabStore, useTabContext, useTabStore } from "../hooks/useTabStore";
import { EmotionEditorToolbarTheme } from "../theme-factory"; import { EmotionEditorToolbarTheme } from "../theme-factory";
import { EventTypes, Settings } from "../utils"; import { EventTypes, Settings } from "../utils";
import Header from "./header"; import Header from "./header";
import StatusBar from "./statusbar"; import StatusBar from "./statusbar";
import Tags from "./tags"; import Tags from "./tags";
import Title from "./title"; import Title from "./title";
import { toBlobURL } from "@notesnook/editor/dist/utils/downloader";
globalThis.toBlobURL = toBlobURL as typeof globalThis.toBlobURL; globalThis.toBlobURL = toBlobURL as typeof globalThis.toBlobURL;
const Tiptap = ({ settings }: { settings: Settings }) => { const Tiptap = ({ settings }: { settings: Settings }) => {
const tab = useTabContext();
const isFocused = useTabStore((state) => state.currentTab === tab?.id);
const [tick, setTick] = useState(0); const [tick, setTick] = useState(0);
const contentRef = useRef<HTMLDivElement>(null); const contentRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
@@ -56,45 +59,48 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
premium: settings.premium premium: settings.premium
}, },
onPermissionDenied: () => { onPermissionDenied: () => {
post(EventTypes.pro); post(EventTypes.pro, undefined, tab.id, tab.noteId);
} }
}); });
const _editor = useTiptap( const _editor = useTiptap(
{ {
onUpdate: ({ editor, transaction }) => { onUpdate: ({ editor, transaction }) => {
global.editorController.contentChange( globalThis.editorControllers[tab.id]?.contentChange(
editor as Editor, editor as Editor,
transaction.getMeta("ignoreEdit") transaction.getMeta("ignoreEdit")
); );
}, },
onOpenAttachmentPicker: (editor, type) => { onOpenAttachmentPicker: (editor, type) => {
global.editorController.openFilePicker(type); globalThis.editorControllers[tab.id]?.openFilePicker(type);
return true; return true;
}, },
onDownloadAttachment: (editor, attachment) => { onDownloadAttachment: (editor, attachment) => {
global.editorController.downloadAttachment(attachment); globalThis.editorControllers[tab.id]?.downloadAttachment(attachment);
return true; return true;
}, },
onPreviewAttachment(editor, attachment) { onPreviewAttachment(editor, attachment) {
global.editorController.previewAttachment(attachment); globalThis.editorControllers[tab.id]?.previewAttachment(attachment);
return true; return true;
}, },
getAttachmentData(attachment) { getAttachmentData(attachment) {
return global.editorController.getAttachmentData(attachment); return globalThis.editorControllers[tab.id]?.getAttachmentData(
attachment
) as Promise<string | undefined>;
}, },
element: !layout ? undefined : contentRef.current || undefined, element: !layout ? undefined : contentRef.current || undefined,
editable: !settings.readonly, editable: !settings.readonly,
editorProps: { editorProps: {
editable: () => !settings.readonly editable: () => !settings.readonly
}, },
content: global.editorController?.content?.current, content: globalThis.editorControllers[tab.id]?.content?.current,
isMobile: true, isMobile: true,
doubleSpacedLines: settings.doubleSpacedLines, doubleSpacedLines: settings.doubleSpacedLines,
onOpenLink: (url) => { onOpenLink: (url) => {
return global.editorController.openLink(url); return globalThis.editorControllers[tab.id]?.openLink(url) || true;
}, },
copyToClipboard: (text) => { copyToClipboard: (text) => {
globalThis.editorController.copyToClipboard(text); globalThis.editorControllers[tab.id]?.copyToClipboard(text);
}, },
downloadOptions: { downloadOptions: {
corsHost: settings.corsProxy corsHost: settings.corsProxy
@@ -118,17 +124,45 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
left: 0, left: 0,
top: 0 top: 0
}); });
globalThis.editorController.setTitlePlaceholder("Note title"); globalThis.editorControllers[tab.id]?.setTitlePlaceholder("Note title");
}, []); }, [tab.id]);
const controller = useEditorController(update); const controller = useEditorController(update);
const controllerRef = useRef(controller); const controllerRef = useRef(controller);
globalThis.editorController = controller;
globalThis.editor = _editor; globalThis.editorControllers[tab.id] = controller;
globalThis.editors[tab.id] = _editor;
useLayoutEffect(() => { useLayoutEffect(() => {
setLayout(true); 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( const onClickEmptyArea: React.MouseEventHandler<HTMLDivElement> = useCallback(
(event) => { (event) => {
@@ -149,15 +183,17 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
return; 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 isParagraph = firstChild?.type.name === "paragraph";
const isFirstChildEmpty = const isFirstChildEmpty =
!firstChild?.textContent || firstChild?.textContent?.length === 0; !firstChild?.textContent || firstChild?.textContent?.length === 0;
if (isParagraph && isFirstChildEmpty) { if (isParagraph && isFirstChildEmpty) {
globalThis.editor?.commands.focus("end"); editor?.commands.focus("end");
return; return;
} }
globalThis.editor editor
?.chain() ?.chain()
.insertContentAt(0, "<p></p>", { .insertContentAt(0, "<p></p>", {
updateSelection: true updateSelection: true
@@ -166,33 +202,35 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
.run(); .run();
} }
}, },
[] [tab.id]
); );
const onClickBottomArea = useCallback(() => { 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; if (!docSize) return;
const lastChild = globalThis.editor?.state.doc.lastChild; const lastChild = editor?.state.doc.lastChild;
const isParagraph = lastChild?.type.name === "paragraph"; const isParagraph = lastChild?.type.name === "paragraph";
const isLastChildEmpty = const isLastChildEmpty =
!lastChild?.textContent || lastChild?.textContent?.length === 0; !lastChild?.textContent || lastChild?.textContent?.length === 0;
if (isParagraph && isLastChildEmpty) { if (isParagraph && isLastChildEmpty) {
globalThis.editor?.commands.focus("end"); editor?.commands.focus("end");
return; return;
} }
globalThis.editor editor
?.chain() ?.chain()
.insertContentAt(docSize - 1, "<p></p>", { .insertContentAt(docSize - 1, "<p></p>", {
updateSelection: true updateSelection: true
}) })
.focus("end") .focus("end")
.run(); .run();
}, []); }, [tab.id]);
return ( return (
<> <>
<div <div
style={{ style={{
display: "flex", display: isFocused ? "flex" : "none",
flex: 1, flex: 1,
flexDirection: "column", flexDirection: "column",
maxWidth: "100vw" 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 ArrowBackIcon from "mdi-react/ArrowBackIcon";
import CrownIcon from "mdi-react/CrownIcon";
import DotsHorizontalIcon from "mdi-react/DotsHorizontalIcon";
import ArrowULeftTopIcon from "mdi-react/ArrowULeftTopIcon"; import ArrowULeftTopIcon from "mdi-react/ArrowULeftTopIcon";
import ArrowURightTopIcon from "mdi-react/ArrowURightTopIcon"; import ArrowURightTopIcon from "mdi-react/ArrowURightTopIcon";
import CrownIcon from "mdi-react/CrownIcon";
import DotsHorizontalIcon from "mdi-react/DotsHorizontalIcon";
import FullscreenIcon from "mdi-react/FullscreenIcon"; import FullscreenIcon from "mdi-react/FullscreenIcon";
import MagnifyIcon from "mdi-react/MagnifyIcon"; import MagnifyIcon from "mdi-react/MagnifyIcon";
import React from "react"; import React from "react";
import { useSafeArea } from "../hooks/useSafeArea"; import { useSafeArea } from "../hooks/useSafeArea";
import { useTabContext, useTabStore } from "../hooks/useTabStore";
import { EventTypes, Settings } from "../utils"; import { EventTypes, Settings } from "../utils";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
@@ -65,7 +66,11 @@ function Header({
hasUndo: boolean; hasUndo: boolean;
hasRedo: boolean; hasRedo: boolean;
}): JSX.Element { }): JSX.Element {
const tab = useTabContext();
const editor = editors[tab.id];
const insets = useSafeArea(); const insets = useSafeArea();
const openedTabsCount = useTabStore((state) => state.tabs.length);
return ( return (
<div <div
style={{ style={{
@@ -95,7 +100,7 @@ function Header({
) : ( ) : (
<Button <Button
onPress={() => { onPress={() => {
post(EventTypes.back); post(EventTypes.back, undefined, tab.id, tab.noteId);
}} }}
preventDefault={false} preventDefault={false}
style={{ style={{
@@ -113,7 +118,7 @@ function Header({
}} }}
> >
<ArrowBackIcon <ArrowBackIcon
size={27 * settings.fontScale} size={28 * settings.fontScale}
style={{ style={{
position: "absolute" position: "absolute"
}} }}
@@ -235,7 +240,7 @@ function Header({
}} }}
> >
<MagnifyIcon <MagnifyIcon
size={25 * settings.fontScale} size={28 * settings.fontScale}
style={{ style={{
position: "absolute" position: "absolute"
}} }}
@@ -246,7 +251,7 @@ function Header({
{settings.deviceMode !== "mobile" && !settings.fullscreen ? ( {settings.deviceMode !== "mobile" && !settings.fullscreen ? (
<Button <Button
onPress={() => { onPress={() => {
post(EventTypes.fullscreen); post(EventTypes.fullscreen, undefined, tab.id, tab.noteId);
}} }}
preventDefault={false} preventDefault={false}
style={{ style={{
@@ -274,7 +279,46 @@ function Header({
<Button <Button
onPress={() => { 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} preventDefault={false}
style={{ 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 React, { RefObject, useEffect, useRef, useState } from "react";
import { getTotalWords, Editor } from "@notesnook/editor"; import { getTotalWords, Editor } from "@notesnook/editor";
import { useTabContext } from "../hooks/useTabStore";
function StatusBar({ container }: { container: RefObject<HTMLDivElement> }) { function StatusBar({ container }: { container: RefObject<HTMLDivElement> }) {
const [status, setStatus] = useState({ const [status, setStatus] = useState({
date: "", date: "",
saved: "" saved: ""
}); });
const tab = useTabContext();
const [sticky, setSticky] = useState(false); const [sticky, setSticky] = useState(false);
const stickyRef = useRef(false); const stickyRef = useRef(false);
const prevScroll = useRef(0); const prevScroll = useRef(0);
@@ -34,6 +36,7 @@ function StatusBar({ container }: { container: RefObject<HTMLDivElement> }) {
const statusBar = useRef({ const statusBar = useRef({
set: setStatus, set: setStatus,
updateWords: () => { updateWords: () => {
const editor = editors[tab.id];
const words = getTotalWords(editor as Editor) + " words"; const words = getTotalWords(editor as Editor) + " words";
if (currentWords.current === words) return; if (currentWords.current === words) return;
setWords(words); setWords(words);
@@ -43,7 +46,13 @@ function StatusBar({ container }: { container: RefObject<HTMLDivElement> }) {
setWords(`0 words`); 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 onScroll = React.useCallback((event: Event) => {
const currentOffset = (event.target as HTMLElement)?.scrollTop; 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/>. 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 { EventTypes, Settings } from "../utils";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
import { useTabContext } from "../hooks/useTabStore";
function Tags(props: { settings: Settings }): JSX.Element { function Tags(props: { settings: Settings }): JSX.Element {
const [tags, setTags] = useState< const [tags, setTags] = useState<
{ title: string; alias: string; id: string; type: "tag" }[] { title: string; alias: string; id: string; type: "tag" }[]
>([]); >([]);
const editorTags = useRef({ const tagsRef = useRef({
setTags: setTags 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 openManageTagsSheet = () => {
const editor = editors[tab.id];
if (editor?.isFocused) { if (editor?.isFocused) {
editor.commands.blur(); 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; const fontScale = props.settings?.fontScale || 1;
@@ -116,7 +124,7 @@ function Tags(props: { settings: Settings }): JSX.Element {
}} }}
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
post(EventTypes.tag, tag); post(EventTypes.tag, tag, tab.id, tab.noteId);
}} }}
> >
#{tag.alias} #{tag.alias}

View File

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

View File

@@ -32,6 +32,8 @@ import {
} from "react"; } from "react";
import { EventTypes, isReactNative, post, randId, saveTheme } from "../utils"; import { EventTypes, isReactNative, post, randId, saveTheme } from "../utils";
import { injectCss, transform } from "../utils/css"; import { injectCss, transform } from "../utils/css";
import { useTabContext, useTabStore } from "./useTabStore";
type Attachment = { type Attachment = {
hash: string; hash: string;
filename: string; filename: string;
@@ -105,6 +107,7 @@ export type EditorController = {
}; };
export function useEditorController(update: () => void): EditorController { export function useEditorController(update: () => void): EditorController {
const tab = useTabContext();
const setTheme = useThemeEngineStore((store) => store.setTheme); const setTheme = useThemeEngineStore((store) => store.setTheme);
const { colors } = useThemeColors("editor"); const { colors } = useThemeColors("editor");
const [title, setTitle] = useState(""); const [title, setTitle] = useState("");
@@ -118,20 +121,26 @@ export function useEditorController(update: () => void): EditorController {
const selectionChange = useCallback((_editor: Editor) => {}, []); const selectionChange = useCallback((_editor: Editor) => {}, []);
const titleChange = useCallback((title: string) => { const titleChange = useCallback(
post(EventTypes.contentchange); (title: string) => {
post(EventTypes.title, title); 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) => { const countWords = useCallback(
if (typeof timers.current.wordCounter === "number") (ms = 300) => {
clearTimeout(timers.current.wordCounter); if (typeof timers.current.wordCounter === "number")
timers.current.wordCounter = setTimeout(() => { clearTimeout(timers.current.wordCounter);
console.time("wordCounter"); timers.current.wordCounter = setTimeout(() => {
statusBar?.current?.updateWords(); console.time("wordCounter");
console.timeEnd("wordCounter"); statusBars[tab.id]?.current?.updateWords();
}, ms); console.timeEnd("wordCounter");
}, []); }, ms);
},
[tab.id]
);
useEffect(() => { useEffect(() => {
injectCss(transform(colors)); injectCss(transform(colors));
@@ -145,29 +154,33 @@ export function useEditorController(update: () => void): EditorController {
if (typeof timers.current.change === "number") { if (typeof timers.current.change === "number") {
clearTimeout(timers.current?.change); clearTimeout(timers.current?.change);
} }
timers.current.change = setTimeout( timers.current.change = setTimeout(() => {
() => { htmlContentRef.current = editor.getHTML();
htmlContentRef.current = editor.getHTML(); post(
post( EventTypes.content,
EventTypes.content, {
{ html: htmlContentRef.current,
html: htmlContentRef.current, ignoreEdit: ignoreEdit
ignoreEdit: ignoreEdit },
}, tab.id,
currentSessionId tab.noteId,
); currentSessionId
}, );
ignoreEdit ? 0 : 300 }, 300);
);
countWords(5000); countWords(5000);
}, },
[countWords] [countWords, tab.id, tab.noteId]
); );
const scroll = useCallback( 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(() => { const onUpdate = useCallback(() => {
@@ -177,19 +190,34 @@ export function useEditorController(update: () => void): EditorController {
const onMessage = useCallback( const onMessage = useCallback(
(event: Event & { data?: string }) => { (event: Event & { data?: string }) => {
if (event?.data?.[0] !== "{") return; if (event?.data?.[0] !== "{") return;
const message = JSON.parse(event.data); const message = JSON.parse(event.data);
const type = message.type; const type = message.type;
const value = message.value; 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) { switch (type) {
case "native:updatehtml": { case "native:updatehtml": {
htmlContentRef.current = value; htmlContentRef.current = value;
if (!editor) break; if (!editor) break;
const { from, to } = editor.state.selection; const { from, to } = editor.state.selection;
editor?.commands.setContent(htmlContentRef.current, false, { editor?.commands.setContent(htmlContentRef.current, false, {
preserveWhitespace: true preserveWhitespace: true
}); });
editor.commands.setTextSelection({ editor.commands.setTextSelection({
from, from,
to to
@@ -198,6 +226,7 @@ export function useEditorController(update: () => void): EditorController {
break; break;
} }
case "native:html": case "native:html":
// logger("info", "loading html", htmlContentRef.current);
htmlContentRef.current = value; htmlContentRef.current = value;
update(); update();
countWords(); countWords();
@@ -232,7 +261,7 @@ export function useEditorController(update: () => void): EditorController {
} }
post(type); // Notify that message was delivered successfully. post(type); // Notify that message was delivered successfully.
}, },
[update, countWords, setTheme] [tab, update, countWords, setTheme]
); );
useEffect(() => { useEffect(() => {
@@ -249,20 +278,32 @@ export function useEditorController(update: () => void): EditorController {
}; };
}, [onMessage]); }, [onMessage]);
const openFilePicker = useCallback((type: "image" | "file" | "camera") => { const openFilePicker = useCallback(
post(EventTypes.filepicker, type); (type: "image" | "file" | "camera") => {
}, []); post(EventTypes.filepicker, type, tab.id, tab.noteId);
},
[tab.id, tab.noteId]
);
const downloadAttachment = useCallback((attachment: Attachment) => { const downloadAttachment = useCallback(
post(EventTypes.download, attachment); (attachment: Attachment) => {
}, []); post(EventTypes.download, attachment, tab.id, tab.noteId);
const previewAttachment = useCallback((attachment: Attachment) => { },
post(EventTypes.previewAttachment, attachment); [tab.id, tab.noteId]
}, []); );
const openLink = useCallback((url: string) => { const previewAttachment = useCallback(
post(EventTypes.link, url); (attachment: Attachment) => {
return true; 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) => { const copyToClipboard = (text: string) => {
post(EventTypes.copyToClipboard, text); 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/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { ToolbarGroupDefinition } from "@notesnook/editor"; import { Editor, ToolbarGroupDefinition } from "@notesnook/editor";
import { Editor } from "@notesnook/editor";
import { Dispatch, MutableRefObject, RefObject, SetStateAction } from "react";
import { useEditorController } from "../hooks/useEditorController";
import { ThemeDefinition } from "@notesnook/theme"; import { ThemeDefinition } from "@notesnook/theme";
import { Dispatch, MutableRefObject, RefObject, SetStateAction } from "react";
import { EditorController } from "../hooks/useEditorController";
globalThis.sessionId = "notesnook-editor";
globalThis.pendingResolvers = {}; globalThis.pendingResolvers = {};
export function randId(prefix: string) { export function randId(prefix: string) {
@@ -60,15 +61,20 @@ declare global {
var pendingResolvers: { var pendingResolvers: {
[key: string]: (value: any) => void; [key: string]: (value: any) => void;
}; };
var statusBar: React.MutableRefObject<{ var statusBars: Record<
set: React.Dispatch< number,
React.SetStateAction<{ | React.MutableRefObject<{
date: string; set: React.Dispatch<
saved: string; React.SetStateAction<{
date: string;
saved: string;
}>
>;
updateWords: () => void;
resetWords: () => void;
}> }>
>; | undefined
updateWords: () => void; >;
}>;
var __PLATFORM__: "ios" | "android"; var __PLATFORM__: "ios" | "android";
var readonly: boolean; var readonly: boolean;
var noToolbar: boolean; var noToolbar: boolean;
@@ -78,14 +84,16 @@ declare global {
* Id of current session * Id of current session
*/ */
var sessionId: string; 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: { var settingsController: {
update: (settings: Settings) => void; 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. * Global ref to manage tags in editor.
*/ */
var editorTags: MutableRefObject<{ var editorTags: Record<
setTags: React.Dispatch< number,
React.SetStateAction< | MutableRefObject<{
{ title: string; alias: string; id: string; type: "tag" }[] setTags: React.Dispatch<
> React.SetStateAction<
>; { title: string; alias: string; id: string; type: "tag" }[]
}>; >
>;
}>
| undefined
>;
function logger(type: "info" | "warn" | "error", ...logs: unknown[]): void; function logger(type: "info" | "warn" | "error", ...logs: unknown[]): void;
/** /**
@@ -134,7 +146,10 @@ declare global {
function post<T extends keyof typeof EventTypes>( function post<T extends keyof typeof EventTypes>(
type: (typeof EventTypes)[T], type: (typeof EventTypes)[T],
value?: unknown value?: unknown,
tabId?: number,
noteId?: string,
sessionId?: string
): void; ): void;
interface Window { interface Window {
/** /**
@@ -168,7 +183,10 @@ export const EventTypes = {
reminders: "editor-event:reminders", reminders: "editor-event:reminders",
previewAttachment: "editor-event:preview-attachment", previewAttachment: "editor-event:preview-attachment",
copyToClipboard: "editor-events:copy-to-clipboard", 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; } as const;
export function isReactNative(): boolean { export function isReactNative(): boolean {
@@ -191,6 +209,8 @@ export function logger(
export function post<T extends keyof typeof EventTypes>( export function post<T extends keyof typeof EventTypes>(
type: (typeof EventTypes)[T], type: (typeof EventTypes)[T],
value?: unknown, value?: unknown,
tabId?: number,
noteId?: string,
sessionId?: string sessionId?: string
): void { ): void {
if (isReactNative()) { if (isReactNative()) {
@@ -198,7 +218,9 @@ export function post<T extends keyof typeof EventTypes>(
JSON.stringify({ JSON.stringify({
type, type,
value: value, value: value,
sessionId: sessionId || globalThis.sessionId sessionId: sessionId || globalThis.sessionId,
tabId,
noteId
}) })
); );
} else { } else {