mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-16 19:57:52 +01:00
mobile: add editor tabs
This commit is contained in:
committed by
Abdullah Atta
parent
815c9eb7de
commit
3f23507a74
@@ -104,7 +104,7 @@ const DialogHeader = ({
|
||||
title={button.title}
|
||||
icon={button.icon}
|
||||
type={button.type || "secondary"}
|
||||
height={25}
|
||||
height={30}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
@@ -394,9 +394,10 @@ export class VaultDialog extends Component {
|
||||
return;
|
||||
} else {
|
||||
await db.vault.add(this.state.note.id);
|
||||
if (this.state.note.id === editorController.current?.note?.id) {
|
||||
eSendEvent(eClearEditor);
|
||||
}
|
||||
|
||||
// if (this.state.note.id === editorController.current?.note?.id) {
|
||||
// eSendEvent(eClearEditor, );
|
||||
// }
|
||||
this.close();
|
||||
ToastManager.show({
|
||||
message: "Note locked successfully",
|
||||
@@ -501,9 +502,10 @@ export class VaultDialog extends Component {
|
||||
}
|
||||
if (this.state.note?.id) {
|
||||
await db.vault.add(this.state.note.id);
|
||||
if (this.state.note.id === editorController.current?.note?.id) {
|
||||
eSendEvent(eClearEditor);
|
||||
}
|
||||
// TODO
|
||||
// if (this.state.note.id === editorController.current?.note?.id) {
|
||||
// eSendEvent(eClearEditor);
|
||||
// }
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
|
||||
@@ -33,11 +33,12 @@ import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
|
||||
import NotebookScreen from "../../../screens/notebook";
|
||||
import { TaggedNotes } from "../../../screens/notes/tagged";
|
||||
import { useEditorStore } from "../../../stores/use-editor-store";
|
||||
import useNavigationStore from "../../../stores/use-navigation-store";
|
||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
|
||||
import { useTabStore } from "../../../screens/editor/tiptap/use-tab-store";
|
||||
import { NotebooksWithDateEdited, TagsWithDateEdited } from "@notesnook/common";
|
||||
import { Properties } from "../../properties";
|
||||
import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
@@ -45,7 +46,6 @@ import { ReminderTime } from "../../ui/reminder-time";
|
||||
import { TimeSince } from "../../ui/time-since";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { NotebooksWithDateEdited, TagsWithDateEdited } from "@notesnook/common";
|
||||
|
||||
type NoteItemProps = {
|
||||
item: Note | BaseTrashItem<Note>;
|
||||
@@ -73,8 +73,9 @@ const NoteItem = ({
|
||||
locked,
|
||||
noOpen = false
|
||||
}: NoteItemProps) => {
|
||||
const isEditingNote = useEditorStore(
|
||||
(state) => state.currentEditingNote === item.id
|
||||
const isEditingNote = useTabStore(
|
||||
(state) =>
|
||||
state.tabs.find((t) => t.id === state.currentTab)?.noteId === item.id
|
||||
);
|
||||
const { colors } = useThemeColors();
|
||||
const compactMode = useIsCompactModeEnabled(
|
||||
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
openVault,
|
||||
presentSheet
|
||||
} from "../../../services/event-manager";
|
||||
import { useEditorStore } from "../../../stores/use-editor-store";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { eOnLoadNote, eShowMergeDialog } from "../../../utils/events";
|
||||
import { tabBarRef } from "../../../utils/global-refs";
|
||||
@@ -91,9 +90,6 @@ export const openNote = async (
|
||||
)
|
||||
});
|
||||
} else {
|
||||
if (note?.readonly) {
|
||||
useEditorStore.getState().setReadonly(note?.readonly);
|
||||
}
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: note
|
||||
});
|
||||
|
||||
@@ -17,17 +17,18 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Item } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import useIsSelected from "../../../hooks/use-selected";
|
||||
import { useEditorStore } from "../../../stores/use-editor-store";
|
||||
import { Item } from "@notesnook/core";
|
||||
import { useTabStore } from "../../../screens/editor/tiptap/use-tab-store";
|
||||
|
||||
export const Filler = ({ item, color }: { item: Item; color?: string }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const isEditingNote = useEditorStore(
|
||||
(state) => state.currentEditingNote === item.id
|
||||
const isEditingNote = useTabStore(
|
||||
(state) =>
|
||||
state.tabs.find((t) => t.id === state.currentTab)?.noteId === item.id
|
||||
);
|
||||
|
||||
const [selected] = useIsSelected(item);
|
||||
@@ -41,8 +42,8 @@ export const Filler = ({ item, color }: { item: Item; color?: string }) => {
|
||||
backgroundColor: colors.selected.background,
|
||||
borderLeftWidth: 5,
|
||||
borderLeftColor: isEditingNote
|
||||
? item.color
|
||||
? colors.static[item.color]
|
||||
? color
|
||||
? color
|
||||
: colors.selected.accent
|
||||
: "transparent"
|
||||
}}
|
||||
|
||||
@@ -47,6 +47,7 @@ import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
|
||||
|
||||
const MergeConflicts = () => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -88,10 +89,11 @@ const MergeConflicts = () => {
|
||||
});
|
||||
}
|
||||
Navigation.queueRoutesForUpdate();
|
||||
if (editorController.current?.note?.id === note.id) {
|
||||
|
||||
if (useTabStore.getState().getCurrentNoteId() === note.id) {
|
||||
// reload the note in editor
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: editorController.current?.note,
|
||||
item: editorController.current.note.current[note.id],
|
||||
forced: true
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,10 +22,10 @@ import React, { useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import Editor from "../../screens/editor";
|
||||
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
|
||||
import { editorController } from "../../screens/editor/tiptap/utils";
|
||||
import { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { useEditorStore } from "../../stores/use-editor-store";
|
||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
||||
import { useTrashStore } from "../../stores/use-trash-store";
|
||||
import { eCloseSheet, eOnLoadNote } from "../../utils/events";
|
||||
@@ -58,10 +58,11 @@ export default function NotePreview({ session, content, note }) {
|
||||
return;
|
||||
}
|
||||
await db.noteHistory.restore(session.id);
|
||||
if (useEditorStore.getState()?.currentEditingNote === session?.noteId) {
|
||||
if (editorController.current?.note) {
|
||||
if (useTabStore.getState().hasTabForNote(session?.noteId)) {
|
||||
const note = editorController.current.note.current[session?.noteId];
|
||||
if (note) {
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: editorController.current?.note,
|
||||
item: note,
|
||||
forced: true
|
||||
});
|
||||
}
|
||||
|
||||
154
apps/mobile/app/components/sheets/editor-tabs/index.tsx
Normal file
154
apps/mobile/app/components/sheets/editor-tabs/index.tsx
Normal 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} />
|
||||
});
|
||||
};
|
||||
@@ -43,6 +43,7 @@ import PublishNoteSheet from "../components/sheets/publish-note";
|
||||
import { RelationsList } from "../components/sheets/relations-list/index";
|
||||
import ReminderSheet from "../components/sheets/reminder";
|
||||
import { useSideBarDraggingStore } from "../components/side-menu/dragging-store";
|
||||
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
|
||||
import {
|
||||
ToastManager,
|
||||
eSendEvent,
|
||||
@@ -52,7 +53,6 @@ import {
|
||||
} from "../services/event-manager";
|
||||
import Navigation from "../services/navigation";
|
||||
import Notifications from "../services/notifications";
|
||||
import { useEditorStore } from "../stores/use-editor-store";
|
||||
import { useMenuStore } from "../stores/use-menu-store";
|
||||
import useNavigationStore from "../stores/use-navigation-store";
|
||||
import { useRelationStore } from "../stores/use-relation-store";
|
||||
@@ -528,8 +528,12 @@ export const useActions = ({
|
||||
const currentReadOnly = (item as Note).readonly;
|
||||
await db.notes.readonly(!currentReadOnly, item?.id);
|
||||
|
||||
if (useEditorStore.getState().currentEditingNote === item.id) {
|
||||
useEditorStore.getState().setReadonly(!currentReadOnly);
|
||||
if (useTabStore.getState().hasTabForNote(item.id)) {
|
||||
const tabId = useTabStore.getState().getTabForNote(item.id);
|
||||
if (!tabId) return;
|
||||
useTabStore.getState().updateTab(tabId, {
|
||||
readonly: !currentReadOnly
|
||||
});
|
||||
}
|
||||
Navigation.queueRoutesForUpdate();
|
||||
close();
|
||||
|
||||
@@ -53,6 +53,7 @@ import { MMKV } from "../common/database/mmkv";
|
||||
import Migrate from "../components/sheets/migrate";
|
||||
import NewFeature from "../components/sheets/new-feature";
|
||||
import { Walkthrough } from "../components/walkthroughs";
|
||||
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
|
||||
import {
|
||||
clearAppState,
|
||||
editorController,
|
||||
@@ -79,7 +80,6 @@ import SettingsService from "../services/settings";
|
||||
import Sync from "../services/sync";
|
||||
import { initAfterSync } from "../stores";
|
||||
import { useAttachmentStore } from "../stores/use-attachment-store";
|
||||
import { useEditorStore } from "../stores/use-editor-store";
|
||||
import { useMessageStore } from "../stores/use-message-store";
|
||||
import { useSettingStore } from "../stores/use-setting-store";
|
||||
import { SyncStatus, useUserStore } from "../stores/use-user-store";
|
||||
@@ -233,7 +233,7 @@ async function checkForShareExtensionLaunchedInBackground() {
|
||||
}
|
||||
|
||||
if (notesAddedFromIntent || shareExtensionOpened) {
|
||||
const id = useEditorStore.getState().currentEditingNote;
|
||||
const id = useTabStore.getState().getCurrentNoteId();
|
||||
const note = id && (await db.notes.note(id));
|
||||
eSendEvent("webview_reset");
|
||||
if (note) setTimeout(() => eSendEvent("loadingNote", note), 1);
|
||||
@@ -246,7 +246,7 @@ async function checkForShareExtensionLaunchedInBackground() {
|
||||
|
||||
async function saveEditorState() {
|
||||
if (editorState().currentlyEditing) {
|
||||
const id = useEditorStore.getState().currentEditingNote;
|
||||
const id = useTabStore.getState().getCurrentNoteId();
|
||||
const note = id ? await db.notes.note(id) : undefined;
|
||||
const locked = note && (await db.vaults.itemExists(note));
|
||||
if (locked) return;
|
||||
@@ -520,7 +520,7 @@ export const useAppEvents = () => {
|
||||
}
|
||||
} else {
|
||||
SettingsService.appEnteredBackground();
|
||||
const id = useEditorStore.getState().currentEditingNote;
|
||||
const id = useTabStore.getState().getCurrentNoteId();
|
||||
const note = id ? await db.notes.note(id) : undefined;
|
||||
const locked = note && (await db.vaults.itemExists(note));
|
||||
if (locked && SettingsService.canLockAppInBackground()) {
|
||||
@@ -538,7 +538,9 @@ export const useAppEvents = () => {
|
||||
}
|
||||
|
||||
if (Platform.OS === "ios") {
|
||||
editorController.current?.commands.blur();
|
||||
editorController.current?.commands.blur(
|
||||
useTabStore.getState().currentTab
|
||||
);
|
||||
Keyboard.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ import { FluidTabs } from "../components/tabs";
|
||||
import useGlobalSafeAreaInsets from "../hooks/use-global-safe-area-insets";
|
||||
import { useShortcutManager } from "../hooks/use-shortcut-manager";
|
||||
import { hideAllTooltips } from "../hooks/use-tooltip";
|
||||
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
|
||||
import {
|
||||
clearAppState,
|
||||
editorController,
|
||||
@@ -59,7 +60,6 @@ import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../services/event-manager";
|
||||
import { useEditorStore } from "../stores/use-editor-store";
|
||||
import { useSettingStore } from "../stores/use-setting-store";
|
||||
import {
|
||||
eClearEditor,
|
||||
@@ -285,7 +285,7 @@ const _TabsHolder = () => {
|
||||
case "mobile":
|
||||
if (
|
||||
!editorState().movedAway &&
|
||||
useEditorStore.getState().currentEditingNote
|
||||
useTabStore.getState().getCurrentNoteId()
|
||||
) {
|
||||
tabBarRef.current?.goToIndex(2, false);
|
||||
} else {
|
||||
@@ -511,6 +511,7 @@ const onChangeTab = async (obj) => {
|
||||
editorState().movedAway = false;
|
||||
editorState().isFocused = true;
|
||||
activateKeepAwake();
|
||||
console.log(editorState().currentlyEditing, "currentlyEditing...");
|
||||
if (!editorState().currentlyEditing) {
|
||||
eSendEvent(eOnLoadNote, {
|
||||
newNote: true
|
||||
@@ -522,12 +523,13 @@ const onChangeTab = async (obj) => {
|
||||
editorState().movedAway = true;
|
||||
editorState().isFocused = false;
|
||||
eSendEvent(eClearEditor, "removeHandler");
|
||||
setTimeout(() => useEditorStore.getState().setSearchReplace(false), 1);
|
||||
let id = useEditorStore.getState().currentEditingNote;
|
||||
let note = db.notes.note(id);
|
||||
let id = useTabStore.getState().getCurrentNoteId();
|
||||
let note = await db.notes.note(id);
|
||||
const locked = note && (await db.vaults.itemExists(note));
|
||||
if (locked) {
|
||||
eSendEvent(eClearEditor);
|
||||
useTabStore.getState().updateTab(useTabStore.getState().currentTab, {
|
||||
locked: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ import { db } from "../../common/database";
|
||||
import { IconButton } from "../../components/ui/icon-button";
|
||||
import useKeyboard from "../../hooks/use-keyboard";
|
||||
import { eSubscribeEvent } from "../../services/event-manager";
|
||||
import { useEditorStore } from "../../stores/use-editor-store";
|
||||
import { getElevationStyle } from "../../utils/elevation";
|
||||
import { openLinkInBrowser } from "../../utils/functions";
|
||||
import EditorOverlay from "./loading";
|
||||
@@ -43,6 +42,7 @@ import { EDITOR_URI } from "./source";
|
||||
import { EditorProps, useEditorType } from "./tiptap/types";
|
||||
import { useEditor } from "./tiptap/use-editor";
|
||||
import { useEditorEvents } from "./tiptap/use-editor-events";
|
||||
import { useTabStore } from "./tiptap/use-tab-store";
|
||||
import { editorController } from "./tiptap/utils";
|
||||
|
||||
const style: ViewStyle = {
|
||||
@@ -173,15 +173,24 @@ const Editor = React.memo(
|
||||
export default Editor;
|
||||
|
||||
const ReadonlyButton = ({ editor }: { editor: useEditorType }) => {
|
||||
const readonly = useEditorStore((state) => state.readonly);
|
||||
const readonly = useTabStore(
|
||||
(state) => state.tabs.find((t) => t.id === state.currentTab)?.readonly
|
||||
);
|
||||
|
||||
const keyboard = useKeyboard();
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
const onPress = async () => {
|
||||
if (editor.note.current) {
|
||||
await db.notes.readonly(false, editor.note.current.id);
|
||||
editor.note.current = await db.notes?.note(editor.note.current.id);
|
||||
useEditorStore.getState().setReadonly(false);
|
||||
const noteId = useTabStore
|
||||
.getState()
|
||||
.getNoteIdForTab(useTabStore.getState().currentTab);
|
||||
if (noteId) {
|
||||
await db.notes.readonly(!editor.note.current.readonly, noteId);
|
||||
editor.note.current[noteId] = await db.notes?.note(noteId);
|
||||
|
||||
useTabStore.getState().updateTab(useTabStore.getState().currentTab, {
|
||||
readonly: editor.note.current[noteId as string]?.readonly
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -23,13 +23,11 @@ import { View } from "react-native";
|
||||
import { ProgressBarComponent } from "../../components/ui/svg/lazy";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { useEditorStore } from "../../stores/use-editor-store";
|
||||
import { useTabStore } from "./tiptap/use-tab-store";
|
||||
|
||||
export const ProgressBar = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const currentlyEditingNote = useEditorStore(
|
||||
(state) => state.currentEditingNote
|
||||
);
|
||||
const currentlyEditingNote = useTabStore((state) => state.getCurrentNoteId());
|
||||
const downloading = useAttachmentStore((state) => state.downloading);
|
||||
|
||||
const loading = currentlyEditingNote
|
||||
|
||||
@@ -28,5 +28,5 @@ const EditorMobileSourceUrl =
|
||||
* The url should be something like this: http://192.168.100.126:3000/index.html
|
||||
*/
|
||||
export const EDITOR_URI = __DEV__
|
||||
? EditorMobileSourceUrl
|
||||
? "http://192.168.43.252:3000/index.html"
|
||||
: EditorMobileSourceUrl;
|
||||
|
||||
@@ -28,6 +28,7 @@ import { sleep } from "../../../utils/time";
|
||||
import { Settings } from "./types";
|
||||
import { getResponse, randId, textInput } from "./utils";
|
||||
import { Note } from "@notesnook/core/dist/types";
|
||||
import { useTabStore } from "./use-tab-store";
|
||||
|
||||
type Action = { job: string; id: string };
|
||||
|
||||
@@ -41,7 +42,7 @@ async function call(webview: RefObject<WebView | undefined>, action?: Action) {
|
||||
return response ? response.value : response;
|
||||
}
|
||||
|
||||
const fn = (fn: string) => {
|
||||
const fn = (fn: string, name?: string) => {
|
||||
const id = randId("fn_");
|
||||
return {
|
||||
job: `(async () => {
|
||||
@@ -52,7 +53,7 @@ const fn = (fn: string) => {
|
||||
post("${id}",response);
|
||||
} catch(e) {
|
||||
const DEV_MODE = ${__DEV__};
|
||||
if (DEV_MODE && typeof logger !== "undefined") logger('error', "webview: ", e.message, e.stack);
|
||||
if (DEV_MODE && typeof logger !== "undefined") logger('error', "webview: ", e.message, e.stack, "${name}");
|
||||
}
|
||||
return true;
|
||||
})();true;`,
|
||||
@@ -68,67 +69,87 @@ class Commands {
|
||||
this.previousSettings = null;
|
||||
}
|
||||
|
||||
async doAsync<T>(job: string) {
|
||||
async doAsync<T>(job: string, name?: string) {
|
||||
if (!this.ref.current) return false;
|
||||
return call(this.ref, fn(job)) as Promise<T>;
|
||||
return call(this.ref, fn(job, name)) as Promise<T>;
|
||||
}
|
||||
|
||||
focus = async () => {
|
||||
focus = async (tabId: number) => {
|
||||
console.log("focus");
|
||||
if (!this.ref.current) return;
|
||||
if (Platform.OS === "android") {
|
||||
//this.ref.current?.requestFocus();
|
||||
setTimeout(async () => {
|
||||
if (!this.ref) return;
|
||||
textInput.current?.focus();
|
||||
await this.doAsync("editor.commands.focus()");
|
||||
await this.doAsync(`editors[${tabId}]?.commands.focus()`, "focus");
|
||||
this.ref?.current?.requestFocus();
|
||||
}, 1);
|
||||
} else {
|
||||
await sleep(400);
|
||||
await this.doAsync("editor.commands.focus()");
|
||||
await this.doAsync(`editors[${tabId}]?.commands.focus()`, "focus");
|
||||
}
|
||||
};
|
||||
|
||||
blur = async () =>
|
||||
await this.doAsync(`
|
||||
editor && editor.commands.blur();
|
||||
typeof globalThis.editorTitle !== "undefined" && editorTitle.current && editorTitle.current.blur();
|
||||
`);
|
||||
blur = async (tabId: number) =>
|
||||
await this.doAsync(
|
||||
`
|
||||
const editor = editors[${tabId}];
|
||||
const editorTitle = editorTitles[${tabId}];
|
||||
editor && editor.commands.blur();
|
||||
typeof editorTitle !== "undefined" && editorTitle.current && editorTitle.current.blur();
|
||||
`,
|
||||
"blur"
|
||||
);
|
||||
|
||||
clearContent = async () => {
|
||||
clearContent = async (tabId: number) => {
|
||||
console.log("clearContent");
|
||||
this.previousSettings = null;
|
||||
await this.doAsync(
|
||||
`
|
||||
if (typeof globalThis.statusBar !== "undefined") {
|
||||
globalThis.statusBar.current.resetWords();
|
||||
globalThis.statusBar.current.set({date:"",saved:""});
|
||||
}
|
||||
|
||||
editor.commands.blur();
|
||||
typeof globalThis.editorTitle !== "undefined" && editorTitle.current && editorTitle.current?.blur();
|
||||
const editor = editors[${tabId}];
|
||||
const editorController = editorControllers[${tabId}];
|
||||
const editorTitle = editorTitles[${tabId}];
|
||||
const statusBar = statusBars[${tabId}];
|
||||
|
||||
editor.commands.blur();
|
||||
typeof editorTitle !== "undefined" && editorTitle.current && editorTitle.current?.blur();
|
||||
if (editorController.content) editorController.content.current = null;
|
||||
editorController.onUpdate();
|
||||
editorController.setTitle(null);
|
||||
|
||||
`
|
||||
editorController.countWords(0);
|
||||
if (typeof statusBar !== "undefined") {
|
||||
statusBar.current.resetWords();
|
||||
statusBar.current.set({date:"",saved:""});
|
||||
}`,
|
||||
"clearContent"
|
||||
);
|
||||
};
|
||||
|
||||
setSessionId = async (id: string | null) =>
|
||||
await this.doAsync(`globalThis.sessionId = "${id}";`);
|
||||
|
||||
setStatus = async (date: string | undefined, saved: string) =>
|
||||
setStatus = async (
|
||||
date: string | undefined,
|
||||
saved: string,
|
||||
tabId: number
|
||||
) => {
|
||||
console.log("setStatus");
|
||||
await this.doAsync(
|
||||
`typeof globalThis.statusBar !== "undefined" && statusBar.current.set({date:"${date}",saved:"${saved}"})`
|
||||
`
|
||||
const statusBar = statusBars[${tabId}];
|
||||
typeof statusBar !== "undefined" && statusBar.current.set({date:"${date}",saved:"${saved}"})`,
|
||||
"setStatus"
|
||||
);
|
||||
};
|
||||
|
||||
setPlaceholder = async (placeholder: string) => {
|
||||
await this.doAsync(`
|
||||
const element = document.querySelector(".is-editor-empty");
|
||||
if (element) {
|
||||
element.setAttribute("data-placeholder","${placeholder}");
|
||||
}
|
||||
`);
|
||||
// await this.doAsync(`
|
||||
// const element = document.querySelector(".is-editor-empty");
|
||||
// if (element) {
|
||||
// element.setAttribute("data-placeholder","${placeholder}");
|
||||
// }
|
||||
// `);
|
||||
};
|
||||
|
||||
setInsets = async (insets: EdgeInsets) => {
|
||||
@@ -173,10 +194,14 @@ editorController.setTitle(null);
|
||||
|
||||
setTags = async (note: Note | null | undefined) => {
|
||||
if (!note) return;
|
||||
const tabId = useTabStore.getState().getTabForNote(note.id);
|
||||
|
||||
const tags = await db.relations.to(note, "tag").resolve();
|
||||
await this.doAsync(`
|
||||
if (typeof editorTags !== "undefined" && editorTags.current) {
|
||||
editorTags.current.setTags(${JSON.stringify(
|
||||
await this.doAsync(
|
||||
`
|
||||
const tags = editorTags[${tabId}];
|
||||
if (tags && tags.current) {
|
||||
tags.current.setTags(${JSON.stringify(
|
||||
tags.map((tag) => ({
|
||||
title: tag.title,
|
||||
alias: tag.title,
|
||||
@@ -185,28 +210,38 @@ editorController.setTitle(null);
|
||||
}))
|
||||
)});
|
||||
}
|
||||
`);
|
||||
};
|
||||
|
||||
clearTags = async () => {
|
||||
await this.doAsync(`
|
||||
if (typeof editorTags !== "undefined" && editorTags.current) {
|
||||
editorTags.current.setTags([]);
|
||||
}
|
||||
`);
|
||||
};
|
||||
|
||||
insertAttachment = async (attachment: Attachment) => {
|
||||
await this.doAsync(
|
||||
`editor && editor.commands.insertAttachment(${JSON.stringify(
|
||||
attachment
|
||||
)})`
|
||||
`,
|
||||
"setTags"
|
||||
);
|
||||
};
|
||||
|
||||
setAttachmentProgress = async (attachmentProgress: Partial<Attachment>) => {
|
||||
clearTags = async (tabId: number) => {
|
||||
await this.doAsync(
|
||||
`editor && editor.commands.updateAttachment(${JSON.stringify(
|
||||
`
|
||||
const tags = editorTags[${tabId}];
|
||||
logger("info", Object.keys(editorTags), typeof editorTags[0]);
|
||||
if (tags && tags.current) {
|
||||
tags.current.setTags([]);
|
||||
}
|
||||
`,
|
||||
"clearTags"
|
||||
);
|
||||
};
|
||||
|
||||
insertAttachment = async (attachment: Attachment, tabId: number) => {
|
||||
await this.doAsync(
|
||||
`const editor = editors[${tabId}];
|
||||
editor && editor.commands.insertAttachment(${JSON.stringify(attachment)})`
|
||||
);
|
||||
};
|
||||
|
||||
setAttachmentProgress = async (
|
||||
attachmentProgress: AttachmentProgress,
|
||||
tabId: number
|
||||
) => {
|
||||
await this.doAsync(
|
||||
`const editor = editors[${tabId}];
|
||||
editor && editor.commands.setAttachmentProgress(${JSON.stringify(
|
||||
attachmentProgress
|
||||
)}, {
|
||||
preventUpdate: true,
|
||||
@@ -220,10 +255,13 @@ editorController.setTitle(null);
|
||||
insertImage = async (
|
||||
image: Omit<ImageAttributes, "bloburl"> & {
|
||||
dataurl: string;
|
||||
}
|
||||
},
|
||||
tabId: number
|
||||
) => {
|
||||
await this.doAsync(
|
||||
`const image = toBlobURL("${image.dataurl}", "${image.hash}");
|
||||
`const editor = editors[${tabId}];
|
||||
|
||||
const image = toBlobURL("${image.dataurl}", "${image.hash}");
|
||||
editor && editor.commands.insertImage({
|
||||
...${JSON.stringify({
|
||||
...image,
|
||||
@@ -234,22 +272,30 @@ editorController.setTitle(null);
|
||||
);
|
||||
};
|
||||
|
||||
updateWebclip = async ({ src, hash }: Partial<ImageAttributes>) => {
|
||||
updateWebclip = async (
|
||||
{ src, hash }: Partial<ImageAttributes>,
|
||||
tabId: number
|
||||
) => {
|
||||
await this.doAsync(
|
||||
`editor && editor.commands.updateWebClip(${JSON.stringify({
|
||||
`const editor = editors[${tabId}];
|
||||
editor && editor.commands.updateWebClip(${JSON.stringify({
|
||||
hash
|
||||
})},${JSON.stringify({ src })})`
|
||||
);
|
||||
};
|
||||
|
||||
updateImage = async ({
|
||||
hash,
|
||||
dataurl
|
||||
}: Partial<Omit<ImageAttributes, "bloburl">> & {
|
||||
dataurl: string;
|
||||
}) => {
|
||||
updateImage = async (
|
||||
{
|
||||
hash,
|
||||
dataurl
|
||||
}: Partial<Omit<ImageAttributes, "bloburl">> & {
|
||||
dataurl: string;
|
||||
},
|
||||
tabId: number
|
||||
) => {
|
||||
await this.doAsync(
|
||||
`const image = toBlobURL("${dataurl}", "${hash}");
|
||||
`const editor = editors[${tabId}];
|
||||
const image = toBlobURL("${dataurl}", "${hash}");
|
||||
editor && editor.commands.updateImage(${JSON.stringify({
|
||||
hash
|
||||
})}, {
|
||||
|
||||
@@ -37,5 +37,8 @@ export const EventTypes = {
|
||||
reminders: "editor-event:reminders",
|
||||
previewAttachment: "editor-event:preview-attachment",
|
||||
copyToClipboard: "editor-events:copy-to-clipboard",
|
||||
getAttachmentData: "editor-events:get-attachment-data"
|
||||
getAttachmentData: "editor-events:get-attachment-data",
|
||||
tabsChanged: "editor-events:tabs-changed",
|
||||
showTabs: "editor-events:showTabs",
|
||||
tabFocused: "editor-events:tab-focused"
|
||||
};
|
||||
|
||||
@@ -37,6 +37,9 @@ import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import { FILE_SIZE_LIMIT, IMAGE_SIZE_LIMIT } from "../../../utils/constants";
|
||||
import { eCloseSheet } from "../../../utils/events";
|
||||
import { editorController, editorState } from "./utils";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import filesystem from "../../../common/filesystem";
|
||||
import { useTabStore } from "./use-tab-store";
|
||||
|
||||
const showEncryptionSheet = (file) => {
|
||||
presentSheet({
|
||||
@@ -52,6 +55,15 @@ const santizeUri = (uri) => {
|
||||
return uri;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* noteId: string,
|
||||
* tabId: string,
|
||||
* type: "image" | "camera" | "file"
|
||||
* reupload: boolean
|
||||
* hash?: string
|
||||
* }} fileOptions
|
||||
*/
|
||||
const file = async (fileOptions) => {
|
||||
try {
|
||||
const options = {
|
||||
@@ -104,22 +116,33 @@ const file = async (fileOptions) => {
|
||||
if (!(await attachFile(uri, hash, file.type, file.name, fileOptions)))
|
||||
return;
|
||||
if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri);
|
||||
if (isImage(file.type)) {
|
||||
editorController.current?.commands.insertImage({
|
||||
hash: hash,
|
||||
filename: file.name,
|
||||
mime: file.type,
|
||||
size: file.size,
|
||||
dataurl: await db.attachments.read(hash, "base64"),
|
||||
title: file.name
|
||||
});
|
||||
} else {
|
||||
editorController.current?.commands.insertAttachment({
|
||||
hash: hash,
|
||||
filename: file.name,
|
||||
mime: file.type,
|
||||
size: file.size
|
||||
});
|
||||
|
||||
if (
|
||||
useTabStore.getState().getNoteIdForTab(options.tabId) === options.noteId
|
||||
) {
|
||||
if (isImage(file.type)) {
|
||||
editorController.current?.commands.insertImage(
|
||||
{
|
||||
hash: hash,
|
||||
filename: file.name,
|
||||
mime: file.type,
|
||||
size: file.size,
|
||||
dataurl: await db.attachments.read(hash, "base64"),
|
||||
title: file.name
|
||||
},
|
||||
fileOptions.tabId
|
||||
);
|
||||
} else {
|
||||
editorController.current?.commands.insertAttachment(
|
||||
{
|
||||
hash: hash,
|
||||
filename: file.name,
|
||||
mime: file.type,
|
||||
size: file.size
|
||||
},
|
||||
fileOptions.tabId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -136,6 +159,15 @@ const file = async (fileOptions) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* noteId: string,
|
||||
* tabId: string,
|
||||
* type: "image" | "camera" | "file"
|
||||
* reupload: boolean
|
||||
* hash?: string
|
||||
* }} options
|
||||
*/
|
||||
const camera = async (options) => {
|
||||
try {
|
||||
await db.attachments.generateKey();
|
||||
@@ -181,6 +213,17 @@ const gallery = async (options) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {{
|
||||
* noteId: string,
|
||||
* tabId: string,
|
||||
* type: "image" | "camera" | "file"
|
||||
* reupload: boolean
|
||||
* hash?: string
|
||||
* }} options
|
||||
* @returns
|
||||
*/
|
||||
const pick = async (options) => {
|
||||
if (!PremiumService.get()) {
|
||||
let user = await db.user.getUser();
|
||||
@@ -253,17 +296,40 @@ const handleImageResponse = async (response, options) => {
|
||||
|
||||
if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri);
|
||||
|
||||
editorController.current?.commands.insertImage({
|
||||
hash: hash,
|
||||
mime: image.type,
|
||||
title: fileName,
|
||||
dataurl: b64,
|
||||
size: image.fileSize,
|
||||
filename: fileName
|
||||
});
|
||||
if (
|
||||
useTabStore.getState().getNoteIdForTab(options.tabId) === options.noteId
|
||||
) {
|
||||
editorController.current?.commands.insertImage(
|
||||
{
|
||||
hash: hash,
|
||||
mime: image.type,
|
||||
title: fileName,
|
||||
dataurl: b64,
|
||||
size: image.fileSize,
|
||||
filename: fileName
|
||||
},
|
||||
options.tabId
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} uri
|
||||
* @param {*} hash
|
||||
* @param {*} type
|
||||
* @param {*} filename
|
||||
/**
|
||||
* @param {{
|
||||
* noteId: string,
|
||||
* tabId: string,
|
||||
* type: "image" | "camera" | "file"
|
||||
* reupload: boolean
|
||||
* hash?: string
|
||||
* }} options
|
||||
* @returns
|
||||
*/
|
||||
export async function attachFile(uri, hash, type, filename, options) {
|
||||
try {
|
||||
let exists = db.attachments.exists(hash);
|
||||
@@ -299,10 +365,7 @@ export async function attachFile(uri, hash, type, filename, options) {
|
||||
} else {
|
||||
encryptionInfo = { hash: hash };
|
||||
}
|
||||
await db.attachments.add(
|
||||
encryptionInfo,
|
||||
editorController.current?.note?.id
|
||||
);
|
||||
await db.attachments.add(encryptionInfo, options.noteId);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
|
||||
36
apps/mobile/app/screens/editor/tiptap/session-history.ts
Normal file
36
apps/mobile/app/screens/editor/tiptap/session-history.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -66,10 +66,12 @@ export type EditorProps = {
|
||||
onChange?: (html: string) => void;
|
||||
};
|
||||
|
||||
export type EditorMessage = {
|
||||
export type EditorMessage<T> = {
|
||||
sessionId: string;
|
||||
value: unknown;
|
||||
value: T;
|
||||
type: string;
|
||||
noteId: string;
|
||||
tabId: number;
|
||||
};
|
||||
|
||||
export type SavePayload = {
|
||||
@@ -77,9 +79,9 @@ export type SavePayload = {
|
||||
id?: string;
|
||||
data?: string;
|
||||
type?: "tiptap";
|
||||
sessionId?: string | null;
|
||||
sessionHistoryId?: number;
|
||||
ignoreEdit: boolean;
|
||||
tabId: number;
|
||||
};
|
||||
|
||||
export type AppState = {
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
import { WebViewMessageEvent } from "react-native-webview";
|
||||
import { db } from "../../../common/database";
|
||||
import downloadAttachment from "../../../common/filesystem/download-attachment";
|
||||
import EditorTabs from "../../../components/sheets/editor-tabs";
|
||||
import ManageTagsSheet from "../../../components/sheets/manage-tags";
|
||||
import { RelationsList } from "../../../components/sheets/relations-list";
|
||||
import ReminderSheet from "../../../components/sheets/reminder";
|
||||
@@ -66,9 +67,10 @@ import { tabBarRef } from "../../../utils/global-refs";
|
||||
import { useDragState } from "../../settings/editor/state";
|
||||
import { EventTypes } from "./editor-events";
|
||||
import { EditorMessage, EditorProps, useEditorType } from "./types";
|
||||
import { useTabStore } from "./use-tab-store";
|
||||
import { EditorEvents, editorState } from "./utils";
|
||||
|
||||
const publishNote = async (editor: useEditorType) => {
|
||||
const publishNote = async () => {
|
||||
const user = useUserStore.getState().user;
|
||||
if (!user) {
|
||||
ToastManager.show({
|
||||
@@ -91,9 +93,12 @@ const publishNote = async (editor: useEditorType) => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
const currentNote = editor?.note?.current;
|
||||
if (currentNote?.id) {
|
||||
const note = await db.notes?.note(currentNote.id);
|
||||
const noteId = useTabStore
|
||||
.getState()
|
||||
.getNoteIdForTab(useTabStore.getState().currentTab);
|
||||
|
||||
if (noteId) {
|
||||
const note = await db.notes?.note(noteId);
|
||||
const locked = note && (await db.vaults.itemExists(note));
|
||||
if (locked) {
|
||||
ToastManager.show({
|
||||
@@ -110,11 +115,12 @@ const publishNote = async (editor: useEditorType) => {
|
||||
}
|
||||
};
|
||||
|
||||
const showActionsheet = async (editor: useEditorType) => {
|
||||
const currentNote = editor?.note?.current;
|
||||
if (currentNote?.id) {
|
||||
const note = await db.notes?.note(currentNote.id);
|
||||
|
||||
const showActionsheet = async () => {
|
||||
const noteId = useTabStore
|
||||
.getState()
|
||||
.getNoteIdForTab(useTabStore.getState().currentTab);
|
||||
if (noteId) {
|
||||
const note = await db.notes?.note(noteId);
|
||||
if (editorState().isFocused || editorState().isFocused) {
|
||||
editorState().isFocused = true;
|
||||
}
|
||||
@@ -145,7 +151,6 @@ export const useEditorEvents = (
|
||||
]);
|
||||
|
||||
const handleBack = useRef<NativeEventSubscription>();
|
||||
const readonly = useEditorStore((state) => state.readonly);
|
||||
const isPremium = useUserStore((state) => state.premium);
|
||||
const { fontScale } = useWindowDimensions();
|
||||
|
||||
@@ -193,7 +198,7 @@ export const useEditorEvents = (
|
||||
deviceMode: deviceMode || "mobile",
|
||||
fullscreen: fullscreen || false,
|
||||
premium: isPremium,
|
||||
readonly: readonly || editorPropReadonly,
|
||||
readonly: false,
|
||||
tools: tools || getDefaultPresets().default,
|
||||
noHeader: noHeader,
|
||||
noToolbar: noToolbar,
|
||||
@@ -212,13 +217,11 @@ export const useEditorEvents = (
|
||||
}, [
|
||||
fullscreen,
|
||||
isPremium,
|
||||
readonly,
|
||||
editor.loading,
|
||||
deviceMode,
|
||||
tools,
|
||||
editor.commands,
|
||||
doubleSpacedLines,
|
||||
editorPropReadonly,
|
||||
noHeader,
|
||||
noToolbar,
|
||||
corsProxy,
|
||||
@@ -238,7 +241,7 @@ export const useEditorEvents = (
|
||||
return;
|
||||
}
|
||||
editorState().currentlyEditing = false;
|
||||
editor.reset();
|
||||
// editor.reset(); Notes remain open.
|
||||
setTimeout(async () => {
|
||||
if (deviceMode !== "mobile" && fullscreen) {
|
||||
if (fullscreen) {
|
||||
@@ -334,23 +337,7 @@ export const useEditorEvents = (
|
||||
const onMessage = useCallback(
|
||||
async (event: WebViewMessageEvent) => {
|
||||
const data = event.nativeEvent.data;
|
||||
const editorMessage = JSON.parse(data) as EditorMessage;
|
||||
|
||||
if (editorMessage.type === EventTypes.content) {
|
||||
editor.saveContent({
|
||||
type: editorMessage.type,
|
||||
content: (editorMessage.value as ContentMessage).html,
|
||||
ignoreEdit: (editorMessage.value as ContentMessage).ignoreEdit,
|
||||
forSessionId: editorMessage.sessionId
|
||||
});
|
||||
} else if (editorMessage.type === EventTypes.title) {
|
||||
editor.saveContent({
|
||||
type: editorMessage.type,
|
||||
title: editorMessage.value as string,
|
||||
forSessionId: editorMessage.sessionId,
|
||||
ignoreEdit: false
|
||||
});
|
||||
}
|
||||
const editorMessage = JSON.parse(data) as EditorMessage<any>;
|
||||
|
||||
if (editorMessage.type === EventTypes.back) {
|
||||
return onBackPress();
|
||||
@@ -363,7 +350,29 @@ export const useEditorEvents = (
|
||||
return;
|
||||
}
|
||||
|
||||
const noteId = useTabStore
|
||||
.getState()
|
||||
.getNoteIdForTab(editorMessage.tabId);
|
||||
|
||||
switch (editorMessage.type) {
|
||||
case EventTypes.content:
|
||||
editor.saveContent({
|
||||
type: editorMessage.type,
|
||||
content: editorMessage.value as string,
|
||||
noteId: noteId,
|
||||
tabId: editorMessage.tabId,
|
||||
ignoreEdit: (editorMessage.value as ContentMessage).ignoreEdit
|
||||
});
|
||||
break;
|
||||
case EventTypes.title:
|
||||
editor.saveContent({
|
||||
type: editorMessage.type,
|
||||
title: editorMessage.value as string,
|
||||
noteId: noteId,
|
||||
tabId: editorMessage.tabId,
|
||||
ignoreEdit: false
|
||||
});
|
||||
break;
|
||||
case EventTypes.logger:
|
||||
logger.info("[WEBVIEW LOG]", editorMessage.value);
|
||||
break;
|
||||
@@ -373,41 +382,45 @@ export const useEditorEvents = (
|
||||
case EventTypes.selection:
|
||||
break;
|
||||
case EventTypes.reminders:
|
||||
if (!editor.note.current) {
|
||||
if (!noteId) {
|
||||
ToastManager.show({
|
||||
heading: "Create a note first to add a reminder",
|
||||
type: "success"
|
||||
});
|
||||
return;
|
||||
}
|
||||
const note = await db.notes.note(noteId);
|
||||
if (!note) return;
|
||||
RelationsList.present({
|
||||
reference: editor.note.current as any,
|
||||
reference: note as any,
|
||||
referenceType: "reminder",
|
||||
relationType: "from",
|
||||
title: "Reminders",
|
||||
onAdd: () =>
|
||||
ReminderSheet.present(undefined, editor.note.current as any, true)
|
||||
onAdd: () => ReminderSheet.present(undefined, note, true)
|
||||
});
|
||||
break;
|
||||
case EventTypes.newtag:
|
||||
if (!editor.note.current) {
|
||||
if (!noteId) {
|
||||
ToastManager.show({
|
||||
heading: "Create a note first to add a tag",
|
||||
type: "success"
|
||||
});
|
||||
return;
|
||||
}
|
||||
ManageTagsSheet.present([editor.note.current?.id]);
|
||||
ManageTagsSheet.present([noteId]);
|
||||
break;
|
||||
case EventTypes.tag:
|
||||
if (editorMessage.value) {
|
||||
if (!editor.note.current) return;
|
||||
if (!noteId) return;
|
||||
const note = await db.notes.note(noteId);
|
||||
if (!note) return;
|
||||
|
||||
db.relations
|
||||
.unlink(editorMessage.value as ItemReference, editor.note.current)
|
||||
.unlink(editorMessage.value as ItemReference, note)
|
||||
.then(async () => {
|
||||
useTagStore.getState().refresh();
|
||||
useRelationStore.getState().update();
|
||||
await editor.commands.setTags(editor.note.current);
|
||||
await editor.commands.setTags(note);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
});
|
||||
}
|
||||
@@ -415,7 +428,11 @@ export const useEditorEvents = (
|
||||
case EventTypes.filepicker:
|
||||
editorState().isAwaitingResult = true;
|
||||
const { pick } = require("./picker.js").default;
|
||||
pick({ type: editorMessage.value });
|
||||
pick({
|
||||
type: editorMessage.value,
|
||||
noteId: noteId,
|
||||
tabId: editorMessage.tabId
|
||||
});
|
||||
setTimeout(() => {
|
||||
editorState().isAwaitingResult = false;
|
||||
}, 1000);
|
||||
@@ -472,10 +489,10 @@ export const useEditorEvents = (
|
||||
eSendEvent(eOpenPremiumDialog);
|
||||
break;
|
||||
case EventTypes.monograph:
|
||||
publishNote(editor);
|
||||
publishNote();
|
||||
break;
|
||||
case EventTypes.properties:
|
||||
showActionsheet(editor);
|
||||
showActionsheet();
|
||||
break;
|
||||
case EventTypes.fullscreen:
|
||||
editorState().isFullscreen = true;
|
||||
@@ -494,13 +511,41 @@ export const useEditorEvents = (
|
||||
} else {
|
||||
eSendEvent("PDFPreview", attachment);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case EventTypes.copyToClipboard: {
|
||||
Clipboard.setString(editorMessage.value as string);
|
||||
break;
|
||||
}
|
||||
case EventTypes.tabsChanged: {
|
||||
useTabStore.setState({
|
||||
tabs: (editorMessage.value as any)?.tabs,
|
||||
currentTab: (editorMessage.value as any)?.currentTab
|
||||
});
|
||||
console.log("tabs updated...");
|
||||
break;
|
||||
}
|
||||
case EventTypes.showTabs: {
|
||||
EditorTabs.present();
|
||||
break;
|
||||
}
|
||||
case EventTypes.tabFocused: {
|
||||
// Reload the note
|
||||
const note = await db.notes.note(editorMessage.noteId);
|
||||
if (note) {
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: note,
|
||||
forced: true
|
||||
});
|
||||
}
|
||||
// TODO
|
||||
// Handle any updates that occured in an note while the tab was not focused.
|
||||
// If editor has no content, reload the note, because it might be an app reload
|
||||
// or the tab was destroyed in background...
|
||||
// Maybe cache changes, something like pendingUpdates list.
|
||||
// Do proper cleanup when a tab is destroyed though.
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -47,14 +47,15 @@ import Navigation from "../../../services/navigation";
|
||||
import Notifications from "../../../services/notifications";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { TipManager } from "../../../services/tip-manager";
|
||||
import { useEditorStore } from "../../../stores/use-editor-store";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import { useTagStore } from "../../../stores/use-tag-store";
|
||||
import { eClearEditor, eOnLoadNote } from "../../../utils/events";
|
||||
import { tabBarRef } from "../../../utils/global-refs";
|
||||
import { onNoteCreated } from "../../notes/common";
|
||||
import Commands from "./commands";
|
||||
import { SessionHistory } from "./session-history";
|
||||
import { EditorState, SavePayload } from "./types";
|
||||
import { useTabStore } from "./use-tab-store";
|
||||
import {
|
||||
EditorEvents,
|
||||
clearAppState,
|
||||
@@ -62,32 +63,50 @@ import {
|
||||
getAppState,
|
||||
isContentInvalid,
|
||||
isEditorLoaded,
|
||||
makeSessionId,
|
||||
post
|
||||
} from "./utils";
|
||||
|
||||
// Keep a fixed session id, dont' change it when a new note is opened, session id can stay the same always I think once the app is opened. DONE
|
||||
// Editor will save any note content & title is recieved. and dispatch update to relavant tab always.
|
||||
|
||||
// Editor keeps track of what tab is opened and which note is currently focused by keeping a synced zustand store with editor. DONE
|
||||
// the useEditor hook can recieve save messages for different notes at a time. DONE
|
||||
// When a note is created, the useEditor hook must immediately notify the editor with the note id and set the note id in the editor tabs store
|
||||
// so further changes will go into that note. DONE
|
||||
// Events sent to editor have the tab id value added to ensure the correct tab will recieve and return events only.
|
||||
// The useEditorEvents hook can manage events from different tabs at the same time as long as the attached session id matches. DONE
|
||||
// useEditor hook will keep historySessionId for different notes instead of a single note. DONE
|
||||
|
||||
export const useEditor = (
|
||||
editorId = "",
|
||||
readonly?: boolean,
|
||||
onChange?: (html: string) => void
|
||||
) => {
|
||||
const theme = useThemeEngineStore((state) => state.theme);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const sessionIdRef = useRef(makeSessionId());
|
||||
const sessionIdRef = useRef("notesnook-editor");
|
||||
const editorRef = useRef<WebView>(null);
|
||||
const currentNote = useRef<
|
||||
| (Note & {
|
||||
content?: NoteContent<false> & {
|
||||
isPreview?: boolean;
|
||||
};
|
||||
})
|
||||
| null
|
||||
>();
|
||||
const currentContent = useRef<Partial<UnencryptedContentItem> | null>();
|
||||
|
||||
const currentNotes = useRef<
|
||||
Record<
|
||||
string,
|
||||
| (Note & {
|
||||
content?: NoteContent<false> & {
|
||||
isPreview?: boolean;
|
||||
};
|
||||
})
|
||||
| null
|
||||
| undefined
|
||||
>
|
||||
>({});
|
||||
|
||||
const currentContents = useRef<
|
||||
Record<string, Partial<UnencryptedContentItem> | null>
|
||||
>({});
|
||||
|
||||
const timers = useRef<{ [name: string]: NodeJS.Timeout }>({});
|
||||
const commands = useMemo(() => new Commands(editorRef), [editorRef]);
|
||||
const sessionHistoryId = useRef<number>();
|
||||
const editorSessionHistory = useMemo(() => new SessionHistory(), []);
|
||||
const state = useRef<Partial<EditorState>>(defaultState);
|
||||
const placeholderTip = useRef(TipManager.placeholderTip());
|
||||
const tags = useTagStore((state) => state.items);
|
||||
@@ -96,11 +115,18 @@ export const useEditor = (
|
||||
const saveCount = useRef(0);
|
||||
const lastContentChangeTime = useRef<number>(0);
|
||||
const lock = useRef(false);
|
||||
const lockedSessionId = useRef<string>();
|
||||
const currentLoadingNoteId = useRef<string>();
|
||||
const loadingState = useRef<string>();
|
||||
const postMessage = useCallback(
|
||||
async <T>(type: string, data: T, waitFor = 300) =>
|
||||
await post(editorRef, sessionIdRef.current, type, data, waitFor),
|
||||
async <T>(type: string, data: T, tabId?: number, waitFor = 300) =>
|
||||
await post(
|
||||
editorRef,
|
||||
sessionIdRef.current,
|
||||
typeof tabId !== "number" ? useTabStore.getState().currentTab : tabId,
|
||||
type,
|
||||
data,
|
||||
waitFor
|
||||
),
|
||||
[sessionIdRef]
|
||||
);
|
||||
|
||||
@@ -115,14 +141,16 @@ export const useEditor = (
|
||||
}, [theme, postMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
commands.setTags(currentNote.current);
|
||||
for (const id in currentNotes.current) {
|
||||
commands.setTags(currentNotes.current[id]);
|
||||
}
|
||||
}, [commands, tags]);
|
||||
|
||||
const overlay = useCallback(
|
||||
(show: boolean, data = { type: "new" }) => {
|
||||
eSendEvent(
|
||||
"loadingNote" + editorId,
|
||||
show ? data || currentNote.current : false
|
||||
show ? data || currentNotes.current : false
|
||||
);
|
||||
},
|
||||
[editorId]
|
||||
@@ -144,32 +172,31 @@ export const useEditor = (
|
||||
);
|
||||
|
||||
const reset = useCallback(
|
||||
async (resetState = true, resetContent = true) => {
|
||||
currentNote.current?.id && db.fs().cancel(currentNote.current.id);
|
||||
currentNote.current = null;
|
||||
currentContent.current = null;
|
||||
sessionHistoryId.current = undefined;
|
||||
async (tabId: number, resetState = true, resetContent = true) => {
|
||||
const noteId = useTabStore.getState().getNoteIdForTab(tabId);
|
||||
if (noteId) {
|
||||
currentNotes.current?.id && db.fs().cancel(noteId, "download");
|
||||
|
||||
currentNotes.current[noteId] = null;
|
||||
currentContents.current[noteId] = null;
|
||||
editorSessionHistory.clearSession(noteId);
|
||||
}
|
||||
|
||||
clearTimeout(timers.current["loading-images" + noteId]);
|
||||
|
||||
saveCount.current = 0;
|
||||
loadingState.current = undefined;
|
||||
lock.current = false;
|
||||
useEditorStore.getState().setReadonly(false);
|
||||
resetContent && postMessage(EditorEvents.title, "");
|
||||
resetContent && postMessage(EditorEvents.title, "", tabId);
|
||||
lastContentChangeTime.current = 0;
|
||||
resetContent && (await commands.clearContent());
|
||||
resetContent && (await commands.clearTags());
|
||||
resetContent && (await commands.clearContent(tabId));
|
||||
resetContent && (await commands.clearTags(tabId));
|
||||
|
||||
if (resetState) {
|
||||
const newSessionId = makeSessionId();
|
||||
sessionIdRef.current = newSessionId;
|
||||
await commands.setSessionId(newSessionId);
|
||||
|
||||
isDefaultEditor &&
|
||||
useEditorStore.getState().setCurrentlyEditingNote(null);
|
||||
placeholderTip.current = TipManager.placeholderTip();
|
||||
await commands.setPlaceholder(placeholderTip.current);
|
||||
}
|
||||
useTabStore.getState().updateTab(tabId, {
|
||||
noteId: undefined
|
||||
});
|
||||
},
|
||||
[commands, isDefaultEditor, postMessage]
|
||||
[commands, editorSessionHistory, postMessage]
|
||||
);
|
||||
|
||||
const saveNote = useCallback(
|
||||
@@ -178,32 +205,24 @@ export const useEditor = (
|
||||
id,
|
||||
data,
|
||||
type,
|
||||
sessionId: currentSessionId,
|
||||
ignoreEdit,
|
||||
sessionHistoryId: currentSessionHistoryId,
|
||||
ignoreEdit
|
||||
tabId
|
||||
}: SavePayload) => {
|
||||
if (
|
||||
readonly ||
|
||||
useEditorStore.getState().readonly ||
|
||||
currentNote.current?.readonly
|
||||
)
|
||||
return;
|
||||
if (currentNotes.current[id as string]?.readonly) return;
|
||||
try {
|
||||
if (id && !(await db.notes?.note(id))) {
|
||||
isDefaultEditor &&
|
||||
useEditorStore.getState().setCurrentlyEditingNote(null);
|
||||
await reset();
|
||||
await reset(tabId);
|
||||
return;
|
||||
}
|
||||
let note = id ? await db.notes?.note(id) : undefined;
|
||||
const locked = note && (await db.vaults.itemExists(note));
|
||||
if (note?.conflicted) return;
|
||||
|
||||
if (isContentInvalid(data)) {
|
||||
if (isContentInvalid(data) && id) {
|
||||
// Create a new history session if recieved empty or invalid content
|
||||
// To ensure that history is preserved for correct content.
|
||||
sessionHistoryId.current = Date.now();
|
||||
currentSessionHistoryId = sessionHistoryId.current;
|
||||
currentSessionHistoryId = editorSessionHistory.newSession(id);
|
||||
}
|
||||
|
||||
const noteData: Partial<Note> & {
|
||||
@@ -211,9 +230,7 @@ export const useEditor = (
|
||||
content?: NoteContent<false>;
|
||||
} = {
|
||||
id,
|
||||
sessionId: isContentInvalid(data)
|
||||
? undefined
|
||||
: (currentSessionHistoryId as any)
|
||||
sessionId: `${currentSessionHistoryId}`
|
||||
};
|
||||
|
||||
noteData.title = title;
|
||||
@@ -229,10 +246,25 @@ export const useEditor = (
|
||||
type: type as ContentType
|
||||
};
|
||||
}
|
||||
|
||||
// If note is edited, the tab becomes a persistent tab automatically.
|
||||
useTabStore.getState().updateTab(tabId, {
|
||||
previewTab: false
|
||||
});
|
||||
|
||||
if (!locked) {
|
||||
id = await db.notes?.add(noteData);
|
||||
if (!note && id) {
|
||||
currentNote.current = await db.notes?.note(id);
|
||||
useTabStore.getState().updateTab(tabId, {
|
||||
noteId: id
|
||||
});
|
||||
|
||||
editorSessionHistory.newSession(id);
|
||||
|
||||
if (id) {
|
||||
currentNotes.current[id] = await db.notes?.note(id);
|
||||
}
|
||||
|
||||
const defaultNotebook = db.settings.getDefaultNotebook();
|
||||
if (!state.current.onNoteCreated && defaultNotebook) {
|
||||
onNoteCreated(id, {
|
||||
@@ -244,25 +276,14 @@ export const useEditor = (
|
||||
}
|
||||
|
||||
if (!noteData.title) {
|
||||
postMessage(EditorEvents.title, currentNote.current?.title);
|
||||
postMessage(
|
||||
EditorEvents.title,
|
||||
currentNotes.current?.title,
|
||||
tabId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
useEditorStore.getState().currentEditingNote !== id &&
|
||||
isDefaultEditor &&
|
||||
state.current.currentlyEditing
|
||||
) {
|
||||
setTimeout(() => {
|
||||
if (
|
||||
(currentNote.current?.id && currentNote.current?.id !== id) ||
|
||||
!state.current.currentlyEditing
|
||||
)
|
||||
return;
|
||||
id && useEditorStore.getState().setCurrentlyEditingNote(id);
|
||||
});
|
||||
}
|
||||
|
||||
if (Notifications.isNotePinned(id as string)) {
|
||||
Notifications.pinNote(id as string);
|
||||
}
|
||||
@@ -271,19 +292,21 @@ export const useEditor = (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await db.vault?.save(noteData as any);
|
||||
}
|
||||
if (id && sessionIdRef.current === currentSessionId) {
|
||||
|
||||
if (id && useTabStore.getState().getTabForNote(id) === tabId) {
|
||||
note = (await db.notes?.note(id)) as Note;
|
||||
await commands.setStatus(
|
||||
getFormattedDate(note.dateEdited, "date-time"),
|
||||
"Saved"
|
||||
"Saved",
|
||||
tabId
|
||||
);
|
||||
|
||||
lastContentChangeTime.current = note.dateEdited;
|
||||
|
||||
if (
|
||||
saveCount.current < 2 ||
|
||||
currentNote.current?.title !== note.title ||
|
||||
currentNote.current?.headline?.slice(0, 200) !==
|
||||
currentNotes.current[id]?.title !== note.title ||
|
||||
currentNotes.current[id]?.headline?.slice(0, 200) !==
|
||||
note.headline?.slice(0, 200)
|
||||
) {
|
||||
Navigation.queueRoutesForUpdate();
|
||||
@@ -297,7 +320,7 @@ export const useEditor = (
|
||||
DatabaseLogger.error(e as Error);
|
||||
}
|
||||
},
|
||||
[commands, isDefaultEditor, postMessage, readonly, reset]
|
||||
[commands, editorSessionHistory, postMessage, reset]
|
||||
);
|
||||
|
||||
const loadContent = useCallback(
|
||||
@@ -306,13 +329,13 @@ export const useEditor = (
|
||||
content?: NoteContent<false>;
|
||||
}
|
||||
) => {
|
||||
currentNote.current = note;
|
||||
currentNotes.current[note.id] = note;
|
||||
const locked = note && (await db.vaults.itemExists(note));
|
||||
if ((locked || note.content) && note.content?.data) {
|
||||
currentContent.current = {
|
||||
currentContents.current[note.id] = {
|
||||
data: note.content?.data,
|
||||
type: note.content?.type || "tiptap",
|
||||
noteId: currentNote.current?.id as string
|
||||
noteId: note.id
|
||||
};
|
||||
} else if (note.contentId) {
|
||||
const rawContent = await db.content?.get(note.contentId);
|
||||
@@ -321,7 +344,7 @@ export const useEditor = (
|
||||
!isDeleted(rawContent) &&
|
||||
isUnencryptedContent(rawContent)
|
||||
) {
|
||||
currentContent.current = {
|
||||
currentContents.current[note.id] = {
|
||||
data: rawContent.data,
|
||||
type: rawContent.type
|
||||
};
|
||||
@@ -334,93 +357,105 @@ export const useEditor = (
|
||||
const loadNote = useCallback(
|
||||
async (event: { item?: Note; forced?: boolean; newNote?: boolean }) => {
|
||||
state.current.currentlyEditing = true;
|
||||
const editorState = useEditorStore.getState();
|
||||
|
||||
if (
|
||||
!state.current.ready &&
|
||||
(await isEditorLoaded(editorRef, sessionIdRef.current))
|
||||
(await isEditorLoaded(
|
||||
editorRef,
|
||||
sessionIdRef.current,
|
||||
useTabStore.getState().currentTab
|
||||
))
|
||||
) {
|
||||
state.current.ready = true;
|
||||
}
|
||||
|
||||
if (event.newNote) {
|
||||
currentNote.current && (await reset());
|
||||
const nextSessionId = makeSessionId(event.item?.id);
|
||||
sessionIdRef.current = nextSessionId;
|
||||
sessionHistoryId.current = Date.now();
|
||||
await commands.setSessionId(nextSessionId);
|
||||
if (state.current?.ready) await commands.focus();
|
||||
lastContentChangeTime.current = 0;
|
||||
useEditorStore.getState().setReadonly(false);
|
||||
useTabStore.getState().focusEmptyTab();
|
||||
const tabId = useTabStore.getState().currentTab;
|
||||
currentNotes.current && (await reset(tabId));
|
||||
setTimeout(() => {
|
||||
if (state.current?.ready) commands.focus(tabId);
|
||||
lastContentChangeTime.current = 0;
|
||||
});
|
||||
} else {
|
||||
if (!event.item) return;
|
||||
const item = event.item;
|
||||
|
||||
if (!event.forced && currentNote.current?.id === item.id) return;
|
||||
// If note was already opened in a tab, focus that tab.
|
||||
if (useTabStore.getState().hasTabForNote(event.item.id)) {
|
||||
const tabId = useTabStore.getState().getTabForNote(event.item.id);
|
||||
if (typeof tabId === "number") {
|
||||
useTabStore.getState().focusTab(tabId);
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise we focus the preview tab or create one to open the note in.
|
||||
useTabStore.getState().focusPreviewTab(event.item.id, {
|
||||
readonly: event.item.readonly,
|
||||
locked: false
|
||||
});
|
||||
const tabId = useTabStore.getState().currentTab;
|
||||
|
||||
// If note is already loaded and forced reload is not requested, return.
|
||||
if (!event.forced && currentNotes.current[item.id]) return;
|
||||
|
||||
state.current.movedAway = false;
|
||||
state.current.currentlyEditing = true;
|
||||
|
||||
if (currentNote.current?.id !== item.id) {
|
||||
currentNote.current && (await reset(false, false));
|
||||
isDefaultEditor && editorState.setCurrentlyEditingNote(item.id);
|
||||
if (!currentNotes.current[item.id]) {
|
||||
// Reset current tab if note isn't already loaded.
|
||||
currentNotes.current && (await reset(tabId, false, false));
|
||||
}
|
||||
|
||||
await loadContent(item);
|
||||
|
||||
if (
|
||||
currentNote.current?.id === item.id &&
|
||||
currentNotes.current[item.id] &&
|
||||
loadingState.current &&
|
||||
currentContent.current?.data &&
|
||||
loadingState.current === currentContent.current?.data
|
||||
currentContents.current?.data &&
|
||||
loadingState.current === currentContents.current?.data
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!currentContent.current?.data ||
|
||||
currentContent.current?.data.length < 50000
|
||||
) {
|
||||
if (state.current.ready) overlay(false);
|
||||
} else {
|
||||
overlay(true);
|
||||
}
|
||||
if (!state.current.ready) {
|
||||
currentNote.current = item;
|
||||
currentNotes.current[item.id] = item;
|
||||
return;
|
||||
}
|
||||
|
||||
lastContentChangeTime.current = item.dateEdited;
|
||||
const nextSessionId = makeSessionId(item.id);
|
||||
sessionIdRef.current = nextSessionId;
|
||||
lockedSessionId.current = nextSessionId;
|
||||
sessionHistoryId.current = Date.now();
|
||||
await commands.setSessionId(nextSessionId);
|
||||
currentNote.current = item;
|
||||
currentLoadingNoteId.current = item.id;
|
||||
currentNotes.current[item.id] = item;
|
||||
|
||||
if (!currentNotes.current[item.id]) return;
|
||||
|
||||
editorSessionHistory.newSession(item.id);
|
||||
|
||||
await commands.setStatus(
|
||||
getFormattedDate(item.dateEdited, "date-time"),
|
||||
"Saved"
|
||||
);
|
||||
await postMessage(EditorEvents.title, item.title);
|
||||
loadingState.current = currentContent.current?.data;
|
||||
|
||||
await postMessage(
|
||||
EditorEvents.html,
|
||||
currentContent.current?.data || "",
|
||||
10000
|
||||
"Saved",
|
||||
tabId
|
||||
);
|
||||
|
||||
await postMessage(EditorEvents.title, item.title, tabId);
|
||||
loadingState.current = currentContents.current[item.id]?.data;
|
||||
if (currentContents.current?.data) {
|
||||
await postMessage(
|
||||
EditorEvents.html,
|
||||
currentContents.current?.data,
|
||||
tabId,
|
||||
10000
|
||||
);
|
||||
}
|
||||
loadingState.current = undefined;
|
||||
useEditorStore.getState().setReadonly(item.readonly);
|
||||
await commands.setTags(currentNote.current);
|
||||
await commands.setTags(item);
|
||||
commands.setSettings();
|
||||
setTimeout(() => {
|
||||
if (lockedSessionId.current === nextSessionId) {
|
||||
lockedSessionId.current = undefined;
|
||||
if (currentLoadingNoteId.current === event.item?.id) {
|
||||
currentLoadingNoteId.current = undefined;
|
||||
}
|
||||
}, 300);
|
||||
overlay(false);
|
||||
}
|
||||
},
|
||||
[commands, isDefaultEditor, loadContent, overlay, postMessage, reset]
|
||||
[commands, editorSessionHistory, loadContent, postMessage, reset]
|
||||
);
|
||||
|
||||
const lockNoteWithVault = useCallback((note: Note) => {
|
||||
@@ -441,55 +476,56 @@ export const useEditor = (
|
||||
if (!data) return;
|
||||
const noteId = data.type === "tiptap" ? data.noteId : data.id;
|
||||
|
||||
if (!currentNote.current || noteId !== currentNote.current.id) return;
|
||||
if (!useTabStore.getState().hasTabForNote(noteId)) return;
|
||||
|
||||
const tabId = useTabStore.getState().getTabForNote(noteId);
|
||||
const isContentEncrypted =
|
||||
typeof (data as ContentItem)?.data === "object";
|
||||
|
||||
const note = await db.notes?.note(currentNote.current?.id);
|
||||
const note = await db.notes?.note(noteId);
|
||||
|
||||
if (lastContentChangeTime.current >= (data as Note).dateEdited) return;
|
||||
|
||||
lock.current = true;
|
||||
|
||||
if (data.type === "tiptap" && note) {
|
||||
const locked = await db.vaults.itemExists(currentNote.current);
|
||||
// Handle this case where note was locked on another device and synced.
|
||||
const locked = await db.vaults.itemExists(
|
||||
currentNotes.current[note.id]
|
||||
);
|
||||
if (!locked && isContentEncrypted) {
|
||||
lockNoteWithVault(note);
|
||||
} else if (locked && isEncryptedContent(data)) {
|
||||
const decryptedContent = await db.vault?.decryptContent(
|
||||
data,
|
||||
currentNote?.current?.id
|
||||
);
|
||||
const decryptedContent = await db.vault?.decryptContent(data, noteId);
|
||||
if (!decryptedContent) {
|
||||
lockNoteWithVault(note);
|
||||
} else {
|
||||
await postMessage(EditorEvents.updatehtml, decryptedContent.data);
|
||||
currentContent.current = decryptedContent;
|
||||
await postMessage(
|
||||
EditorEvents.updatehtml,
|
||||
decryptedContent.data,
|
||||
tabId
|
||||
);
|
||||
currentContents.current[note.id] = decryptedContent;
|
||||
}
|
||||
} else {
|
||||
const _nextContent = data.data;
|
||||
if (_nextContent === currentContent.current?.data) return;
|
||||
if (_nextContent === currentContents.current?.data) return;
|
||||
lastContentChangeTime.current = note.dateEdited;
|
||||
await postMessage(EditorEvents.updatehtml, _nextContent);
|
||||
await postMessage(EditorEvents.updatehtml, _nextContent, tabId);
|
||||
if (!isEncryptedContent(data)) {
|
||||
currentContent.current = data as UnencryptedContentItem;
|
||||
currentContents.current[note.id] = data as UnencryptedContentItem;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (data.type !== "note") return;
|
||||
const note = data;
|
||||
if (note.title !== currentNote.current.title) {
|
||||
postMessage(EditorEvents.title, note.title);
|
||||
}
|
||||
if (note.tags !== currentNote.current.tags) {
|
||||
await commands.setTags(note);
|
||||
}
|
||||
await commands.setStatus(
|
||||
if (!note) return;
|
||||
postMessage(EditorEvents.title, note.title, tabId);
|
||||
commands.setTags(note);
|
||||
commands.setStatus(
|
||||
getFormattedDate(note.dateEdited, "date-time"),
|
||||
"Saved"
|
||||
"Saved",
|
||||
tabId as number
|
||||
);
|
||||
}
|
||||
|
||||
lock.current = false;
|
||||
},
|
||||
[lockNoteWithVault, postMessage, commands]
|
||||
@@ -513,55 +549,44 @@ export const useEditor = (
|
||||
content,
|
||||
type,
|
||||
forSessionId,
|
||||
ignoreEdit
|
||||
ignoreEdit,
|
||||
noteId,
|
||||
tabId
|
||||
}: {
|
||||
noteId?: string;
|
||||
title?: string;
|
||||
content?: string;
|
||||
type: string;
|
||||
forSessionId: string;
|
||||
ignoreEdit: boolean;
|
||||
tabId: number;
|
||||
}) => {
|
||||
if (lock.current || lockedSessionId.current === forSessionId) return;
|
||||
if (lock.current || currentLoadingNoteId.current === noteId) return;
|
||||
|
||||
lastContentChangeTime.current = Date.now();
|
||||
|
||||
if (
|
||||
sessionHistoryId.current &&
|
||||
Date.now() - sessionHistoryId.current > 5 * 60 * 1000
|
||||
) {
|
||||
sessionHistoryId.current = Date.now();
|
||||
}
|
||||
|
||||
if (type === EditorEvents.content) {
|
||||
currentContent.current = {
|
||||
if (type === EditorEvents.content && noteId) {
|
||||
currentContents.current[noteId as string] = {
|
||||
data: content,
|
||||
type: "tiptap",
|
||||
noteId: currentNote.current?.id as string
|
||||
noteId: noteId as string
|
||||
};
|
||||
}
|
||||
const noteIdFromSessionId =
|
||||
!forSessionId || forSessionId.startsWith("session")
|
||||
? null
|
||||
: forSessionId.split("_")[0];
|
||||
|
||||
const noteId = noteIdFromSessionId || currentNote.current?.id;
|
||||
const params: SavePayload = {
|
||||
title,
|
||||
data: content,
|
||||
type: "tiptap",
|
||||
sessionId: forSessionId,
|
||||
id: noteId,
|
||||
sessionHistoryId: sessionHistoryId.current,
|
||||
ignoreEdit
|
||||
ignoreEdit,
|
||||
sessionHistoryId: noteId ? editorSessionHistory.get(noteId) : undefined,
|
||||
tabId: tabId
|
||||
};
|
||||
withTimer(
|
||||
noteId || "newnote",
|
||||
() => {
|
||||
if (
|
||||
currentNote.current &&
|
||||
!params.id &&
|
||||
params.sessionId === forSessionId
|
||||
) {
|
||||
params.id = currentNote.current?.id;
|
||||
if (!params.id) {
|
||||
params.id = useTabStore.getState().getNoteIdForTab(tabId);
|
||||
}
|
||||
if (onChange && params.data) {
|
||||
onChange(params.data);
|
||||
@@ -572,13 +597,12 @@ export const useEditor = (
|
||||
ignoreEdit ? 0 : 150
|
||||
);
|
||||
},
|
||||
[withTimer, onChange, saveNote]
|
||||
[editorSessionHistory, withTimer, onChange, saveNote]
|
||||
);
|
||||
|
||||
const restoreEditorState = useCallback(async () => {
|
||||
const appState = getAppState();
|
||||
if (!appState) return;
|
||||
overlay(true, appState.note);
|
||||
state.current.isRestoringState = true;
|
||||
state.current.currentlyEditing = true;
|
||||
state.current.movedAway = false;
|
||||
@@ -604,7 +628,7 @@ export const useEditor = (
|
||||
}
|
||||
clearAppState();
|
||||
state.current.isRestoringState = false;
|
||||
}, [loadNote, overlay]);
|
||||
}, [loadNote]);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eOnLoadNote + editorId, loadNote);
|
||||
@@ -622,7 +646,13 @@ export const useEditor = (
|
||||
}, [loading]);
|
||||
|
||||
const onReady = useCallback(async () => {
|
||||
if (!(await isEditorLoaded(editorRef, sessionIdRef.current))) {
|
||||
if (
|
||||
!(await isEditorLoaded(
|
||||
editorRef,
|
||||
sessionIdRef.current,
|
||||
useTabStore.getState().currentTab
|
||||
))
|
||||
) {
|
||||
eSendEvent("webview_reset", "onReady");
|
||||
return false;
|
||||
} else {
|
||||
@@ -632,23 +662,24 @@ export const useEditor = (
|
||||
}, [isDefaultEditor, restoreEditorState]);
|
||||
|
||||
const onLoad = useCallback(async () => {
|
||||
if (currentNote.current) overlay(true);
|
||||
if (currentNotes.current) overlay(true);
|
||||
clearTimeout(timers.current["editor:loaded"]);
|
||||
timers.current["editor:loaded"] = setTimeout(async () => {
|
||||
postMessage(EditorEvents.theme, theme);
|
||||
|
||||
commands.setInsets(
|
||||
isDefaultEditor ? insets : { top: 0, left: 0, right: 0, bottom: 0 }
|
||||
);
|
||||
await commands.setSessionId(sessionIdRef.current);
|
||||
await commands.setSettings();
|
||||
timers.current["editor:loaded"] = setTimeout(async () => {
|
||||
if (!state.current.ready && (await onReady())) {
|
||||
state.current.ready = true;
|
||||
}
|
||||
if (currentNote.current) {
|
||||
loadNote({ ...currentNote.current, forced: true });
|
||||
} else {
|
||||
await commands.setPlaceholder(placeholderTip.current);
|
||||
overlay(false);
|
||||
|
||||
const noteId = useTabStore.getState().getCurrentNoteId();
|
||||
if (noteId && currentNotes.current[noteId]) {
|
||||
loadNote({ ...currentNotes.current[noteId], forced: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -672,7 +703,7 @@ export const useEditor = (
|
||||
setLoading,
|
||||
state,
|
||||
sessionId: sessionIdRef,
|
||||
note: currentNote,
|
||||
note: currentNotes,
|
||||
onReady,
|
||||
saveContent,
|
||||
onContentChanged,
|
||||
|
||||
140
apps/mobile/app/screens/editor/tiptap/use-tab-store.ts
Normal file
140
apps/mobile/app/screens/editor/tiptap/use-tab-store.ts
Normal 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;
|
||||
}
|
||||
}));
|
||||
@@ -65,14 +65,16 @@ export function makeSessionId(id?: string) {
|
||||
|
||||
export async function isEditorLoaded(
|
||||
ref: RefObject<WebView>,
|
||||
sessionId: string
|
||||
sessionId: string,
|
||||
tabId: number
|
||||
) {
|
||||
return await post(ref, sessionId, EditorEvents.status);
|
||||
return await post(ref, sessionId, tabId, EditorEvents.status);
|
||||
}
|
||||
|
||||
export async function post<T>(
|
||||
ref: RefObject<WebView>,
|
||||
sessionId: string,
|
||||
tabId: number,
|
||||
type: string,
|
||||
value: T | null = null,
|
||||
waitFor = 300
|
||||
@@ -84,7 +86,8 @@ export async function post<T>(
|
||||
const message = {
|
||||
type,
|
||||
value,
|
||||
sessionId: sessionId
|
||||
sessionId: sessionId,
|
||||
tabId
|
||||
};
|
||||
setImmediate(() => ref.current?.postMessage(JSON.stringify(message)));
|
||||
const response = await getResponse(type, waitFor);
|
||||
|
||||
@@ -27,7 +27,7 @@ import { useTagStore } from "../../stores/use-tag-store";
|
||||
import { eOnLoadNote, eOnNotebookUpdated } from "../../utils/events";
|
||||
import { openLinkInBrowser } from "../../utils/functions";
|
||||
import { tabBarRef } from "../../utils/global-refs";
|
||||
import { editorController, editorState } from "../editor/tiptap/utils";
|
||||
import { editorState } from "../editor/tiptap/utils";
|
||||
|
||||
export const PLACEHOLDER_DATA = {
|
||||
title: "Your notes",
|
||||
@@ -52,11 +52,9 @@ export function openMonographsWebpage() {
|
||||
|
||||
export function openEditor() {
|
||||
if (!DDS.isTab) {
|
||||
if (editorController.current?.note) {
|
||||
eSendEvent(eOnLoadNote, { newNote: true });
|
||||
editorState().currentlyEditing = true;
|
||||
editorState().movedAway = false;
|
||||
}
|
||||
eSendEvent(eOnLoadNote, { newNote: true });
|
||||
editorState().currentlyEditing = true;
|
||||
editorState().movedAway = false;
|
||||
tabBarRef.current?.goToPage(1);
|
||||
} else {
|
||||
eSendEvent(eOnLoadNote, { newNote: true });
|
||||
|
||||
@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import create from "zustand";
|
||||
import { editorController } from "../screens/editor/tiptap/utils";
|
||||
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
|
||||
|
||||
export type AttachmentGroupProgress = {
|
||||
total: number;
|
||||
@@ -66,10 +67,13 @@ export const useAttachmentStore = create<AttachmentStore>((set, get) => ({
|
||||
remove: (hash) => {
|
||||
const progress = get().progress;
|
||||
if (!progress) return;
|
||||
editorController.current?.commands.setAttachmentProgress({
|
||||
hash: hash,
|
||||
progress: 100
|
||||
});
|
||||
editorController.current?.commands.setAttachmentProgress(
|
||||
{
|
||||
hash: hash,
|
||||
progress: 100
|
||||
},
|
||||
useTabStore.getState().currentTab
|
||||
);
|
||||
progress[hash] = null;
|
||||
set({ progress: { ...progress } });
|
||||
},
|
||||
@@ -80,11 +84,14 @@ export const useAttachmentStore = create<AttachmentStore>((set, get) => ({
|
||||
const progressPercentage =
|
||||
type === "upload" ? sent / total : recieved / total;
|
||||
|
||||
editorController.current?.commands.setAttachmentProgress({
|
||||
hash: hash,
|
||||
//@ts-ignore
|
||||
progress: Math.round(Math.max(progressPercentage * 100, 0))
|
||||
});
|
||||
editorController.current?.commands.setAttachmentProgress(
|
||||
{
|
||||
hash: hash,
|
||||
//@ts-ignore
|
||||
progress: Math.round(Math.max(progressPercentage * 100, 0))
|
||||
},
|
||||
useTabStore.getState().currentTab
|
||||
);
|
||||
set({ progress: { ...progress } });
|
||||
},
|
||||
encryptionProgress: 0,
|
||||
|
||||
@@ -18,15 +18,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import create, { State } from "zustand";
|
||||
import { eSubscribeEvent, eUnSubscribeEvent } from "../services/event-manager";
|
||||
|
||||
export interface EditorStore extends State {
|
||||
currentEditingNote: string | null;
|
||||
setCurrentlyEditingNote: (note: string | null) => void;
|
||||
sessionId: string | null;
|
||||
setSessionId: (sessionId: string | null) => void;
|
||||
searchReplace: boolean;
|
||||
setSearchReplace: (searchReplace: boolean) => void;
|
||||
searchSelection: string | null;
|
||||
readonly: boolean;
|
||||
setReadonly: (readonly: boolean) => void;
|
||||
@@ -39,34 +36,9 @@ export const useEditorStore = create<EditorStore>((set, get) => ({
|
||||
setSessionId: (sessionId) => {
|
||||
set({ sessionId });
|
||||
},
|
||||
searchReplace: false,
|
||||
searchSelection: null,
|
||||
readonly: false,
|
||||
setReadonly: (readonly) => {
|
||||
set({ readonly: readonly });
|
||||
},
|
||||
setSearchReplace: (searchReplace) => {
|
||||
if (!searchReplace) {
|
||||
set({ searchSelection: null, searchReplace: false });
|
||||
return;
|
||||
}
|
||||
const func = (value: string) => {
|
||||
eUnSubscribeEvent("selectionvalue", func);
|
||||
|
||||
if (!value && get().searchReplace) {
|
||||
// endSearch();
|
||||
return;
|
||||
}
|
||||
set({ searchSelection: value, searchReplace: true });
|
||||
};
|
||||
eSubscribeEvent("selectionvalue", func);
|
||||
// tiny.call(
|
||||
// EditorWebView,
|
||||
// `(function() {
|
||||
// if (editor) {
|
||||
// reactNativeEventHandler('selectionvalue',editor.selection.getContent());
|
||||
// }
|
||||
// })();`
|
||||
// );
|
||||
}
|
||||
}));
|
||||
|
||||
40
packages/editor-mobile/package-lock.json
generated
40
packages/editor-mobile/package-lock.json
generated
@@ -19,6 +19,8 @@
|
||||
"mdi-react": "9.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-freeze": "^1.0.3",
|
||||
"tinycolor2": "1.6.0",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -4358,7 +4360,7 @@
|
||||
"version": "15.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
|
||||
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@types/q": {
|
||||
"version": "1.5.8",
|
||||
@@ -4382,7 +4384,7 @@
|
||||
"version": "18.2.39",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.39.tgz",
|
||||
"integrity": "sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/scheduler": "*",
|
||||
@@ -4417,7 +4419,7 @@
|
||||
"version": "0.16.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
|
||||
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@types/semver": {
|
||||
"version": "7.5.6",
|
||||
@@ -9713,7 +9715,7 @@
|
||||
"version": "9.0.21",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
|
||||
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
@@ -15627,6 +15629,17 @@
|
||||
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/react-freeze": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.3.tgz",
|
||||
"integrity": "sha512-ZnXwLQnGzrDpHBHiC56TXFXvmolPeMjTn1UOm610M4EXGzbEDR7oOIyS2ZiItgbs6eZc4oU/a0hpk8PrcKvv5g==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
@@ -17496,6 +17509,11 @@
|
||||
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tinycolor2": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
|
||||
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="
|
||||
},
|
||||
"node_modules/tmpl": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
|
||||
@@ -17758,6 +17776,20 @@
|
||||
"is-typedarray": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unbox-primitive": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
"mdi-react": "9.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-freeze": "^1.0.3",
|
||||
"tinycolor2": "1.6.0",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -17,17 +17,19 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Global, css } from "@emotion/react";
|
||||
import {
|
||||
ScopedThemeProvider,
|
||||
themeToCSS,
|
||||
useThemeEngineStore
|
||||
} from "@notesnook/theme";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Freeze } from "react-freeze";
|
||||
import "./App.css";
|
||||
import Tiptap from "./components/editor";
|
||||
import { TabContext, useTabStore } from "./hooks/useTabStore";
|
||||
import { EmotionEditorTheme } from "./theme-factory";
|
||||
import { Global, css } from "@emotion/react";
|
||||
import { useMemo } from "react";
|
||||
import { getTheme } from "./utils";
|
||||
import { EventTypes, getTheme } from "./utils";
|
||||
|
||||
const currentTheme = getTheme();
|
||||
if (currentTheme) {
|
||||
@@ -35,11 +37,29 @@ if (currentTheme) {
|
||||
}
|
||||
|
||||
function App(): JSX.Element {
|
||||
const tabs = useTabStore((state) => state.tabs);
|
||||
const currentTab = useTabStore((state) => state.currentTab);
|
||||
|
||||
useEffect(() => {
|
||||
post(EventTypes.tabsChanged, {
|
||||
tabs: tabs,
|
||||
currentTab: currentTab
|
||||
});
|
||||
}, [tabs, currentTab]);
|
||||
|
||||
logger("info", "opened tabs count", tabs);
|
||||
|
||||
return (
|
||||
<ScopedThemeProvider value="base">
|
||||
<EmotionEditorTheme>
|
||||
<GlobalStyles />
|
||||
<Tiptap />
|
||||
{tabs.map((tab) => (
|
||||
<TabContext.Provider key={tab.id} value={tab}>
|
||||
<Freeze freeze={currentTab !== tab.id}>
|
||||
<Tiptap />
|
||||
</Freeze>
|
||||
</TabContext.Provider>
|
||||
))}
|
||||
</EmotionEditorTheme>
|
||||
</ScopedThemeProvider>
|
||||
);
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
usePermissionHandler,
|
||||
useTiptap
|
||||
} from "@notesnook/editor";
|
||||
import { toBlobURL } from "@notesnook/editor/dist/utils/downloader";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import {
|
||||
forwardRef,
|
||||
@@ -36,17 +37,19 @@ import {
|
||||
} from "react";
|
||||
import { useEditorController } from "../hooks/useEditorController";
|
||||
import { useSettings } from "../hooks/useSettings";
|
||||
import { TabStore, useTabContext, useTabStore } from "../hooks/useTabStore";
|
||||
import { EmotionEditorToolbarTheme } from "../theme-factory";
|
||||
import { EventTypes, Settings } from "../utils";
|
||||
import Header from "./header";
|
||||
import StatusBar from "./statusbar";
|
||||
import Tags from "./tags";
|
||||
import Title from "./title";
|
||||
import { toBlobURL } from "@notesnook/editor/dist/utils/downloader";
|
||||
|
||||
globalThis.toBlobURL = toBlobURL as typeof globalThis.toBlobURL;
|
||||
|
||||
const Tiptap = ({ settings }: { settings: Settings }) => {
|
||||
const tab = useTabContext();
|
||||
const isFocused = useTabStore((state) => state.currentTab === tab?.id);
|
||||
const [tick, setTick] = useState(0);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -56,45 +59,48 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
|
||||
premium: settings.premium
|
||||
},
|
||||
onPermissionDenied: () => {
|
||||
post(EventTypes.pro);
|
||||
post(EventTypes.pro, undefined, tab.id, tab.noteId);
|
||||
}
|
||||
});
|
||||
|
||||
const _editor = useTiptap(
|
||||
{
|
||||
onUpdate: ({ editor, transaction }) => {
|
||||
global.editorController.contentChange(
|
||||
globalThis.editorControllers[tab.id]?.contentChange(
|
||||
editor as Editor,
|
||||
transaction.getMeta("ignoreEdit")
|
||||
);
|
||||
},
|
||||
onOpenAttachmentPicker: (editor, type) => {
|
||||
global.editorController.openFilePicker(type);
|
||||
globalThis.editorControllers[tab.id]?.openFilePicker(type);
|
||||
return true;
|
||||
},
|
||||
onDownloadAttachment: (editor, attachment) => {
|
||||
global.editorController.downloadAttachment(attachment);
|
||||
globalThis.editorControllers[tab.id]?.downloadAttachment(attachment);
|
||||
return true;
|
||||
},
|
||||
onPreviewAttachment(editor, attachment) {
|
||||
global.editorController.previewAttachment(attachment);
|
||||
globalThis.editorControllers[tab.id]?.previewAttachment(attachment);
|
||||
return true;
|
||||
},
|
||||
getAttachmentData(attachment) {
|
||||
return global.editorController.getAttachmentData(attachment);
|
||||
return globalThis.editorControllers[tab.id]?.getAttachmentData(
|
||||
attachment
|
||||
) as Promise<string | undefined>;
|
||||
},
|
||||
element: !layout ? undefined : contentRef.current || undefined,
|
||||
editable: !settings.readonly,
|
||||
editorProps: {
|
||||
editable: () => !settings.readonly
|
||||
},
|
||||
content: global.editorController?.content?.current,
|
||||
content: globalThis.editorControllers[tab.id]?.content?.current,
|
||||
isMobile: true,
|
||||
doubleSpacedLines: settings.doubleSpacedLines,
|
||||
onOpenLink: (url) => {
|
||||
return global.editorController.openLink(url);
|
||||
return globalThis.editorControllers[tab.id]?.openLink(url) || true;
|
||||
},
|
||||
copyToClipboard: (text) => {
|
||||
globalThis.editorController.copyToClipboard(text);
|
||||
globalThis.editorControllers[tab.id]?.copyToClipboard(text);
|
||||
},
|
||||
downloadOptions: {
|
||||
corsHost: settings.corsProxy
|
||||
@@ -118,17 +124,45 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
|
||||
left: 0,
|
||||
top: 0
|
||||
});
|
||||
globalThis.editorController.setTitlePlaceholder("Note title");
|
||||
}, []);
|
||||
globalThis.editorControllers[tab.id]?.setTitlePlaceholder("Note title");
|
||||
}, [tab.id]);
|
||||
|
||||
const controller = useEditorController(update);
|
||||
const controllerRef = useRef(controller);
|
||||
globalThis.editorController = controller;
|
||||
globalThis.editor = _editor;
|
||||
|
||||
globalThis.editorControllers[tab.id] = controller;
|
||||
globalThis.editors[tab.id] = _editor;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setLayout(true);
|
||||
}, []);
|
||||
|
||||
const updateScrollPosition = (state: TabStore) => {
|
||||
if (state.currentTab === tab.id) {
|
||||
const position = state.scrollPosition[tab?.id];
|
||||
if (position) {
|
||||
containerRef.current?.scrollTo({
|
||||
left: 0,
|
||||
top: position,
|
||||
behavior: "auto"
|
||||
});
|
||||
}
|
||||
post(
|
||||
EventTypes.tabFocused,
|
||||
!!globalThis.editorControllers[tab.id]?.content.current,
|
||||
tab.id,
|
||||
tab.noteId
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
updateScrollPosition(useTabStore.getState());
|
||||
const unsub = useTabStore.subscribe((state) => {
|
||||
updateScrollPosition(state);
|
||||
});
|
||||
return () => {
|
||||
unsub();
|
||||
};
|
||||
}, [tab.id, tab.noteId]);
|
||||
|
||||
const onClickEmptyArea: React.MouseEventHandler<HTMLDivElement> = useCallback(
|
||||
(event) => {
|
||||
@@ -149,15 +183,17 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstChild = globalThis.editor?.state.doc.firstChild;
|
||||
const editor = editors[tab.id];
|
||||
|
||||
const firstChild = editor?.state.doc.firstChild;
|
||||
const isParagraph = firstChild?.type.name === "paragraph";
|
||||
const isFirstChildEmpty =
|
||||
!firstChild?.textContent || firstChild?.textContent?.length === 0;
|
||||
if (isParagraph && isFirstChildEmpty) {
|
||||
globalThis.editor?.commands.focus("end");
|
||||
editor?.commands.focus("end");
|
||||
return;
|
||||
}
|
||||
globalThis.editor
|
||||
editor
|
||||
?.chain()
|
||||
.insertContentAt(0, "<p></p>", {
|
||||
updateSelection: true
|
||||
@@ -166,33 +202,35 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
|
||||
.run();
|
||||
}
|
||||
},
|
||||
[]
|
||||
[tab.id]
|
||||
);
|
||||
|
||||
const onClickBottomArea = useCallback(() => {
|
||||
const docSize = globalThis.editor?.state.doc.content.size;
|
||||
const editor = editors[tab.id];
|
||||
const docSize = editor?.state.doc.content.size;
|
||||
if (!docSize) return;
|
||||
const lastChild = globalThis.editor?.state.doc.lastChild;
|
||||
const lastChild = editor?.state.doc.lastChild;
|
||||
const isParagraph = lastChild?.type.name === "paragraph";
|
||||
const isLastChildEmpty =
|
||||
!lastChild?.textContent || lastChild?.textContent?.length === 0;
|
||||
if (isParagraph && isLastChildEmpty) {
|
||||
globalThis.editor?.commands.focus("end");
|
||||
editor?.commands.focus("end");
|
||||
return;
|
||||
}
|
||||
globalThis.editor
|
||||
editor
|
||||
?.chain()
|
||||
.insertContentAt(docSize - 1, "<p></p>", {
|
||||
updateSelection: true
|
||||
})
|
||||
.focus("end")
|
||||
.run();
|
||||
}, []);
|
||||
}, [tab.id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
display: isFocused ? "flex" : "none",
|
||||
flex: 1,
|
||||
flexDirection: "column",
|
||||
maxWidth: "100vw"
|
||||
|
||||
@@ -18,14 +18,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import ArrowBackIcon from "mdi-react/ArrowBackIcon";
|
||||
import CrownIcon from "mdi-react/CrownIcon";
|
||||
import DotsHorizontalIcon from "mdi-react/DotsHorizontalIcon";
|
||||
import ArrowULeftTopIcon from "mdi-react/ArrowULeftTopIcon";
|
||||
import ArrowURightTopIcon from "mdi-react/ArrowURightTopIcon";
|
||||
import CrownIcon from "mdi-react/CrownIcon";
|
||||
import DotsHorizontalIcon from "mdi-react/DotsHorizontalIcon";
|
||||
import FullscreenIcon from "mdi-react/FullscreenIcon";
|
||||
import MagnifyIcon from "mdi-react/MagnifyIcon";
|
||||
import React from "react";
|
||||
import { useSafeArea } from "../hooks/useSafeArea";
|
||||
import { useTabContext, useTabStore } from "../hooks/useTabStore";
|
||||
import { EventTypes, Settings } from "../utils";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
@@ -65,7 +66,11 @@ function Header({
|
||||
hasUndo: boolean;
|
||||
hasRedo: boolean;
|
||||
}): JSX.Element {
|
||||
const tab = useTabContext();
|
||||
const editor = editors[tab.id];
|
||||
const insets = useSafeArea();
|
||||
const openedTabsCount = useTabStore((state) => state.tabs.length);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -95,7 +100,7 @@ function Header({
|
||||
) : (
|
||||
<Button
|
||||
onPress={() => {
|
||||
post(EventTypes.back);
|
||||
post(EventTypes.back, undefined, tab.id, tab.noteId);
|
||||
}}
|
||||
preventDefault={false}
|
||||
style={{
|
||||
@@ -113,7 +118,7 @@ function Header({
|
||||
}}
|
||||
>
|
||||
<ArrowBackIcon
|
||||
size={27 * settings.fontScale}
|
||||
size={28 * settings.fontScale}
|
||||
style={{
|
||||
position: "absolute"
|
||||
}}
|
||||
@@ -235,7 +240,7 @@ function Header({
|
||||
}}
|
||||
>
|
||||
<MagnifyIcon
|
||||
size={25 * settings.fontScale}
|
||||
size={28 * settings.fontScale}
|
||||
style={{
|
||||
position: "absolute"
|
||||
}}
|
||||
@@ -246,7 +251,7 @@ function Header({
|
||||
{settings.deviceMode !== "mobile" && !settings.fullscreen ? (
|
||||
<Button
|
||||
onPress={() => {
|
||||
post(EventTypes.fullscreen);
|
||||
post(EventTypes.fullscreen, undefined, tab.id, tab.noteId);
|
||||
}}
|
||||
preventDefault={false}
|
||||
style={{
|
||||
@@ -274,7 +279,46 @@ function Header({
|
||||
|
||||
<Button
|
||||
onPress={() => {
|
||||
post(EventTypes.properties);
|
||||
post(EventTypes.showTabs, undefined, tab.id, tab.noteId);
|
||||
}}
|
||||
preventDefault={false}
|
||||
style={{
|
||||
borderWidth: 0,
|
||||
borderRadius: 100,
|
||||
color: "var(--nn_primary_icon)",
|
||||
marginRight: 12,
|
||||
width: 39,
|
||||
height: 39,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
position: "relative"
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
border: "2.5px solid var(--nn_primary_icon)",
|
||||
width: 20 * settings.fontScale,
|
||||
height: 20 * settings.fontScale,
|
||||
borderRadius: 5,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
fontSize: 15 * settings.fontScale
|
||||
}}
|
||||
>
|
||||
{openedTabsCount}
|
||||
</p>
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onPress={() => {
|
||||
post(EventTypes.properties, undefined, tab.id, tab.noteId);
|
||||
}}
|
||||
preventDefault={false}
|
||||
style={{
|
||||
|
||||
@@ -19,12 +19,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { RefObject, useEffect, useRef, useState } from "react";
|
||||
import { getTotalWords, Editor } from "@notesnook/editor";
|
||||
import { useTabContext } from "../hooks/useTabStore";
|
||||
|
||||
function StatusBar({ container }: { container: RefObject<HTMLDivElement> }) {
|
||||
const [status, setStatus] = useState({
|
||||
date: "",
|
||||
saved: ""
|
||||
});
|
||||
const tab = useTabContext();
|
||||
const [sticky, setSticky] = useState(false);
|
||||
const stickyRef = useRef(false);
|
||||
const prevScroll = useRef(0);
|
||||
@@ -34,6 +36,7 @@ function StatusBar({ container }: { container: RefObject<HTMLDivElement> }) {
|
||||
const statusBar = useRef({
|
||||
set: setStatus,
|
||||
updateWords: () => {
|
||||
const editor = editors[tab.id];
|
||||
const words = getTotalWords(editor as Editor) + " words";
|
||||
if (currentWords.current === words) return;
|
||||
setWords(words);
|
||||
@@ -43,7 +46,13 @@ function StatusBar({ container }: { container: RefObject<HTMLDivElement> }) {
|
||||
setWords(`0 words`);
|
||||
}
|
||||
});
|
||||
globalThis.statusBar = statusBar;
|
||||
|
||||
useEffect(() => {
|
||||
globalThis.statusBars[tab.id] = statusBar;
|
||||
return () => {
|
||||
globalThis.statusBars[tab.id] = undefined;
|
||||
};
|
||||
}, [tab.id, statusBar]);
|
||||
|
||||
const onScroll = React.useCallback((event: Event) => {
|
||||
const currentOffset = (event.target as HTMLElement)?.scrollTop;
|
||||
|
||||
@@ -17,26 +17,34 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useRef, useState } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { EventTypes, Settings } from "../utils";
|
||||
import styles from "./styles.module.css";
|
||||
import { useTabContext } from "../hooks/useTabStore";
|
||||
|
||||
function Tags(props: { settings: Settings }): JSX.Element {
|
||||
const [tags, setTags] = useState<
|
||||
{ title: string; alias: string; id: string; type: "tag" }[]
|
||||
>([]);
|
||||
const editorTags = useRef({
|
||||
const tagsRef = useRef({
|
||||
setTags: setTags
|
||||
});
|
||||
const tab = useTabContext();
|
||||
|
||||
global.editorTags = editorTags;
|
||||
useEffect(() => {
|
||||
globalThis.editorTags[tab.id] = tagsRef;
|
||||
return () => {
|
||||
globalThis.editorTags[tab.id] = undefined;
|
||||
};
|
||||
}, [tab.id, tagsRef]);
|
||||
|
||||
const openManageTagsSheet = () => {
|
||||
const editor = editors[tab.id];
|
||||
if (editor?.isFocused) {
|
||||
editor.commands.blur();
|
||||
editorTitle.current?.blur();
|
||||
editorTitles[tab.id]?.current?.blur();
|
||||
}
|
||||
post(EventTypes.newtag);
|
||||
post(EventTypes.newtag, undefined, tab.id, tab.noteId);
|
||||
};
|
||||
const fontScale = props.settings?.fontScale || 1;
|
||||
|
||||
@@ -116,7 +124,7 @@ function Tags(props: { settings: Settings }): JSX.Element {
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
post(EventTypes.tag, tag);
|
||||
post(EventTypes.tag, tag, tab.id, tab.noteId);
|
||||
}}
|
||||
>
|
||||
#{tag.alias}
|
||||
|
||||
@@ -22,6 +22,7 @@ import React, { RefObject, useCallback, useEffect, useRef } from "react";
|
||||
import { EditorController } from "../hooks/useEditorController";
|
||||
import styles from "./styles.module.css";
|
||||
import { replaceDateTime } from "@notesnook/editor/dist/extensions/date-time";
|
||||
import { useTabContext } from "../hooks/useTabStore";
|
||||
function Title({
|
||||
controller,
|
||||
title,
|
||||
@@ -39,10 +40,10 @@ function Title({
|
||||
dateFormat: string;
|
||||
timeFormat: string;
|
||||
}) {
|
||||
const tab = useTabContext();
|
||||
const titleRef = useRef<HTMLTextAreaElement>(null);
|
||||
const titleSizeDiv = useRef<HTMLDivElement>(null);
|
||||
const emitUpdate = useRef(true);
|
||||
global.editorTitle = titleRef;
|
||||
|
||||
const resizeTextarea = useCallback(() => {
|
||||
if (!titleSizeDiv.current || !titleRef.current) return;
|
||||
@@ -65,6 +66,13 @@ function Title({
|
||||
};
|
||||
}, [resizeTextarea, title]);
|
||||
|
||||
useEffect(() => {
|
||||
globalThis.editorTitles[tab.id] = titleRef;
|
||||
return () => {
|
||||
globalThis.editorTitles[tab.id] = undefined;
|
||||
};
|
||||
}, [tab.id, titleRef]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@@ -121,6 +129,7 @@ function Title({
|
||||
resizeTextarea();
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
const editor = editors[tab.id];
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -32,6 +32,8 @@ import {
|
||||
} from "react";
|
||||
import { EventTypes, isReactNative, post, randId, saveTheme } from "../utils";
|
||||
import { injectCss, transform } from "../utils/css";
|
||||
import { useTabContext, useTabStore } from "./useTabStore";
|
||||
|
||||
type Attachment = {
|
||||
hash: string;
|
||||
filename: string;
|
||||
@@ -105,6 +107,7 @@ export type EditorController = {
|
||||
};
|
||||
|
||||
export function useEditorController(update: () => void): EditorController {
|
||||
const tab = useTabContext();
|
||||
const setTheme = useThemeEngineStore((store) => store.setTheme);
|
||||
const { colors } = useThemeColors("editor");
|
||||
const [title, setTitle] = useState("");
|
||||
@@ -118,20 +121,26 @@ export function useEditorController(update: () => void): EditorController {
|
||||
|
||||
const selectionChange = useCallback((_editor: Editor) => {}, []);
|
||||
|
||||
const titleChange = useCallback((title: string) => {
|
||||
post(EventTypes.contentchange);
|
||||
post(EventTypes.title, title);
|
||||
}, []);
|
||||
const titleChange = useCallback(
|
||||
(title: string) => {
|
||||
post(EventTypes.contentchange, undefined, tab.id, tab.noteId);
|
||||
post(EventTypes.title, title, tab.id, tab.noteId);
|
||||
},
|
||||
[tab.id, tab.noteId]
|
||||
);
|
||||
|
||||
const countWords = useCallback((ms = 300) => {
|
||||
if (typeof timers.current.wordCounter === "number")
|
||||
clearTimeout(timers.current.wordCounter);
|
||||
timers.current.wordCounter = setTimeout(() => {
|
||||
console.time("wordCounter");
|
||||
statusBar?.current?.updateWords();
|
||||
console.timeEnd("wordCounter");
|
||||
}, ms);
|
||||
}, []);
|
||||
const countWords = useCallback(
|
||||
(ms = 300) => {
|
||||
if (typeof timers.current.wordCounter === "number")
|
||||
clearTimeout(timers.current.wordCounter);
|
||||
timers.current.wordCounter = setTimeout(() => {
|
||||
console.time("wordCounter");
|
||||
statusBars[tab.id]?.current?.updateWords();
|
||||
console.timeEnd("wordCounter");
|
||||
}, ms);
|
||||
},
|
||||
[tab.id]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
injectCss(transform(colors));
|
||||
@@ -145,29 +154,33 @@ export function useEditorController(update: () => void): EditorController {
|
||||
if (typeof timers.current.change === "number") {
|
||||
clearTimeout(timers.current?.change);
|
||||
}
|
||||
timers.current.change = setTimeout(
|
||||
() => {
|
||||
htmlContentRef.current = editor.getHTML();
|
||||
post(
|
||||
EventTypes.content,
|
||||
{
|
||||
html: htmlContentRef.current,
|
||||
ignoreEdit: ignoreEdit
|
||||
},
|
||||
currentSessionId
|
||||
);
|
||||
},
|
||||
ignoreEdit ? 0 : 300
|
||||
);
|
||||
timers.current.change = setTimeout(() => {
|
||||
htmlContentRef.current = editor.getHTML();
|
||||
post(
|
||||
EventTypes.content,
|
||||
{
|
||||
html: htmlContentRef.current,
|
||||
ignoreEdit: ignoreEdit
|
||||
},
|
||||
tab.id,
|
||||
tab.noteId,
|
||||
currentSessionId
|
||||
);
|
||||
}, 300);
|
||||
|
||||
countWords(5000);
|
||||
},
|
||||
[countWords]
|
||||
[countWords, tab.id, tab.noteId]
|
||||
);
|
||||
|
||||
const scroll = useCallback(
|
||||
(_event: React.UIEvent<HTMLDivElement, UIEvent>) => {},
|
||||
[]
|
||||
(_event: React.UIEvent<HTMLDivElement, UIEvent>) => {
|
||||
if (!tab) return;
|
||||
useTabStore
|
||||
.getState()
|
||||
.setScrollPosition(tab.id, _event.currentTarget.scrollTop);
|
||||
},
|
||||
[tab]
|
||||
);
|
||||
|
||||
const onUpdate = useCallback(() => {
|
||||
@@ -177,19 +190,34 @@ export function useEditorController(update: () => void): EditorController {
|
||||
const onMessage = useCallback(
|
||||
(event: Event & { data?: string }) => {
|
||||
if (event?.data?.[0] !== "{") return;
|
||||
|
||||
const message = JSON.parse(event.data);
|
||||
const type = message.type;
|
||||
const value = message.value;
|
||||
global.sessionId = message.sessionId;
|
||||
|
||||
if (message.tabId !== tab.id) {
|
||||
logger("info", "tab id not matched");
|
||||
return;
|
||||
}
|
||||
|
||||
logger(
|
||||
"info",
|
||||
"webview message for tab",
|
||||
message.type,
|
||||
tab.id,
|
||||
message.tabId
|
||||
);
|
||||
|
||||
const editor = editors[tab.id];
|
||||
switch (type) {
|
||||
case "native:updatehtml": {
|
||||
htmlContentRef.current = value;
|
||||
if (!editor) break;
|
||||
const { from, to } = editor.state.selection;
|
||||
|
||||
editor?.commands.setContent(htmlContentRef.current, false, {
|
||||
preserveWhitespace: true
|
||||
});
|
||||
|
||||
editor.commands.setTextSelection({
|
||||
from,
|
||||
to
|
||||
@@ -198,6 +226,7 @@ export function useEditorController(update: () => void): EditorController {
|
||||
break;
|
||||
}
|
||||
case "native:html":
|
||||
// logger("info", "loading html", htmlContentRef.current);
|
||||
htmlContentRef.current = value;
|
||||
update();
|
||||
countWords();
|
||||
@@ -232,7 +261,7 @@ export function useEditorController(update: () => void): EditorController {
|
||||
}
|
||||
post(type); // Notify that message was delivered successfully.
|
||||
},
|
||||
[update, countWords, setTheme]
|
||||
[tab, update, countWords, setTheme]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -249,20 +278,32 @@ export function useEditorController(update: () => void): EditorController {
|
||||
};
|
||||
}, [onMessage]);
|
||||
|
||||
const openFilePicker = useCallback((type: "image" | "file" | "camera") => {
|
||||
post(EventTypes.filepicker, type);
|
||||
}, []);
|
||||
const openFilePicker = useCallback(
|
||||
(type: "image" | "file" | "camera") => {
|
||||
post(EventTypes.filepicker, type, tab.id, tab.noteId);
|
||||
},
|
||||
[tab.id, tab.noteId]
|
||||
);
|
||||
|
||||
const downloadAttachment = useCallback((attachment: Attachment) => {
|
||||
post(EventTypes.download, attachment);
|
||||
}, []);
|
||||
const previewAttachment = useCallback((attachment: Attachment) => {
|
||||
post(EventTypes.previewAttachment, attachment);
|
||||
}, []);
|
||||
const openLink = useCallback((url: string) => {
|
||||
post(EventTypes.link, url);
|
||||
return true;
|
||||
}, []);
|
||||
const downloadAttachment = useCallback(
|
||||
(attachment: Attachment) => {
|
||||
post(EventTypes.download, attachment, tab.id, tab.noteId);
|
||||
},
|
||||
[tab.id, tab.noteId]
|
||||
);
|
||||
const previewAttachment = useCallback(
|
||||
(attachment: Attachment) => {
|
||||
post(EventTypes.previewAttachment, attachment, tab.id, tab.noteId);
|
||||
},
|
||||
[tab.id, tab.noteId]
|
||||
);
|
||||
const openLink = useCallback(
|
||||
(url: string) => {
|
||||
post(EventTypes.link, url, tab.id, tab.noteId);
|
||||
return true;
|
||||
},
|
||||
[tab.id, tab.noteId]
|
||||
);
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
post(EventTypes.copyToClipboard, text);
|
||||
|
||||
206
packages/editor-mobile/src/hooks/useTabStore.ts
Normal file
206
packages/editor-mobile/src/hooks/useTabStore.ts
Normal 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;
|
||||
};
|
||||
@@ -17,11 +17,12 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { ToolbarGroupDefinition } from "@notesnook/editor";
|
||||
import { Editor } from "@notesnook/editor";
|
||||
import { Dispatch, MutableRefObject, RefObject, SetStateAction } from "react";
|
||||
import { useEditorController } from "../hooks/useEditorController";
|
||||
import { Editor, ToolbarGroupDefinition } from "@notesnook/editor";
|
||||
import { ThemeDefinition } from "@notesnook/theme";
|
||||
import { Dispatch, MutableRefObject, RefObject, SetStateAction } from "react";
|
||||
import { EditorController } from "../hooks/useEditorController";
|
||||
|
||||
globalThis.sessionId = "notesnook-editor";
|
||||
|
||||
globalThis.pendingResolvers = {};
|
||||
export function randId(prefix: string) {
|
||||
@@ -60,15 +61,20 @@ declare global {
|
||||
var pendingResolvers: {
|
||||
[key: string]: (value: any) => void;
|
||||
};
|
||||
var statusBar: React.MutableRefObject<{
|
||||
set: React.Dispatch<
|
||||
React.SetStateAction<{
|
||||
date: string;
|
||||
saved: string;
|
||||
var statusBars: Record<
|
||||
number,
|
||||
| React.MutableRefObject<{
|
||||
set: React.Dispatch<
|
||||
React.SetStateAction<{
|
||||
date: string;
|
||||
saved: string;
|
||||
}>
|
||||
>;
|
||||
updateWords: () => void;
|
||||
resetWords: () => void;
|
||||
}>
|
||||
>;
|
||||
updateWords: () => void;
|
||||
}>;
|
||||
| undefined
|
||||
>;
|
||||
var __PLATFORM__: "ios" | "android";
|
||||
var readonly: boolean;
|
||||
var noToolbar: boolean;
|
||||
@@ -78,14 +84,16 @@ declare global {
|
||||
* Id of current session
|
||||
*/
|
||||
var sessionId: string;
|
||||
|
||||
var tabStore: any;
|
||||
/**
|
||||
* Current tiptap instance
|
||||
* Current tiptap editors
|
||||
*/
|
||||
var editor: Editor | null;
|
||||
var editors: Record<number, Editor | null>;
|
||||
/**
|
||||
* Current editor controller
|
||||
* Current editor controllers
|
||||
*/
|
||||
var editorController: ReturnType<typeof useEditorController>;
|
||||
var editorControllers: Record<number, EditorController | undefined>;
|
||||
|
||||
var settingsController: {
|
||||
update: (settings: Settings) => void;
|
||||
@@ -113,17 +121,21 @@ declare global {
|
||||
>;
|
||||
};
|
||||
|
||||
var editorTitle: RefObject<HTMLTextAreaElement>;
|
||||
var editorTitles: Record<number, RefObject<HTMLTextAreaElement> | undefined>;
|
||||
/**
|
||||
* Global ref to manage tags in editor.
|
||||
*/
|
||||
var editorTags: MutableRefObject<{
|
||||
setTags: React.Dispatch<
|
||||
React.SetStateAction<
|
||||
{ title: string; alias: string; id: string; type: "tag" }[]
|
||||
>
|
||||
>;
|
||||
}>;
|
||||
var editorTags: Record<
|
||||
number,
|
||||
| MutableRefObject<{
|
||||
setTags: React.Dispatch<
|
||||
React.SetStateAction<
|
||||
{ title: string; alias: string; id: string; type: "tag" }[]
|
||||
>
|
||||
>;
|
||||
}>
|
||||
| undefined
|
||||
>;
|
||||
|
||||
function logger(type: "info" | "warn" | "error", ...logs: unknown[]): void;
|
||||
/**
|
||||
@@ -134,7 +146,10 @@ declare global {
|
||||
|
||||
function post<T extends keyof typeof EventTypes>(
|
||||
type: (typeof EventTypes)[T],
|
||||
value?: unknown
|
||||
value?: unknown,
|
||||
tabId?: number,
|
||||
noteId?: string,
|
||||
sessionId?: string
|
||||
): void;
|
||||
interface Window {
|
||||
/**
|
||||
@@ -168,7 +183,10 @@ export const EventTypes = {
|
||||
reminders: "editor-event:reminders",
|
||||
previewAttachment: "editor-event:preview-attachment",
|
||||
copyToClipboard: "editor-events:copy-to-clipboard",
|
||||
getAttachmentData: "editor-events:get-attachment-data"
|
||||
getAttachmentData: "editor-events:get-attachment-data",
|
||||
tabsChanged: "editor-events:tabs-changed",
|
||||
showTabs: "editor-events:show-tabs",
|
||||
tabFocused: "editor-events:tab-focused"
|
||||
} as const;
|
||||
|
||||
export function isReactNative(): boolean {
|
||||
@@ -191,6 +209,8 @@ export function logger(
|
||||
export function post<T extends keyof typeof EventTypes>(
|
||||
type: (typeof EventTypes)[T],
|
||||
value?: unknown,
|
||||
tabId?: number,
|
||||
noteId?: string,
|
||||
sessionId?: string
|
||||
): void {
|
||||
if (isReactNative()) {
|
||||
@@ -198,7 +218,9 @@ export function post<T extends keyof typeof EventTypes>(
|
||||
JSON.stringify({
|
||||
type,
|
||||
value: value,
|
||||
sessionId: sessionId || globalThis.sessionId
|
||||
sessionId: sessionId || globalThis.sessionId,
|
||||
tabId,
|
||||
noteId
|
||||
})
|
||||
);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user