Compare commits

..

1 Commits

Author SHA1 Message Date
Ammar Ahmed
d68b7f1963 mobile: close sheet on backup completed 2025-01-29 16:01:08 +05:00
79 changed files with 2081 additions and 3489 deletions

View File

@@ -182,17 +182,18 @@ const Actions = ({
relations
.map((relation) => relation.fromId)
.forEach(async (id) => {
useTabStore.getState().forEachNoteTab(id, async (tab) => {
const isFocused = useTabStore.getState().currentTab === tab.id;
const tab = useTabStore.getState().getTabForNote(id);
if (tab !== undefined) {
const isFocused = useTabStore.getState().currentTab === tab;
if (isFocused) {
eSendEvent(eOnLoadNote, {
item: await db.notes.note(id),
forced: true
});
} else {
editorController.current.commands.setLoading(true, tab.id);
editorController.current.commands.setLoading(true, tab);
}
});
}
});
close?.();
},

View File

@@ -141,7 +141,7 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
close();
eSendEvent(eOnLoadNote, {
item: item,
newTab: true
presistTab: true
});
if (!DDS.isTab) {
tabBarRef.current?.goToPage(1);

View File

@@ -16,8 +16,8 @@ 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 { EVENTS, Note } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { Note } from "@notesnook/core";
import { EVENTS } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect } from "react";
import { View } from "react-native";
@@ -25,18 +25,27 @@ import { FlatList } from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../../common/database";
import { useDBItem } from "../../../hooks/use-db-item";
import {
TabItem,
useTabStore
} from "../../../screens/editor/tiptap/use-tab-store";
import { useTabStore } from "../../../screens/editor/tiptap/use-tab-store";
import { editorController } from "../../../screens/editor/tiptap/utils";
import { eSendEvent, presentSheet } from "../../../services/event-manager";
import { eUnlockNote } from "../../../utils/events";
import { SIZE } from "../../../utils/size";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import { Pressable } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
type TabItem = {
id: number;
noteId?: string;
previewTab?: boolean;
locked?: boolean;
noteLocked?: boolean;
readonly?: boolean;
pinned?: boolean;
};
const TabItemComponent = (props: {
tab: TabItem;
@@ -44,7 +53,7 @@ const TabItemComponent = (props: {
close?: (ctx?: string | undefined) => void;
}) => {
const { colors } = useThemeColors();
const [item, update] = useDBItem(props.tab.session?.noteId, "note");
const [item, update] = useDBItem(props.tab.noteId, "note");
useEffect(() => {
const syncCompletedSubscription = db.eventManager?.subscribe(
@@ -76,11 +85,11 @@ const TabItemComponent = (props: {
onPress={() => {
if (!props.isFocused) {
useTabStore.getState().focusTab(props.tab.id);
if (props.tab.session?.locked) {
if (props.tab.locked) {
eSendEvent(eUnlockNote);
}
if (!props.tab.session?.noteId) {
if (!props.tab.noteId) {
setTimeout(() => {
editorController?.current?.commands?.focus(props.tab.id);
}, 300);
@@ -88,6 +97,11 @@ const TabItemComponent = (props: {
}
props.close?.();
}}
onLongPress={() => {
useTabStore.getState().updateTab(props.tab.id, {
previewTab: false
});
}}
>
<View
style={{
@@ -98,9 +112,9 @@ const TabItemComponent = (props: {
flexShrink: 1
}}
>
{props.tab.session?.noteLocked ? (
{props.tab.noteLocked ? (
<>
{props.tab.session?.locked ? (
{props.tab.locked ? (
<Icon size={SIZE.md} name="lock" />
) : (
<Icon size={SIZE.md} name="lock-open-outline" />
@@ -108,9 +122,7 @@ const TabItemComponent = (props: {
</>
) : null}
{props.tab.session?.readonly ? (
<Icon size={SIZE.md} name="pencil-lock" />
) : null}
{props.tab.readonly ? <Icon size={SIZE.md} name="pencil-lock" /> : null}
<Paragraph
color={
@@ -118,10 +130,15 @@ const TabItemComponent = (props: {
? colors.selected.paragraph
: colors.primary.paragraph
}
style={{
fontFamily: props.tab.previewTab
? "OpenSans-Italic"
: "OpenSans-Regular"
}}
numberOfLines={1}
size={SIZE.md}
>
{props.tab.session?.noteId
{props.tab.noteId
? item?.title || strings.untitledNote()
: strings.newNote()}
</Paragraph>
@@ -140,7 +157,8 @@ const TabItemComponent = (props: {
color={props.tab.pinned ? colors.primary.accent : colors.primary.icon}
onPress={() => {
useTabStore.getState().updateTab(props.tab.id, {
pinned: !props.tab.pinned
pinned: !props.tab.pinned,
previewTab: false
});
}}
top={0}
@@ -179,7 +197,6 @@ export default function EditorTabs({
}: {
close?: (ctx?: string | undefined) => void;
}) {
const { colors } = useThemeColors();
const [tabs, currentTab] = useTabStore((state) => [
state.tabs,
state.currentTab
@@ -216,13 +233,25 @@ export default function EditorTabs({
}}
>
<Heading size={SIZE.lg}>{strings.tabs()}</Heading>
<IconButton
<Button
onPress={() => {
useTabStore.getState().newTab();
setTimeout(() => {
editorController?.current?.commands?.focus(
useTabStore.getState().currentTab
);
}, 500);
close?.();
}}
name="plus"
color={colors.primary.accent}
title={strings.newTab()}
icon="plus"
style={{
flexDirection: "row",
justifyContent: "flex-start",
borderRadius: 100,
height: 35
}}
iconSize={SIZE.lg}
/>
</View>

View File

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

View File

@@ -43,7 +43,7 @@ export const Update = ({ version: appVersion, fwdRef }) => {
let notes = version?.notes
? version.notes.replace("Thank you for using Notesnook!", "").split("- ")
: ["Bug fixes and performance improvements"];
notes = notes?.map((n) => n.replace(/\n|<br>/g, ""));
notes = notes?.map((n) => n.replace(/\n/g, ""));
const isGithubRelease = Config.GITHUB_RELEASE === "true";
const getSupportedAbi = () => {
@@ -178,20 +178,18 @@ export const Update = ({ version: appVersion, fwdRef }) => {
{version.body}
</Paragraph>
) : null}
{notes.map((item) =>
item && item !== "" ? (
<Paragraph
key={item}
color={colors.secondary.paragraph}
style={{
marginBottom: 5
}}
selectable
>
{item}
</Paragraph>
) : null
)}
{notes.map((item) => (
<Paragraph
key={item}
color={colors.secondary.paragraph}
style={{
marginBottom: 5
}}
selectable
>
{item}
</Paragraph>
))}
</ScrollView>
<Seperator />
<Button

View File

@@ -19,14 +19,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
/* eslint-disable no-inner-declarations */
import {
Color,
createInternalLink,
ItemReference,
Note,
Notebook,
Reminder,
Tag,
TrashItem,
VAULT_ERRORS
VAULT_ERRORS,
createInternalLink
} from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { DisplayedNotification } from "@notifee/react-native";
@@ -50,11 +50,11 @@ 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,
eSubscribeEvent,
openVault,
presentSheet,
ToastManager
presentSheet
} from "../services/event-manager";
import Navigation from "../services/navigation";
import Notifications from "../services/notifications";
@@ -544,13 +544,14 @@ export const useActions = ({
const toggleReadyOnlyMode = async () => {
const currentReadOnly = (item as Note).readonly;
await db.notes.readonly(!currentReadOnly, item?.id);
useTabStore.getState().forEachNoteTab(item.id, (tab) => {
useTabStore.getState().updateTab(tab.id, {
session: {
readonly: !currentReadOnly
}
if (useTabStore.getState().hasTabForNote(item.id)) {
const tabId = useTabStore.getState().getTabForNote(item.id);
if (!tabId) return;
useTabStore.getState().updateTab(tabId, {
readonly: !currentReadOnly
});
});
}
Navigation.queueRoutesForUpdate();
close();
};

View File

@@ -51,10 +51,7 @@ import { endProgress, startProgress } from "../components/dialogs/progress";
import Migrate from "../components/sheets/migrate";
import NewFeature from "../components/sheets/new-feature";
import { Walkthrough } from "../components/walkthroughs";
import {
resetTabStore,
useTabStore
} from "../screens/editor/tiptap/use-tab-store";
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
import {
clearAppState,
editorController,
@@ -106,7 +103,6 @@ import { getGithubVersion } from "../utils/github-version";
import { tabBarRef } from "../utils/global-refs";
import { NotesnookModule } from "../utils/notesnook-module";
import { sleep } from "../utils/time";
import ReminderSheet from "../components/sheets/reminder";
const onCheckSyncStatus = async (type: SyncStatusEvent) => {
const { disableSync, disableAutoSync } = SettingsService.get();
@@ -161,8 +157,6 @@ const onUserSessionExpired = async () => {
const onAppOpenedFromURL = async (event: { url: string }) => {
const url = event.url;
console.log("URL", url);
try {
if (url.startsWith("https://app.notesnook.com/account/verified")) {
await onUserEmailVerified();
@@ -172,25 +166,6 @@ const onAppOpenedFromURL = async (event: { url: string }) => {
eSendEvent(eOnLoadNote, { newNote: true });
tabBarRef.current?.goToPage(1, false);
return;
} else if (url.startsWith("https://notesnook.com/open_note")) {
const id = new URL(url).searchParams.get("id");
if (id) {
const note = await db.notes.note(id);
if (note) {
eSendEvent(eOnLoadNote, {
item: note
});
tabBarRef.current?.goToPage(1, false);
}
}
} else if (url.startsWith("https://notesnook.com/open_reminder")) {
const id = new URL(url).searchParams.get("id");
if (id) {
const reminder = await db.reminders.reminder(id);
if (reminder) ReminderSheet.present(reminder);
}
} else if (url.startsWith("https://notesnook.com/new_reminder")) {
ReminderSheet.present();
}
} catch (e) {
console.error(e);
@@ -247,7 +222,6 @@ const onLogout = async (reason: string) => {
await PremiumService.setPremiumStatus();
await BiometricService.resetCredentials();
MMKV.clearStore();
resetTabStore();
clearAllStores();
setImmediate(() => {
refreshAllStores();
@@ -646,17 +620,22 @@ export const useAppEvents = () => {
EV.subscribe(EVENTS.vaultLocked, async () => {
// Lock all notes in all tabs...
for (const tab of useTabStore.getState().tabs) {
const noteId = useTabStore.getState().getTab(tab.id)?.session?.noteId;
const noteId = useTabStore.getState().getTab(tab.id)?.noteId;
if (!noteId) continue;
const note = await db.notes.note(noteId);
const locked = note && (await db.vaults.itemExists(note));
if (locked) {
useTabStore.getState().updateTab(tab.id, {
session: {
locked: true,
noteLocked: true
}
locked: true
});
if (
tab.id === useTabStore.getState().currentTab &&
locked &&
!editorState().movedAway
) {
// Show unlock note screen.
eSendEvent(eUnlockNote);
}
}
}
}),
@@ -716,6 +695,10 @@ export const useAppEvents = () => {
// Reset the editor if the app has been in background for more than 10 minutes.
eSendEvent(eEditorReset);
}
setTimeout(async () => {
IntentService.onAppStateChanged();
}, 100);
} else {
await saveEditorState();
if (

View File

@@ -529,9 +529,7 @@ const onChangeTab = async (event) => {
const locked = note && (await db.vaults.itemExists(note));
if (locked) {
useTabStore.getState().updateTab(tab.id, {
session: {
locked: true
}
locked: true
});
}
}

View File

@@ -39,12 +39,7 @@
"@lingui/core": "5.1.2",
"@lingui/react": "5.1.2",
"react-native-check-version": "^1.3.0",
"react-native-material-menu": "^2.0.0",
"@trpc/client": "^10.45.2",
"@trpc/react-query": "^10.45.2",
"@trpc/server": "^10.45.2",
"@tanstack/react-query": "^4.36.1",
"async-mutex": "0.5.0"
"react-native-material-menu": "^2.0.0"
},
"sideEffects": false
}

View File

@@ -19,8 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
/* eslint-disable @typescript-eslint/no-var-requires */
import { i18n } from "@lingui/core";
import { strings } from "@notesnook/intl";
import React, {
forwardRef,
useCallback,
@@ -48,7 +46,6 @@ import {
eUnlockWithPassword
} from "../../utils/events";
import { openLinkInBrowser } from "../../utils/functions";
import { tabBarRef } from "../../utils/global-refs";
import EditorOverlay from "./loading";
import { EDITOR_URI } from "./source";
import { EditorProps, useEditorType } from "./tiptap/types";
@@ -61,6 +58,9 @@ import {
openInternalLink,
randId
} from "./tiptap/utils";
import { tabBarRef } from "../../utils/global-refs";
import { strings } from "@notesnook/intl";
import { i18n } from "@lingui/core";
const style: ViewStyle = {
height: "100%",
@@ -203,13 +203,11 @@ const useLockedNoteHandler = () => {
useEffect(() => {
for (const tab of useTabStore.getState().tabs) {
const noteId = useTabStore.getState().getTab(tab.id)?.session?.noteId;
const noteId = useTabStore.getState().getTab(tab.id)?.noteId;
if (!noteId) continue;
if (tabRef.current && tabRef.current.session?.noteLocked) {
if (tabRef.current && tabRef.current.noteLocked) {
useTabStore.getState().updateTab(tabRef.current.id, {
session: {
locked: true
}
locked: true
});
}
}
@@ -223,27 +221,23 @@ const useLockedNoteHandler = () => {
biometryAvailable: !!biometry,
biometryEnrolled: !!fingerprint
});
syncTabs("biometry");
syncTabs();
})();
}, [tab?.id]);
useEffect(() => {
const unlockWithBiometrics = async () => {
try {
if (!tabRef.current?.session?.noteLocked || !tabRef.current) return;
console.log("Trying to unlock with biometrics...");
if (!tabRef.current?.noteLocked || !tabRef.current) return;
const credentials = await BiometricService.getCredentials(
"Unlock note",
"Unlock note to open it in editor."
);
if (
credentials &&
credentials?.password &&
tabRef.current.session?.noteId
) {
if (credentials && credentials?.password && tabRef.current.noteId) {
const note = await db.vault.open(
tabRef.current.session?.noteId,
tabRef.current.noteId,
credentials?.password
);
@@ -252,9 +246,7 @@ const useLockedNoteHandler = () => {
});
useTabStore.getState().updateTab(tabRef.current.id, {
session: {
locked: false
}
locked: false
});
}
} catch (e) {
@@ -269,7 +261,7 @@ const useLockedNoteHandler = () => {
password: string;
biometrics?: boolean;
}) => {
if (!tabRef.current?.session?.noteId || !tabRef.current) return;
if (!tabRef.current?.noteId || !tabRef.current) return;
if (!password || password.trim().length === 0) {
ToastManager.show({
heading: strings.passwordNotEntered(),
@@ -279,10 +271,7 @@ const useLockedNoteHandler = () => {
}
try {
const note = await db.vault.open(
tabRef.current?.session?.noteId,
password
);
const note = await db.vault.open(tabRef.current?.noteId, password);
if (enrollBiometrics && note) {
try {
const unlocked = await db.vault.unlock(password);
@@ -313,9 +302,7 @@ const useLockedNoteHandler = () => {
item: note
});
useTabStore.getState().updateTab(tabRef.current.id, {
session: {
locked: false
}
locked: false
});
} catch (e) {
ToastManager.show({
@@ -327,7 +314,7 @@ const useLockedNoteHandler = () => {
const unlock = () => {
if (
(tabRef.current?.session?.locked,
(tabRef.current?.locked,
useTabStore.getState().biometryAvailable &&
useTabStore.getState().biometryEnrolled &&
!editorState().movedAway)
@@ -338,7 +325,7 @@ const useLockedNoteHandler = () => {
} else {
if (!editorState().movedAway) {
setTimeout(() => {
if (tabRef.current && tabRef.current?.session?.locked) {
if (tabRef.current && tabRef.current?.locked) {
editorController.current?.commands.focus(tabRef.current?.id);
}
}, 100);
@@ -353,13 +340,13 @@ const useLockedNoteHandler = () => {
}),
eSubscribeEvent(eUnlockWithPassword, onSubmit)
];
if (tabRef.current?.session?.locked && tabBarRef.current?.page() === 2) {
if (tabRef.current?.locked && tabBarRef.current?.page() === 2) {
unlock();
}
return () => {
subs.map((s) => s?.unsubscribe());
};
}, [tab?.id, tab?.session?.locked]);
}, [tab?.id, tab?.locked]);
return null;
};

View File

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

View File

@@ -18,11 +18,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Note } from "@notesnook/core";
import type {
Attachment,
ImageAttributes,
LinkAttributes
} from "@notesnook/editor";
import type { Attachment } from "@notesnook/editor";
import type { ImageAttributes } from "@notesnook/editor";
import type { LinkAttributes } from "@notesnook/editor";
import { createRef, RefObject } from "react";
import { Platform } from "react-native";
import { EdgeInsets } from "react-native-safe-area-context";
@@ -39,6 +37,9 @@ async function call(webview: RefObject<WebView | undefined>, action?: Action) {
if (!webview.current || !action) return;
setImmediate(() => webview.current?.injectJavaScript(action.job));
const response = await getResponse(action.id);
// if (!response) {
// console.warn("webview job failed", action.id);
// }
return response ? response.value : response;
}
@@ -74,65 +75,110 @@ class Commands {
return call(this.ref, fn(job, name)) as Promise<T>;
}
async sendCommand<T>(command: string, ...args: any[]) {
return this.doAsync(
`response = globalThis.commands.${command}(${args
.map((arg) =>
typeof arg === "string" ? `"${arg}"` : JSON.stringify(arg)
)
.join(",")})`,
command
);
}
focus = async (tabId: string) => {
focus = async (tabId: number) => {
if (!this.ref.current) return;
const locked = useTabStore.getState().getTab(tabId)?.session?.locked;
if (Platform.OS === "android") {
//this.ref.current?.requestFocus();
setTimeout(async () => {
if (!this.ref) return;
textInput.current?.focus();
await this.sendCommand("focus", tabId, locked);
const locked = useTabStore.getState().getTab(tabId)?.locked;
await this.doAsync(
locked
? `editorControllers[${tabId}]?.focusPassInput();`
: `editors[${tabId}]?.commands.focus()`,
"focus"
);
this.ref?.current?.requestFocus();
}, 1);
} else {
await sleep(400);
await this.sendCommand("focus", tabId, locked);
await this.doAsync(`editors[${tabId}]?.commands.focus()`, "focus");
}
};
blur = async (tabId: string) => this.sendCommand("blur", tabId);
blur = async (tabId: number) =>
await this.doAsync(
`
const editor = editors[${tabId}];
const editorTitle = editorTitles[${tabId}];
typeof editor !== "undefined" && editor.commands.blur();
typeof editorTitle !== "undefined" && editorTitle.current && editorTitle.current.blur();
editorControllers[${tabId}]?.blurPassInput();
clearContent = async (tabId: string) => {
`,
"blur"
);
clearContent = async (tabId: number) => {
this.previousSettings = null;
await this.sendCommand("clearContent", tabId);
await this.doAsync(
`
const editor = editors[${tabId}];
const editorController = editorControllers[${tabId}];
const editorTitle = editorTitles[${tabId}];
const statusBar = statusBars[${tabId}];
if (typeof editor !== "undefined") {
editor.commands.blur();
editor.commands.clearContent(false);
}
typeof editorTitle !== "undefined" && editorTitle.current && editorTitle.current?.blur();
if (typeof editorController.content !== undefined) editorController.content.current = '';
editorController.onUpdate();
editorController.setTitle('');
if (typeof statusBar !== "undefined") {
statusBar.current.resetWords();
statusBar.current.set({date:"",saved:""});
}`,
"clearContent"
);
};
setSessionId = async (id: string | null) =>
await this.sendCommand("setSessionId", id);
await this.doAsync(`globalThis.sessionId = "${id}";`);
setStatus = async (
date: string | undefined,
saved: string,
tabId: string
tabId: number
) => {
this.sendCommand("setStatus", date, saved, tabId);
};
setPlaceholder = async (placeholder: string) => {};
setLoading = async (loading?: boolean, tabId?: string) => {
this.sendCommand(
"setLoading",
loading,
tabId === undefined ? useTabStore.getState().currentTab : tabId
await this.doAsync(
`
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}");
// }
// `);
};
setLoading = async (loading?: boolean, tabId?: number) => {
await this.doAsync(`
const editorController = editorControllers[${
tabId || useTabStore.getState().currentTab
}];
editorController.setLoading(${loading})
`);
};
setInsets = async (insets: EdgeInsets) => {
this.sendCommand("setInsets", insets);
await this.doAsync(`
if (typeof safeAreaController !== "undefined") {
safeAreaController.update(${JSON.stringify(insets)})
}
`);
};
updateSettings = async (settings?: Partial<Settings>) => {
@@ -141,7 +187,13 @@ class Commands {
...this.previousSettings,
...settings
};
this.sendCommand("updateSettings", settings);
await this.doAsync(`
if (typeof globalThis.settingsController !== "undefined") {
globalThis.settingsController.update(${JSON.stringify(
this.previousSettings
)})
}
`);
};
setSettings = async (settings?: Partial<Settings>) => {
@@ -154,31 +206,71 @@ class Commands {
return;
}
}
this.sendCommand("setSettings", settings);
await this.doAsync(`
if (typeof globalThis.settingsController !== "undefined") {
globalThis.settingsController.update(${JSON.stringify(settings)})
}
`);
};
setTags = async (note: Note | null | undefined) => {
if (!note) return;
useTabStore.getState().forEachNoteTab(note.id, async (tab) => {
const tabId = tab.id;
const tags = await db.relations.to(note, "tag").resolve();
await this.sendCommand("setTags", tabId, tags);
});
const tabId = useTabStore.getState().getTabForNote(note.id);
const tags = await db.relations.to(note, "tag").resolve();
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,
id: tag.id,
type: tag.type
}))
)});
}
`,
"setTags"
);
};
clearTags = async (tabId: string) => {
await this.sendCommand("clearTags", tabId);
clearTags = async (tabId: number) => {
await this.doAsync(
`
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.sendCommand("insertAttachment", attachment, tabId);
await this.doAsync(
`const editor = editors[${tabId}];
editor && editor.commands.insertAttachment(${JSON.stringify(attachment)})`
);
};
setAttachmentProgress = async (
attachmentProgress: Partial<Attachment>,
tabId: number
) => {
await this.sendCommand("setAttachmentProgress", attachmentProgress, tabId);
await this.doAsync(
`const editor = editors[${tabId}];
editor && editor.commands.updateAttachment(${JSON.stringify(
attachmentProgress
)}, {
preventUpdate: true,
query: (attachment) => {
return attachment.hash === "${attachmentProgress.hash}";
}
})`
);
};
insertImage = async (
@@ -187,30 +279,50 @@ class Commands {
},
tabId: number
) => {
await this.sendCommand("insertImage", image, tabId);
await this.doAsync(
`const editor = editors[${tabId}];
const image = toBlobURL("${image.dataurl}", "${image.hash}");
editor && editor.commands.insertImage({
...${JSON.stringify({
...image,
dataurl: undefined
})},
bloburl: image
})`
);
};
handleBack = async () => {
return this.sendCommand("handleBack");
return this.doAsync<boolean>(
'response = window.dispatchEvent(new Event("handleBackPress",{cancelable:true}));'
);
};
keyboardShown = async (keyboardShown: boolean) => {
return this.sendCommand("keyboardShown", keyboardShown);
return this.doAsync(`globalThis['keyboardShown']=${keyboardShown};`);
};
getTableOfContents = async () => {
const tabId = useTabStore.getState().currentTab;
return this.sendCommand("getTableOfContents", tabId);
return this.doAsync(`
response = editorControllers[${tabId}]?.getTableOfContents() || [];
`);
};
focusPassInput = async () => {
const tabId = useTabStore.getState().currentTab;
return this.sendCommand("focusPassInput", tabId);
return this.doAsync(`
response = editorControllers[${tabId}]?.focusPassInput() || [];
`);
};
blurPassInput = async () => {
const tabId = useTabStore.getState().currentTab;
return this.sendCommand("blurPassInput", tabId);
return this.doAsync(`
response = editorControllers[${tabId}]?.blurPassInput() || [];
`);
};
createInternalLink = async (
@@ -218,18 +330,30 @@ class Commands {
resolverId: string
) => {
if (!resolverId) return;
return this.sendCommand("createInternalLink", attributes, resolverId);
return this.doAsync(`
if (globalThis.pendingResolvers["${resolverId}"]) {
globalThis.pendingResolvers["${resolverId}"](${JSON.stringify(
attributes
)});
}`);
};
dismissCreateInternalLinkRequest = async (resolverId: string) => {
if (!resolverId) return;
return this.sendCommand("dismissCreateInternalLinkRequest", resolverId);
return this.doAsync(`
if (globalThis.pendingResolvers["${resolverId}"]) {
globalThis.pendingResolvers["${resolverId}"](undefined);
}
`);
};
scrollIntoViewById = async (id: string) => {
const tabId = useTabStore.getState().currentTab;
return this.sendCommand("scrollIntoViewById", id, tabId);
return this.doAsync(`
response = editorControllers[${tabId}]?.scrollIntoView("${id}") || [];
`);
};
//todo add replace image function
}
export default Commands;

View File

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

View File

@@ -44,6 +44,7 @@ import { useTabStore } from "./use-tab-store";
import { editorController, editorState } from "./utils";
import { strings } from "@notesnook/intl";
import { useUserStore } from "../../../stores/use-user-store";
import { sleep } from "../../../utils/time";
const showEncryptionSheet = (file: DocumentPickerResponse) => {
presentSheet({
@@ -132,6 +133,7 @@ const file = async (fileOptions: PickerOptions) => {
if (
fileOptions.tabId !== undefined &&
fileOptions.noteId &&
useTabStore.getState().getNoteIdForTab(fileOptions.tabId) ===
fileOptions.noteId
) {

View File

@@ -74,7 +74,7 @@ export type EditorMessage<T> = {
value: T;
type: string;
noteId: string;
tabId: string;
tabId: number;
resolverId?: string;
hasTimeout?: boolean;
};
@@ -86,7 +86,7 @@ export type SavePayload = {
type?: "tiptap";
sessionHistoryId?: number;
ignoreEdit: boolean;
tabId: string;
tabId: number;
pendingChanges?: boolean;
};

View File

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

View File

@@ -32,11 +32,8 @@ import {
isEncryptedContent,
isTrashItem
} from "@notesnook/core";
import { EditorEvents } from "@notesnook/editor-mobile/src/utils/editor-events";
import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events";
import { strings } from "@notesnook/intl";
import { useThemeEngineStore } from "@notesnook/theme";
import { Mutex } from "async-mutex";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import WebView from "react-native-webview";
import { DatabaseLogger, db } from "../../../common/database";
@@ -66,10 +63,12 @@ import { sleep } from "../../../utils/time";
import { unlockVault } from "../../../utils/unlock-vault";
import { onNoteCreated } from "../../notes/common";
import Commands from "./commands";
import { EventTypes } from "./editor-events";
import { SessionHistory } from "./session-history";
import { EditorState, SavePayload } from "./types";
import { TabSessionItem, syncTabs, useTabStore } from "./use-tab-store";
import { syncTabs, useTabStore } from "./use-tab-store";
import {
EditorEvents,
clearAppState,
defaultState,
getAppState,
@@ -78,68 +77,10 @@ import {
post
} from "./utils";
const loadNoteMutex = new Mutex();
type NoteWithContent = Note & {
content?: NoteContent<false>;
};
type LocalTabStateT = {
editedAt: number;
lastFocusedAt: number;
};
class LocalTabState {
state: Record<string, LocalTabStateT> = {};
noteEditedTime: Record<string, number> = {};
setEditTime(noteId: string, time: number) {
this.noteEditedTime[noteId] = time;
}
get(tabId: string) {
return this.state[tabId] || {};
}
set(tabId: string, state: Partial<LocalTabStateT>) {
this.state[tabId] = {
...this.state[tabId],
...state
};
}
clear(tabId: string) {
delete this.state[tabId];
}
reset() {
this.state = {};
}
needsRefresh(tabId: string, locked: boolean, readonly: boolean) {
const state = this.get(tabId);
const tabSession = useTabStore.getState().getTab(tabId)?.session;
const noteId = useTabStore.getState().getNoteIdForTab(tabId);
if (
tabSession?.locked !== locked ||
tabSession?.readonly !== readonly ||
!noteId
) {
console.log("tab is refreshing...");
return true;
}
console.log(
"Tab refreshing...",
state.editedAt < this.noteEditedTime[noteId]
);
console.log(state.editedAt, this.noteEditedTime[noteId]);
return !state.editedAt || state.editedAt < this.noteEditedTime[noteId];
}
}
export const useEditor = (
editorId = "",
readonly?: boolean,
@@ -158,6 +99,7 @@ export const useEditor = (
isPreview?: boolean;
};
})
| null
| undefined
>
>({});
@@ -177,17 +119,15 @@ export const useEditor = (
const lastContentChangeTime = useRef<Record<string, number>>({});
const lock = useRef(false);
const currentLoadingNoteId = useRef<string>();
const lastTabFocused = useRef<string>();
const localTabState = useRef<LocalTabState>(new LocalTabState());
const loadingState = useRef<string>();
const lastTabFocused = useRef(0);
const blockIdRef = useRef<string>();
const postMessage = useCallback(
async <T>(type: string, data: T, tabId?: string, waitFor = 300) =>
async <T>(type: string, data: T, tabId?: number, waitFor = 300) =>
await post(
editorRef,
sessionIdRef.current,
tabId || useTabStore.getState().currentTab!,
typeof tabId !== "number" ? useTabStore.getState().currentTab : tabId,
type,
data,
waitFor
@@ -202,7 +142,7 @@ export const useEditor = (
}, [commands, insets, isDefaultEditor]);
useEffect(() => {
postMessage(NativeEvents.theme, theme);
postMessage(EditorEvents.theme, theme);
}, [theme, postMessage]);
useEffect(() => {
@@ -212,14 +152,14 @@ export const useEditor = (
}, [commands, tags]);
useEffect(() => {
const event = eSubscribeEvent(eEditorTabFocused, (tabId: string) => {
const event = eSubscribeEvent(eEditorTabFocused, (tabId) => {
if (lastTabFocused.current !== tabId) lock.current = false;
lastTabFocused.current = tabId;
lastTabFocused.current = tabId as number;
});
return () => {
event?.unsubscribe();
};
}, []);
});
const overlay = useCallback(
(show: boolean, data = { type: "new" }) => {
@@ -248,11 +188,11 @@ export const useEditor = (
);
const reset = useCallback(
async (tabId: string, resetState = true, resetContent = true) => {
async (tabId: number, resetState = true, resetContent = true) => {
const noteId = useTabStore.getState().getNoteIdForTab(tabId);
if (noteId) {
currentNotes.current?.id && db.fs().cancel(noteId);
currentNotes.current[noteId] = undefined;
currentNotes.current[noteId] = null;
currentContents.current[noteId] = null;
editorSessionHistory.clearSession(noteId);
lastContentChangeTime.current[noteId] = 0;
@@ -260,11 +200,18 @@ export const useEditor = (
}
saveCount.current = 0;
currentLoadingNoteId.current = undefined;
loadingState.current = undefined;
lock.current = false;
resetContent && postMessage(NativeEvents.title, "", tabId);
resetContent && postMessage(EditorEvents.title, "", tabId);
resetContent && (await commands.clearContent(tabId));
resetContent && (await commands.clearTags(tabId));
useTabStore.getState().updateTab(tabId, {
noteId: undefined,
locked: false,
noteLocked: false,
readonly: false
});
},
[commands, editorSessionHistory, postMessage]
);
@@ -284,16 +231,6 @@ export const useEditor = (
try {
if (id && !(await db.notes?.note(id))) {
await reset(tabId);
useTabStore.getState().updateTab(tabId, {
session: {
noteId: undefined,
noteLocked: undefined,
locked: undefined,
readonly: undefined,
scrollTop: undefined,
selection: undefined
}
});
return;
}
let note = id ? await db.notes?.note(id) : undefined;
@@ -332,6 +269,13 @@ export const useEditor = (
};
}
// If note is edited, the tab becomes a persistent tab automatically.
if (useTabStore.getState().getTab(tabId)?.previewTab) {
useTabStore.getState().updateTab(tabId, {
previewTab: false
});
}
let saved = false;
setTimeout(() => {
if (saved) return;
@@ -366,9 +310,7 @@ export const useEditor = (
}
useTabStore.getState().updateTab(tabId, {
session: {
noteId: id
}
noteId: id
});
const defaultNotebook = db.settings.getDefaultNotebook();
@@ -383,7 +325,7 @@ export const useEditor = (
if (!noteData.title) {
postMessage(
NativeEvents.title,
EditorEvents.title,
currentNotes.current[id]?.title,
tabId
);
@@ -443,8 +385,8 @@ export const useEditor = (
id === useTabStore.getState().getCurrentNoteId() &&
pendingChanges
) {
postMessage(NativeEvents.title, title || note?.title, tabId);
postMessage(NativeEvents.html, data, tabId);
postMessage(EditorEvents.title, title || note?.title, tabId);
postMessage(EditorEvents.html, data, tabId);
currentNotes.current[id] = note;
}
@@ -493,211 +435,179 @@ export const useEditor = (
);
const loadNote = useCallback(
(event: {
async (event: {
item?: Note;
forced?: boolean;
newNote?: boolean;
tabId?: string;
tabId?: number;
blockId?: string;
session?: TabSessionItem;
newTab?: boolean;
presistTab?: boolean;
}) => {
loadNoteMutex.runExclusive(async () => {
if (!event) return;
if (event.blockId) {
blockIdRef.current = event.blockId;
}
state.current.currentlyEditing = true;
if (!event) return;
if (
!state.current.ready &&
(await isEditorLoaded(
editorRef,
sessionIdRef.current,
useTabStore.getState().currentTab!
))
) {
state.current.ready = true;
if (event.blockId) {
blockIdRef.current = event.blockId;
}
state.current.currentlyEditing = true;
if (
!state.current.ready &&
(await isEditorLoaded(
editorRef,
sessionIdRef.current,
useTabStore.getState().currentTab
))
) {
state.current.ready = true;
}
if (event.newNote) {
useTabStore.getState().focusEmptyTab();
const tabId = useTabStore.getState().currentTab;
currentNotes.current && (await reset(tabId));
setTimeout(() => {
if (state.current?.ready && !state.current.movedAway)
commands.focus(tabId);
});
} else {
if (!event.item) {
overlay(false);
return;
}
if (event.newNote && !currentLoadingNoteId.current) {
let tabId;
if (useTabStore.getState().tabs.length === 0) {
tabId = useTabStore.getState().newTab();
const item = event.item;
const currentTab = useTabStore
.getState()
.getTab(useTabStore.getState().currentTab);
if (currentTab?.previewTab && item.id !== currentTab.noteId) {
await commands.setLoading(true, useTabStore.getState().currentTab);
}
const isLockedNote = await db.vaults.itemExists(
event.item as ItemReference
);
const tabLocked =
isLockedNote && !(event.item as NoteWithContent).content;
// If note was already opened in a tab, focus that tab.
if (typeof event.tabId !== "number") {
if (useTabStore.getState().hasTabForNote(event.item.id)) {
const tabId = useTabStore.getState().getTabForNote(event.item.id);
if (typeof tabId === "number") {
useTabStore.getState().updateTab(tabId, {
readonly: event.item.readonly || readonly,
locked: tabLocked,
noteLocked: isLockedNote
});
useTabStore.getState().focusTab(tabId);
setTimeout(() => {
if (blockIdRef.current) {
commands.scrollIntoViewById(blockIdRef.current);
blockIdRef.current = undefined;
}
}, 150);
}
} else {
tabId = useTabStore.getState().currentTab;
await reset(tabId!, true, true);
if (
event.session?.noteId ||
useTabStore.getState().getTab(tabId!)?.session?.noteId
) {
useTabStore.getState().newTabSession(tabId!, {});
}
}
setTimeout(() => {
if (state.current?.ready && !state.current.movedAway)
commands.focus(tabId!);
});
} else {
if (!event.item) {
overlay(false);
return;
}
const item = event.item;
// If note is already open in a tab, focus that tab.
if (useTabStore.getState().hasTabForNote(item.id) && !event.newTab) {
const tabId = useTabStore.getState().getTabForNote(item.id);
const currentTab = useTabStore
.getState()
.getTab(useTabStore.getState().currentTab as string);
if (
currentTab?.session?.noteId !== item.id &&
tabId !== useTabStore.getState().currentTab
) {
useTabStore.getState().focusTab(tabId as string);
return;
}
}
const isLockedNote = await db.vaults.itemExists(
event.item as ItemReference
);
const tabLocked =
isLockedNote && !(event.item as NoteWithContent).content;
const tabId = event.tabId
? event.tabId
: useTabStore.getState().currentTab;
console.log(tabId === useTabStore.getState().currentTab);
// Check if tab needs to be refreshed.
if (!event.newTab) {
if (
tabId &&
event.item.id === useTabStore.getState().getNoteIdForTab(tabId) &&
!localTabState.current?.needsRefresh(
tabId,
isLockedNote,
item.readonly
)
) {
commands.setLoading(false, tabId);
return;
if (event.presistTab) {
// Open note in new tab.
useTabStore.getState().newTab({
readonly: event.item.readonly || readonly,
locked: tabLocked,
noteLocked: isLockedNote,
noteId: event.item.id,
previewTab: false
});
} else {
localTabState.current?.setEditTime(
item.id,
localTabState.current?.noteEditedTime[item.id] ||
item.dateEdited
);
localTabState.current?.set(tabId!, {
editedAt:
localTabState.current?.noteEditedTime[item.id] ||
item.dateEdited
// Otherwise we focus the preview tab or create one to open the note in.
useTabStore.getState().focusPreviewTab(event.item.id, {
readonly: event.item.readonly || readonly,
locked: tabLocked,
noteLocked: isLockedNote
});
}
}
} else {
if (lastTabFocused.current !== event.tabId) {
useTabStore.getState().focusTab(event.tabId);
}
}
const tabId = event.tabId || useTabStore.getState().currentTab;
if (lastTabFocused.current !== tabId) {
// if ((await waitForEvent(eEditorTabFocused, 1000)) !== tabId) {
//
// return;
// }
currentLoadingNoteId.current = item.id;
// Show loading overlay if note is not already loaded.
if (
tabId &&
(event.item?.id !== useTabStore.getState().getNoteIdForTab(tabId) ||
!currentContents.current[event.item.id]?.data)
) {
await commands.setLoading(true, tabId);
}
const session: Partial<TabSessionItem> = event.session || {
noteId: event.item.id
};
session.noteLocked = isLockedNote;
session.locked = tabLocked;
session.readonly = item.readonly;
const tab = useTabStore.getState().getTab(tabId!);
if (useTabStore.getState().tabs.length === 0 || event.newTab) {
useTabStore.getState().newTab({
session: session
});
} else {
// A new session is created if the note is changed.
// If the note is already opened, the session is updated.
if (
!tab?.session ||
(event.item.id !== tab?.session?.noteId && tab?.session?.noteId)
) {
useTabStore.getState().newTabSession(tabId!, session);
} else {
useTabStore.getState().updateTab(tabId!, {
session: session
});
}
}
if (lastTabFocused.current !== tabId) return;
if (tabBarRef.current?.page() === 2) {
state.current.movedAway = false;
}
state.current.currentlyEditing = true;
if (!tabLocked) {
await loadContent(item);
} else {
commands.focus(tabId!);
}
lastContentChangeTime.current[item.id] = item.dateEdited;
currentNotes.current[item.id] = item;
if (!currentNotes.current[item.id]) return;
editorSessionHistory.newSession(item.id);
await commands.setStatus(
getFormattedDate(item.dateEdited, "date-time"),
"Saved",
tabId!
);
await postMessage(NativeEvents.title, item.title, tabId);
overlay(false);
await postMessage(
NativeEvents.html,
{
data: currentContents.current[item.id]?.data || "",
scrollTop: tab?.session?.scrollTop,
selection: tab?.session?.selection
},
tabId,
10000
);
setTimeout(() => {
if (blockIdRef.current) {
commands.scrollIntoViewById(blockIdRef.current);
blockIdRef.current = undefined;
}
}, 300);
await commands.setTags(item);
commands.setSettings();
setTimeout(() => {
if (currentLoadingNoteId.current === event.item?.id) {
currentLoadingNoteId.current = undefined;
}
}, 300);
return;
}
postMessage(NativeEvents.theme, theme);
});
state.current.movedAway = false;
state.current.currentlyEditing = true;
if (!tabLocked) {
await loadContent(item);
}
if (
currentNotes.current[item.id] &&
loadingState.current &&
currentContents.current[item.id]?.data &&
loadingState.current === currentContents.current[item.id]?.data
) {
// If note is already loading, return.
return;
}
if (!state.current.ready) {
currentNotes.current[item.id] = item;
return;
}
lastContentChangeTime.current[item.id] = 0;
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"),
strings.saved(),
tabId
);
await postMessage(EditorEvents.title, item.title, tabId);
overlay(false);
loadingState.current = currentContents.current[item.id]?.data;
await postMessage(
EditorEvents.html,
currentContents.current[item.id]?.data || "",
tabId,
10000
);
setTimeout(() => {
if (blockIdRef.current) {
commands.scrollIntoViewById(blockIdRef.current);
blockIdRef.current = undefined;
}
}, 300);
loadingState.current = undefined;
await commands.setTags(item);
commands.setSettings();
setTimeout(() => {
if (currentLoadingNoteId.current === event.item?.id) {
currentLoadingNoteId.current = undefined;
}
}, 300);
}
postMessage(EditorEvents.theme, theme);
},
[
commands,
@@ -705,6 +615,7 @@ export const useEditor = (
loadContent,
overlay,
postMessage,
readonly,
reset,
theme
]
@@ -716,90 +627,83 @@ export const useEditor = (
isLocal?: boolean
) => {
try {
if (SettingsService.get().disableRealtimeSync && !isLocal) return;
if (!data) return;
await (async () => {
if (SettingsService.get().disableRealtimeSync && !isLocal) return;
if (!data) return;
if (isDeleted(data) || isTrashItem(data)) {
const tabId = useTabStore.getState().getTabForNote(data.id);
if (tabId !== undefined) {
await commands.clearContent(tabId);
useTabStore.getState().removeTab(tabId);
if (isDeleted(data) || isTrashItem(data)) {
const tabId = useTabStore.getState().getTabForNote(data.id);
if (tabId !== undefined) {
await commands.clearContent(tabId);
useTabStore.getState().removeTab(tabId);
}
return;
}
return;
}
const noteId =
(data as ContentItem).type === "tiptap"
? (data as ContentItem).noteId
: data.id;
const noteId =
(data as ContentItem).type === "tiptap"
? (data as ContentItem).noteId
: data.id;
const note = data.type === "note" ? data : await db.notes?.note(noteId);
if (!useTabStore.getState().hasTabForNote(noteId)) return;
const tabId = useTabStore.getState().getTabForNote(noteId) as number;
lock.current = true;
const tab = useTabStore.getState().getTab(tabId);
// Handle this case where note was locked on another device and synced.
const locked = note
? await db.vaults.itemExists(note as ItemReference)
: false;
const note =
data.type === "note" ? data : await db.notes?.note(noteId);
useTabStore.getState().forEachNoteTab(noteId, async (tab) => {
const tabId = tab.id;
lock.current = true;
// Handle this case where note was locked on another device and synced.
const locked = note
? await db.vaults.itemExists(note as ItemReference)
: false;
if (note) {
if (!locked && tab?.session?.noteLocked) {
if (!locked && tab?.noteLocked) {
// Note lock removed.
if (tab.session?.locked) {
if (tab.locked) {
if (useTabStore.getState().currentTab === tabId) {
eSendEvent(eOnLoadNote, {
item: note
item: note,
forced: true
});
} else {
useTabStore.getState().updateTab(tabId, {
session: {
locked: false,
noteLocked: false
}
});
localTabState.current?.set(tabId, {
editedAt: 0
locked: false,
noteLocked: false
});
commands.setLoading(true, tabId);
}
}
} else if (!tab?.session?.noteLocked && locked) {
} else if (!tab?.noteLocked && locked) {
// Note lock added.
useTabStore.getState().updateTab(tabId, {
session: {
locked: true,
noteLocked: true
}
locked: true,
noteLocked: true
});
commands.clearContent(tabId);
if (useTabStore.getState().currentTab !== tabId) {
commands.clearContent(tabId);
commands.setLoading(true, tabId);
}
localTabState.current?.set(tabId, {
editedAt: 0
});
}
if (currentNotes.current[noteId]?.title !== note.title) {
postMessage(NativeEvents.title, note.title, tabId);
postMessage(EditorEvents.title, note.title, tabId);
}
commands.setTags(note);
if (currentNotes.current[noteId]?.dateEdited !== note.dateEdited) {
commands.setStatus(
getFormattedDate(note.dateEdited, "date-time"),
strings.saved(),
tabId as string
tabId as number
);
}
if (tab.session?.readonly !== note.readonly) {
useTabStore.getState().updateTab(tabId, {
session: {
readonly: note.readonly
}
});
}
useTabStore.getState().updateTab(tabId, {
readonly: note.readonly
});
}
if (data.type === "tiptap" && note && !isLocal) {
@@ -810,11 +714,9 @@ export const useEditor = (
if (locked && isEncryptedContent(data)) {
const decryptedContent = await db.vault?.decryptContent(data);
if (!decryptedContent) {
useTabStore.getState().updateTab(tab.id, {
session: {
locked: true,
noteLocked: true
}
useTabStore.getState().updateTab(tabId, {
locked: true,
noteLocked: true
});
if (useTabStore.getState().currentTab !== tabId) {
commands.clearContent(tabId);
@@ -822,7 +724,7 @@ export const useEditor = (
}
} else {
await postMessage(
NativeEvents.updatehtml,
EditorEvents.updatehtml,
decryptedContent.data,
tabId
);
@@ -834,14 +736,14 @@ export const useEditor = (
return;
}
lastContentChangeTime.current[note.id] = note.dateEdited;
await postMessage(NativeEvents.updatehtml, _nextContent, tabId);
await postMessage(EditorEvents.updatehtml, _nextContent, tabId);
if (!isEncryptedContent(data)) {
currentContents.current[note.id] =
data as UnencryptedContentItem;
}
}
}
});
})();
} catch (e) {
DatabaseLogger.error(e as Error, "Error when applying sync changes");
} finally {
@@ -877,7 +779,7 @@ export const useEditor = (
content?: string;
type: string;
ignoreEdit: boolean;
tabId: string;
tabId: number;
pendingChanges?: boolean;
}) => {
DatabaseLogger.log(
@@ -906,13 +808,9 @@ export const useEditor = (
if (noteId) {
lastContentChangeTime.current[noteId] = Date.now();
localTabState.current?.setEditTime(noteId, Date.now());
localTabState?.current?.set(tabId, {
editedAt: Date.now()
});
}
if (type === EditorEvents.content && noteId) {
if (type === EventTypes.content && noteId) {
currentContents.current[noteId as string] = {
data: content,
type: "tiptap",
@@ -962,9 +860,7 @@ export const useEditor = (
if (!appState) return;
state.current.isRestoringState = true;
state.current.currentlyEditing = true;
if (tabBarRef.current?.page() === 2) {
state.current.movedAway = false;
}
state.current.movedAway = false;
if (!state.current.editorStateRestored) {
state.current.isRestoringState = true;
@@ -995,12 +891,9 @@ export const useEditor = (
!(await isEditorLoaded(
editorRef,
sessionIdRef.current,
useTabStore.getState().currentTab!
useTabStore.getState().currentTab
))
) {
localTabState.current?.set(useTabStore.getState().currentTab!, {
editedAt: 0
});
eSendEvent(eEditorReset, "onReady");
return false;
} else {
@@ -1012,17 +905,13 @@ export const useEditor = (
const onLoad = useCallback(async () => {
setTimeout(() => {
postMessage(NativeEvents.theme, theme);
postMessage(EditorEvents.theme, theme);
});
commands.setInsets(
isDefaultEditor ? insets : { top: 0, left: 0, right: 0, bottom: 0 }
);
await commands.setSettings();
localTabState.current?.set(useTabStore.getState().currentTab!, {
editedAt: 0
});
if (!state.current.ready) {
state.current.ready = true;
}

View File

@@ -16,27 +16,18 @@ 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 {
TabHistory as TabHistoryType,
TabSessionHistory
} from "@notesnook/common";
import { getId } from "@notesnook/core";
import { MMKVLoader } from "react-native-mmkv-storage";
import create from "zustand";
import { persist, StateStorage } from "zustand/middleware";
import { db } from "../../../common/database";
import { MMKV } from "../../../common/database/mmkv";
import { eSendEvent } from "../../../services/event-manager";
import { eOnLoadNote } from "../../../utils/events";
import { editorController } from "./utils";
class TabHistory {
history: string[];
class History {
history: number[];
constructor() {
this.history = [];
this.history = [0];
}
add(item: string) {
add(item: number) {
const index = this.history.findIndex((id) => item === id);
if (index !== -1) {
// Item already exists, move it to the top
@@ -45,19 +36,19 @@ class TabHistory {
this.history.unshift(item); // Add item to the beginning of the array
useTabStore.setState({
historyNew: this.history.slice()
tabHistory: this.history.slice()
});
return true; // Item added successfully
}
remove(id: string) {
remove(id: number) {
const index = this.history.findIndex((item) => item === id);
if (index >= -1 && index < this.history.length) {
const removedItem = this.history.splice(index, 1)[0];
return removedItem;
}
useTabStore.setState({
historyNew: this.history.slice()
tabHistory: this.history.slice()
});
return null; // Invalid index
}
@@ -68,7 +59,7 @@ class TabHistory {
return restoredItem;
}
useTabStore.setState({
historyNew: this.history.slice()
tabHistory: this.history.slice()
});
return null; // History is empty
}
@@ -78,354 +69,147 @@ class TabHistory {
}
}
export type TabSessionItem = {
id: string;
noteId?: string;
scrollTop?: number;
selection?: { to: number; from: number };
noteLocked?: boolean;
locked?: boolean;
readonly?: boolean;
};
const TabSessionStorageKV = new MMKVLoader()
.withInstanceID("tab-session-storage")
.disableIndexing()
.initialize();
class TabSessionStorage {
static storage: typeof TabSessionStorageKV = TabSessionStorageKV;
static get(id: string): TabSessionItem | null {
return TabSessionStorage.storage.getMap(id);
}
static set(id: string, session: TabSessionItem): void {
TabSessionStorage.storage.setMap(id, session);
}
static update(id: string, session: Partial<TabSessionItem>) {
const currentSession = TabSessionStorage.get(id);
const newSession = {
...currentSession,
...session
};
TabSessionStorage.set(id, newSession as TabSessionItem);
return newSession;
}
static remove(id: string) {
TabSessionStorageKV.removeItem(id);
}
}
export function syncTabs(
type: "tabs" | "history" | "biometry" | "all" = "all"
) {
const data: Partial<TabStore> = {};
if (type === "tabs" || type === "all") {
data.tabs = useTabStore.getState().tabs;
data.currentTab = useTabStore.getState().currentTab;
}
if (type === "history" || type === "all") {
data.canGoBack = useTabStore.getState().canGoBack;
data.canGoForward = useTabStore.getState().canGoForward;
data.sessionId = useTabStore.getState().sessionId;
}
if (type === "biometry" || type === "all") {
data.biometryAvailable = useTabStore.getState().biometryAvailable;
data.biometryEnrolled = useTabStore.getState().biometryEnrolled;
}
editorController.current?.commands.doAsync(`
globalThis.tabStore?.setState(${JSON.stringify(data)});
`);
}
export const tabSessionHistory = new TabSessionHistory({
get() {
return useTabStore.getState();
},
set(state) {
useTabStore.setState({
...state
});
}
});
export type TabItem = {
id: string;
id: number;
noteId?: string;
previewTab?: boolean;
readonly?: boolean;
locked?: boolean;
noteLocked?: boolean;
pinned?: boolean;
needsRefresh?: boolean;
session?: Partial<TabSessionItem>;
};
const history = new TabHistory();
const history = new History();
export type TabStore = {
tabs: TabItem[];
currentTab?: string;
updateTab: (id: string, options: Omit<Partial<TabItem>, "id">) => void;
currentTab: number;
updateTab: (id: number, options: Omit<Partial<TabItem>, "id">) => void;
focusPreviewTab: (
noteId: string,
options: Omit<Partial<TabItem>, "id">
) => void;
removeTab: (index: string) => void;
removeTab: (index: number) => void;
moveTab: (index: number, toIndex: number) => void;
newTab: (options?: Omit<Partial<TabItem>, "id">) => string;
focusTab: (id: string) => void;
getNoteIdForTab: (id: string) => string | undefined;
getTabForNote: (noteId: string) => string | undefined;
getTabsForNote: (noteId: string) => TabItem[];
forEachNoteTab: (noteId: string, cb: (tab: TabItem) => void) => void;
newTab: (options?: Omit<Partial<TabItem>, "id">) => void;
focusTab: (id: number) => void;
getNoteIdForTab: (id: number) => string | undefined;
getTabForNote: (noteId: string) => number | undefined;
hasTabForNote: (noteId: string) => boolean;
focusEmptyTab: () => void;
getCurrentNoteId: () => string | undefined;
getTab: (tabId: string) => TabItem | undefined;
newTabSession: (
id: string,
options?: Omit<Partial<TabSessionItem>, "id">
) => void;
historyNew: string[];
getTab: (tabId: number) => TabItem | undefined;
tabHistory: number[];
biometryAvailable?: boolean;
biometryEnrolled?: boolean;
tabSessionHistory: TabHistoryType;
goBack(): void;
goForward(): void;
loadSession: (id: string) => Promise<boolean>;
canGoBack?: boolean;
canGoForward?: boolean;
sessionId?: string;
};
const DEFAULT_TABS = {
tabs: [
{
id: "679da59a3924d4bd56d16d3f",
session: {
id: "679da5a5667a16db2353a062"
}
}
],
tabSessionHistory: {
"679da59a3924d4bd56d16d3f": {
backStack: ["679da5a5667a16db2353a062"],
forwardStack: [] as string[]
}
} as TabHistoryType,
historyNew: ["679da59a3924d4bd56d16d3f"],
currentTab: "679da59a3924d4bd56d16d3f"
} as TabStore;
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 function resetTabStore() {
useTabStore.setState({
...DEFAULT_TABS
});
TabSessionStorage.storage.clearStore();
export function syncTabs() {
editorController.current?.commands.doAsync(`
globalThis.tabStore?.setState({
tabs: ${JSON.stringify(useTabStore.getState().tabs)},
currentTab: ${useTabStore.getState().currentTab},
biometryAvailable: ${useTabStore.getState().biometryAvailable},
biometryEnrolled: ${useTabStore.getState().biometryEnrolled}
});
`);
}
export const useTabStore = create<TabStore>(
persist(
(set, get) => ({
...DEFAULT_TABS,
newTabSession: (
_id?: string,
options?: Omit<Partial<TabSessionItem>, "id">
) => {
const tabId = _id || (get().currentTab as string);
const sessionHistory = get().tabSessionHistory[tabId];
let oldSessionId: string | undefined = undefined;
if (sessionHistory) {
const allSessions = sessionHistory.backStack.concat(
sessionHistory.forwardStack
);
allSessions.forEach((id) => {
if (TabSessionStorage.get(id)?.noteId === options?.noteId) {
oldSessionId = id;
}
});
tabs: [
{
id: 0
}
const sessionId =
oldSessionId &&
tabSessionHistory.currentSessionId(tabId) === oldSessionId
? oldSessionId
: tabSessionHistory.add(tabId, oldSessionId);
let session: Partial<TabSessionItem>;
if (!oldSessionId) {
session = {
id: sessionId,
...options
};
TabSessionStorage.set(sessionId, session as TabSessionItem);
} else {
session = {
...TabSessionStorage.get(oldSessionId),
...options
};
}
const index = get().tabs.findIndex((t) => t.id === tabId);
if (index == -1) return;
const tabs = [...get().tabs];
tabs[index] = {
...tabs[index],
session: session
} as TabItem;
set({
tabs: tabs
});
syncTabs();
},
updateTab: (id: string, options: Omit<Partial<TabItem>, "id">) => {
],
tabHistory: [0],
history: new History(),
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];
const sessionId =
options.session?.id || (tabs[index].session?.id as string);
const updatedSession = !options.session
? tabs[index].session
: TabSessionStorage.update(sessionId, options.session);
tabs[index] = {
...tabs[index],
...options,
session: updatedSession
} as TabItem;
...options
};
set({
tabs: tabs
});
syncTabs();
},
goBack: async () => {
const currentTab = get().currentTab;
if (!currentTab) return;
if (!tabSessionHistory.canGoBack(currentTab)) return;
const id = tabSessionHistory.back(currentTab) as string;
const sessionLoaded = await get().loadSession(id);
if (!sessionLoaded) {
tabSessionHistory.remove(currentTab, id);
TabSessionStorage.remove(id);
if (!tabSessionHistory.canGoBack(currentTab)) {
tabSessionHistory.forward(currentTab);
syncTabs();
} else {
return get().goBack();
}
} else {
syncTabs();
}
},
goForward: async () => {
const currentTab = get().currentTab;
if (!currentTab) return;
if (!tabSessionHistory.canGoForward(currentTab)) return;
const id = tabSessionHistory.forward(currentTab) as string;
if (!(await get().loadSession(id))) {
tabSessionHistory.remove(currentTab, id);
TabSessionStorage.remove(id);
if (!tabSessionHistory.canGoForward(currentTab)) {
tabSessionHistory.back(currentTab);
syncTabs();
} else {
return get().goForward();
}
} else {
syncTabs();
}
},
loadSession: async (id: string) => {
const session = TabSessionStorage.get(id);
if (!session) return false;
const note = session?.noteId
? await db.notes.note(session?.noteId)
: undefined;
if (note) {
const isLocked = await db.vaults.itemExists(note);
session.locked = isLocked;
session.noteLocked = isLocked && !session?.noteLocked;
session.readonly = note.readonly;
} else if (session.noteId) {
console.log("Failed to load session...");
return false;
}
eSendEvent(eOnLoadNote, {
item: note,
newNote: !note,
tabId: get().currentTab,
session: session
});
return true;
},
focusPreviewTab: (
noteId: string,
options: Omit<Partial<TabItem>, "id" | "noteId">
) => {},
) => {
const index = get().tabs.findIndex((t) => t.previewTab);
if (index === -1)
return get().newTab({
noteId,
previewTab: true,
...options
});
const tabs = [...get().tabs];
tabs[index] = {
...tabs[index],
...options,
previewTab: true,
noteId: noteId
};
removeTab: (id: string) => {
set({
tabs: tabs
});
get().focusTab(tabs[index].id);
},
removeTab: (id: number) => {
const index = get().tabs.findIndex((t) => t.id === id);
if (index > -1) {
const isFocused = id === get().currentTab;
const nextTabs = get().tabs.slice();
nextTabs.splice(index, 1);
history.remove(id);
const tabSessions = tabSessionHistory.getTabHistory(id);
tabSessions.back.forEach((id) => TabSessionStorage.remove(id));
tabSessions.forward.forEach((id) => TabSessionStorage.remove(id));
tabSessionHistory.clearStackForTab(id);
if (nextTabs.length === 0) {
const id = getId();
set({
tabs: [{ id: id }]
nextTabs.push({
id: 0
});
get().newTabSession(id, {});
get().focusTab(id);
} else {
set({
tabs: nextTabs
});
if (isFocused) {
const lastTab = history.restoreLast();
if (lastTab) get().focusTab(lastTab);
}
}
syncTabs();
set({
tabs: nextTabs
});
get().focusTab(
isFocused ? history.restoreLast() || 0 : get().currentTab
);
}
},
newTab: (options) => {
const id = getId();
const id = getId(get().tabs.length, get().tabs);
const nextTabs = [
...get().tabs,
{
id: id,
...options
}
];
set({
tabs: [
...get().tabs,
{
id: id,
...options
}
]
tabs: nextTabs
});
get().newTabSession(id, options?.session || {});
get().focusTab(id);
return id;
},
focusEmptyTab: () => {
const index = get().tabs.findIndex((t) => !t.session?.noteId);
const index = get().tabs.findIndex((t) => !t.noteId);
if (index === -1) return get().newTab();
get().focusTab(get().tabs[index].id);
@@ -439,46 +223,37 @@ export const useTabStore = create<TabStore>(
syncTabs();
},
focusTab: (id: string) => {
focusTab: (id: number) => {
history.add(id);
set({
currentTab: id,
canGoBack: tabSessionHistory.canGoBack(id),
canGoForward: tabSessionHistory.canGoForward(id),
sessionId: tabSessionHistory.currentSessionId(id)
currentTab: id
});
syncTabs();
},
getNoteIdForTab: (id: string) => {
return get().tabs.find((t) => t.id === id)?.session?.noteId;
getNoteIdForTab: (id: number) => {
return get().tabs.find((t) => t.id === id)?.noteId;
},
hasTabForNote: (noteId: string) => {
return !!get().tabs.find((t) => t.session?.noteId === noteId);
return (
typeof get().tabs.find((t) => t.noteId === noteId)?.id === "number"
);
},
getTabForNote: (noteId: string) => {
return get().tabs.find((t) => t.session?.noteId === noteId)?.id;
},
getTabsForNote(noteId: string) {
return get().tabs.filter((t) => t.session?.noteId === noteId);
},
forEachNoteTab: (noteId: string, cb: (tab: TabItem) => void) => {
const tabs = get().tabs.filter((t) => t.session?.noteId === noteId);
tabs.forEach(cb);
return get().tabs.find((t) => t.noteId === noteId)?.id;
},
getCurrentNoteId: () => {
return get().tabs.find((t) => t.id === get().currentTab)?.session
?.noteId;
return get().tabs.find((t) => t.id === get().currentTab)?.noteId;
},
getTab: (tabId) => {
return get().tabs.find((t) => t.id === tabId);
}
}),
{
name: "tabs-storage-v3",
name: "tabs-storage",
getStorage: () => MMKV as unknown as StateStorage,
onRehydrateStorage: () => {
return (state) => {
history.history = state?.historyNew || [];
history.history = state?.tabHistory.slice() || [];
};
}
}

View File

@@ -31,8 +31,6 @@ import { eOnLoadNote } from "../../../utils/events";
import { NotesnookModule } from "../../../utils/notesnook-module";
import { AppState, EditorState, useEditorType } from "./types";
import { useTabStore } from "./use-tab-store";
import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events";
export const textInput = createRef<TextInput>();
export const editorController =
createRef<useEditorType>() as MutableRefObject<useEditorType>;
@@ -48,6 +46,19 @@ export function editorState() {
return editorController.current?.state.current || defaultState;
}
export const EditorEvents = {
html: "native:html",
updatehtml: "native:updatehtml",
title: "native:title",
theme: "native:theme",
titleplaceholder: "native:titleplaceholder",
logger: "native:logger",
status: "native:status",
keyboardShown: "native:keyboardShown",
attachmentData: "native:attachment-data",
resolve: "native:resolve"
};
export function randId(prefix: string) {
return Math.random()
.toString(36)
@@ -61,15 +72,15 @@ export function makeSessionId(id?: string) {
export async function isEditorLoaded(
ref: RefObject<WebView>,
sessionId: string,
tabId: string
tabId: number
) {
return await post(ref, sessionId, tabId, NativeEvents.status);
return await post(ref, sessionId, tabId, EditorEvents.status);
}
export async function post<T>(
ref: RefObject<WebView>,
sessionId: string,
tabId: string,
tabId: number,
type: string,
value: T | null = null,
waitFor = 300
@@ -174,7 +185,7 @@ export async function openInternalLink(url: string) {
if (!data?.id) return false;
if (
data.id ===
useTabStore.getState().getNoteIdForTab(useTabStore.getState().currentTab!)
useTabStore.getState().getNoteIdForTab(useTabStore.getState().currentTab)
) {
if (data.params?.blockId) {
setTimeout(() => {

View File

@@ -43,7 +43,6 @@ import { useRelationStore } from "../stores/use-relation-store";
import { useReminderStore } from "../stores/use-reminder-store";
import { useSettingStore } from "../stores/use-setting-store";
import { useUserStore } from "../stores/use-user-store";
import { eOnLoadNote } from "../utils/events";
import { tabBarRef } from "../utils/global-refs";
import { convertNoteToText } from "../utils/note-to-text";
import { NotesnookModule } from "../utils/notesnook-module";
@@ -450,10 +449,19 @@ async function loadNote(id: string, jump: boolean) {
})
);
const isLocked = await db.vaults.itemExists({
type: "note",
id: id
});
const tab = useTabStore.getState().getTabForNote(id);
if (useTabStore.getState().currentTab !== tab) {
eSendEvent(eOnLoadNote, {
note: note
if (tab !== undefined) {
useTabStore.getState().focusTab(tab);
} else {
useTabStore.getState().focusPreviewTab(id, {
noteId: id,
readonly: note.readonly,
noteLocked: isLocked
});
}
}

View File

@@ -62,5 +62,4 @@
#Gson
-keepattributes Signature
-keep class com.google.gson.reflect.TypeToken { *; }
-keep class * extends com.google.gson.reflect.TypeToken
-keep class com.streetwriters.notesnook.datatypes.* { *; }
-keep class * extends com.google.gson.reflect.TypeToken

View File

@@ -31,20 +31,14 @@ public class NotePreviewWidget extends AppWidgetProvider {
Intent intent = new Intent(context, MainActivity.class);
intent.putExtra(OpenNoteId, note.getId());
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra(RCTNNativeModule.IntentType, "OpenNote");
intent.setData(Uri.parse("https://notesnook.com/open_note?id=" + note.getId()));
intent.setData(Uri.parse("https://notesnook.com/open_note"));
PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE, getActivityOptionsBundle());
views.setOnClickPendingIntent(R.id.open_note, pendingIntent);
appWidgetManager.updateAppWidget(appWidgetId, views);
}
@Override
public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions);
}
private static Bundle getActivityOptionsBundle() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
ActivityOptions activityOptions = ActivityOptions.makeBasic();

View File

@@ -4,7 +4,6 @@ import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
@@ -46,7 +45,6 @@ class ReminderRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactor
SharedPreferences preferences = context.getSharedPreferences("appPreview", Context.MODE_PRIVATE);
Gson gson = new Gson();
reminders = gson.fromJson(preferences.getString("remindersList","[]"), new TypeToken<List<Reminder>>(){}.getType());
}
@Override
@@ -66,16 +64,14 @@ class ReminderRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactor
boolean useMiniLayout = reminder.getDescription() == null || reminder.getDescription().isEmpty();
RemoteViews views = new RemoteViews(context.getPackageName(), useMiniLayout ? R.layout.widget_reminder_layout_small : R.layout.widget_reminder_layout);
views.setTextViewText(R.id.reminder_title, reminder.getTitle());
if (!useMiniLayout) {
if (!useMiniLayout) {
views.setTextViewText(R.id.reminder_description, reminder.getDescription());
}
}
views.setTextViewText(R.id.reminder_time, reminder.getFormattedTime());
final Intent fillInIntent = new Intent();
final Bundle extras = new Bundle();
extras.putString(ReminderViewsService.OpenReminderId, reminder.getId());
fillInIntent.setData(Uri.parse("https://notesnook.com/open_reminder?id=" + reminder.getId()));
fillInIntent.putExtra(RCTNNativeModule.IntentType, "OpenReminder");
fillInIntent.putExtras(extras);
views.setOnClickFillInIntent(R.id.reminder_item_btn, fillInIntent);
@@ -90,12 +86,11 @@ class ReminderRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactor
@Override
public int getViewTypeCount() {
return 2;
return 1;
}
@Override
public long getItemId(int position) {
return position;
}

View File

@@ -34,13 +34,12 @@ public class ReminderWidgetProvider extends AppWidgetProvider {
static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId, RemoteViews views) {
Intent listview_intent_template = new Intent(context, MainActivity.class);
listview_intent_template.setAction(Intent.ACTION_VIEW);
listview_intent_template.setData(Uri.parse("https://notesnook.com/open_reminder"));
PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, listview_intent_template, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE, getActivityOptionsBundle());
views.setPendingIntentTemplate(R.id.widget_list_view, pendingIntent);
Intent new_reminder_intent = new Intent(context, MainActivity.class);
new_reminder_intent.putExtra(NewReminder, NewReminder);
new_reminder_intent.setAction(Intent.ACTION_VIEW);
new_reminder_intent.putExtra(RCTNNativeModule.IntentType, "NewReminder");
new_reminder_intent.setData(Uri.parse("https://notesnook.com/new_reminder"));
PendingIntent pendingIntent2 = PendingIntent.getActivity(context, appWidgetId, new_reminder_intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, getActivityOptionsBundle());

View File

@@ -5,7 +5,7 @@
<color name="light_blue_600">#FF039BE5</color>
<color name="light_blue_900">#FF01579B</color>
<color name="bootsplash_background">#FFFFFF</color>
<color name="background">#DCEDEDED</color>
<color name="border">#BFBFBF</color>
<color name="background">#D8000000</color>
<color name="border">#CCCCCC</color>
<color name="text">#1D1D1D</color>
</resources>

View File

@@ -3821,7 +3821,6 @@
"@lingui/react": "5.1.2",
"@mdi/js": "^7.2.96",
"@mdi/react": "^1.6.0",
"@notesnook/common": "file:../common",
"@notesnook/editor": "file:../editor",
"@notesnook/intl": "file:../intl",
"@notesnook/theme": "file:../theme",
@@ -3832,7 +3831,6 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-freeze": "^1.0.3",
"tinycolor2": "1.6.0",
"zustand": "^4.4.7"
},
"devDependencies": {
@@ -7742,7 +7740,7 @@
},
"../../packages/editor-mobile/node_modules/@types/prop-types": {
"version": "15.7.11",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"../../packages/editor-mobile/node_modules/@types/q": {
@@ -7762,7 +7760,7 @@
},
"../../packages/editor-mobile/node_modules/@types/react": {
"version": "18.2.39",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@@ -7793,7 +7791,7 @@
},
"../../packages/editor-mobile/node_modules/@types/scheduler": {
"version": "0.16.8",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"../../packages/editor-mobile/node_modules/@types/semver": {
@@ -12618,7 +12616,7 @@
},
"../../packages/editor-mobile/node_modules/immer": {
"version": "9.0.21",
"dev": true,
"devOptional": true,
"license": "MIT",
"funding": {
"type": "opencollective",
@@ -23090,7 +23088,6 @@
},
"../../packages/editor/node_modules/js-tokens": {
"version": "4.0.0",
"dev": true,
"license": "MIT"
},
"../../packages/editor/node_modules/jsesc": {
@@ -23141,7 +23138,6 @@
},
"../../packages/editor/node_modules/loose-envify": {
"version": "1.4.0",
"dev": true,
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
@@ -23653,7 +23649,6 @@
},
"../../packages/editor/node_modules/react": {
"version": "18.3.1",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
@@ -23672,7 +23667,6 @@
},
"../../packages/editor/node_modules/react-dom": {
"version": "18.3.1",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0",
@@ -23811,7 +23805,6 @@
},
"../../packages/editor/node_modules/scheduler": {
"version": "0.23.2",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
@@ -28275,12 +28268,7 @@
"@readme/data-urls": "3.0.0",
"@streetwriters/kysely": "^0.27.4",
"@streetwriters/showdown": "^3.0.1-alpha.2",
"@tanstack/react-query": "^4.36.1",
"@trpc/client": "^10.45.2",
"@trpc/react-query": "^10.45.2",
"@trpc/server": "^10.45.2",
"absolutify": "^0.1.0",
"async-mutex": "0.5.0",
"buffer": "^6.0.3",
"dayjs": "^1.11.13",
"deprecated-react-native-prop-types": "^4.1.0",
@@ -28594,7 +28582,6 @@
},
"node_modules/@babel/helper-builder-binary-assignment-operator-visitor": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.22.5"
@@ -28714,7 +28701,6 @@
},
"node_modules/@babel/helper-hoist-variables": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.22.5"
@@ -28974,7 +28960,6 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz",
"integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
},
@@ -28989,7 +28974,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz",
"integrity": "sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/helper-skip-transparent-expression-wrappers": "^7.22.5",
@@ -29138,7 +29122,6 @@
"version": "7.21.0-placeholder-for-preset-env.2",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
"integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
"dev": true,
"engines": {
"node": ">=6.9.0"
},
@@ -29151,7 +29134,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz",
"integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==",
"deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-unicode-property-regex instead.",
"dev": true,
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.18.6",
"@babel/helper-plugin-utils": "^7.18.6"
@@ -29186,7 +29168,6 @@
},
"node_modules/@babel/plugin-syntax-class-properties": {
"version": "7.12.13",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.12.13"
@@ -29199,7 +29180,6 @@
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz",
"integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.14.5"
},
@@ -29237,7 +29217,6 @@
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz",
"integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.8.3"
},
@@ -29262,7 +29241,6 @@
"version": "7.25.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.25.6.tgz",
"integrity": "sha512-aABl0jHw9bZ2karQ/uUD6XP4u0SG22SJrOHFoL6XB1R7dTovOP4TzTlsxOYC5yQ1pdscVK2JTUnF6QL3ARoAiQ==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
},
@@ -29277,7 +29255,6 @@
"version": "7.25.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.6.tgz",
"integrity": "sha512-sXaDXaJN9SNLymBdlWFA+bjzBhFD617ZaFiY13dGt7TVslVvVgA6fkZOP7Ki3IGElC45lwHdOTrCtKZGVAWeLQ==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
},
@@ -29292,7 +29269,6 @@
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz",
"integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.10.4"
},
@@ -29304,7 +29280,6 @@
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz",
"integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.8.0"
},
@@ -29404,7 +29379,6 @@
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz",
"integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.14.5"
},
@@ -29432,7 +29406,6 @@
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz",
"integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==",
"dev": true,
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.18.6",
"@babel/helper-plugin-utils": "^7.18.6"
@@ -29461,7 +29434,6 @@
"version": "7.25.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.4.tgz",
"integrity": "sha512-jz8cV2XDDTqjKPwVPJBIjORVEmSGYhdRa8e5k5+vN+uwcjSrSxUaebBRa4ko1jqNF2uxyg8G6XYk30Jv285xzg==",
"dev": true,
"dependencies": {
"@babel/helper-environment-visitor": "^7.22.5",
"@babel/helper-plugin-utils": "^7.22.5",
@@ -29492,7 +29464,6 @@
},
"node_modules/@babel/plugin-transform-block-scoped-functions": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -29521,7 +29492,6 @@
"version": "7.25.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.4.tgz",
"integrity": "sha512-nZeZHyCWPfjkdU5pA/uHiTaDAFUEqkpzf1YoQT2NeSynCGYq9rxfyI3XpQbfx/a0hSnFH6TGlEXvae5Vi7GD8g==",
"dev": true,
"dependencies": {
"@babel/helper-create-class-features-plugin": "^7.22.5",
"@babel/helper-plugin-utils": "^7.22.5"
@@ -29537,7 +29507,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz",
"integrity": "sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==",
"dev": true,
"dependencies": {
"@babel/helper-create-class-features-plugin": "^7.22.5",
"@babel/helper-plugin-utils": "^7.22.5",
@@ -29602,7 +29571,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz",
"integrity": "sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==",
"dev": true,
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.22.5",
"@babel/helper-plugin-utils": "^7.22.5"
@@ -29618,7 +29586,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz",
"integrity": "sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
},
@@ -29633,7 +29600,6 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz",
"integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
},
@@ -29646,7 +29612,6 @@
},
"node_modules/@babel/plugin-transform-exponentiation-operator": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5",
@@ -29663,7 +29628,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz",
"integrity": "sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/plugin-syntax-export-namespace-from": "^7.8.3"
@@ -29691,7 +29655,6 @@
},
"node_modules/@babel/plugin-transform-for-of": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -29722,7 +29685,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz",
"integrity": "sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/plugin-syntax-json-strings": "^7.8.3"
@@ -29751,7 +29713,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz",
"integrity": "sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/plugin-syntax-logical-assignment-operators": "^7.10.4"
@@ -29765,7 +29726,6 @@
},
"node_modules/@babel/plugin-transform-member-expression-literals": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -29781,7 +29741,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz",
"integrity": "sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==",
"dev": true,
"dependencies": {
"@babel/helper-module-transforms": "^7.22.5",
"@babel/helper-plugin-utils": "^7.22.5"
@@ -29812,7 +29771,6 @@
"version": "7.25.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.0.tgz",
"integrity": "sha512-YPJfjQPDXxyQWg/0+jHKj1llnY5f/R6a0p/vP4lPymxLu7Lvl4k2WMitqi08yxwQcCVUUdG9LCUj4TNEgAp3Jw==",
"dev": true,
"dependencies": {
"@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-module-transforms": "^7.22.5",
@@ -29830,7 +29788,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz",
"integrity": "sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==",
"dev": true,
"dependencies": {
"@babel/helper-module-transforms": "^7.22.5",
"@babel/helper-plugin-utils": "^7.22.5"
@@ -29860,7 +29817,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz",
"integrity": "sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
},
@@ -29890,7 +29846,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz",
"integrity": "sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/plugin-syntax-numeric-separator": "^7.10.4"
@@ -29906,7 +29861,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz",
"integrity": "sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==",
"dev": true,
"dependencies": {
"@babel/compat-data": "^7.22.5",
"@babel/helper-compilation-targets": "^7.22.5",
@@ -29923,7 +29877,6 @@
},
"node_modules/@babel/plugin-transform-object-super": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -29940,7 +29893,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz",
"integrity": "sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/plugin-syntax-optional-catch-binding": "^7.8.3"
@@ -30014,7 +29966,6 @@
},
"node_modules/@babel/plugin-transform-property-literals": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -30084,7 +30035,6 @@
},
"node_modules/@babel/plugin-transform-regenerator": {
"version": "7.22.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -30101,7 +30051,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz",
"integrity": "sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
},
@@ -30194,7 +30143,6 @@
"version": "7.24.8",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz",
"integrity": "sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
},
@@ -30225,7 +30173,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz",
"integrity": "sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
},
@@ -30240,7 +30187,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz",
"integrity": "sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==",
"dev": true,
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.22.5",
"@babel/helper-plugin-utils": "^7.22.5"
@@ -30270,7 +30216,6 @@
"version": "7.25.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.4.tgz",
"integrity": "sha512-qesBxiWkgN1Q+31xUE9RcMk79eOXXDCv6tfyGMRSs4RGlioSg2WVyQAm07k726cSE56pa+Kb0y9epX2qaXzTvA==",
"dev": true,
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.22.5",
"@babel/helper-plugin-utils": "^7.22.5"
@@ -30286,7 +30231,6 @@
"version": "7.25.4",
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.4.tgz",
"integrity": "sha512-W9Gyo+KmcxjGahtt3t9fb14vFRWvPpu5pT6GBlovAK6BTBcxgjfVMSQCfJl4oi35ODrxP6xx2Wr8LNST57Mraw==",
"dev": true,
"dependencies": {
"@babel/compat-data": "^7.22.5",
"@babel/helper-compilation-targets": "^7.22.5",
@@ -30380,7 +30324,6 @@
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6.tgz",
"integrity": "sha512-ID2yj6K/4lKfhuU3+EX4UvNbIt7eACFbHmNUjzA+ep+B5971CknnA/9DEWKbRokfbbtblxxxXFJJrH47UEAMVg==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.0.0",
"@babel/plugin-proposal-unicode-property-regex": "^7.4.4",
@@ -30396,7 +30339,6 @@
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
}
@@ -33700,8 +33642,7 @@
"node_modules/@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
},
"node_modules/@types/graceful-fs": {
"version": "4.1.6",
@@ -33807,14 +33748,14 @@
},
"node_modules/@types/prop-types": {
"version": "15.7.5",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.18",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
"integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@@ -34170,7 +34111,6 @@
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz",
"integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==",
"dev": true,
"dependencies": {
"@webassemblyjs/helper-numbers": "1.11.6",
"@webassemblyjs/helper-wasm-bytecode": "1.11.6"
@@ -34179,26 +34119,22 @@
"node_modules/@webassemblyjs/floating-point-hex-parser": {
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz",
"integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==",
"dev": true
"integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw=="
},
"node_modules/@webassemblyjs/helper-api-error": {
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz",
"integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==",
"dev": true
"integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q=="
},
"node_modules/@webassemblyjs/helper-buffer": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz",
"integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==",
"dev": true
"integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw=="
},
"node_modules/@webassemblyjs/helper-numbers": {
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz",
"integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==",
"dev": true,
"dependencies": {
"@webassemblyjs/floating-point-hex-parser": "1.11.6",
"@webassemblyjs/helper-api-error": "1.11.6",
@@ -34208,14 +34144,12 @@
"node_modules/@webassemblyjs/helper-wasm-bytecode": {
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz",
"integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==",
"dev": true
"integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA=="
},
"node_modules/@webassemblyjs/helper-wasm-section": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz",
"integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==",
"dev": true,
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
"@webassemblyjs/helper-buffer": "1.11.6",
@@ -34227,7 +34161,6 @@
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz",
"integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==",
"dev": true,
"dependencies": {
"@xtuc/ieee754": "^1.2.0"
}
@@ -34236,7 +34169,6 @@
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz",
"integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==",
"dev": true,
"dependencies": {
"@xtuc/long": "4.2.2"
}
@@ -34244,14 +34176,12 @@
"node_modules/@webassemblyjs/utf8": {
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz",
"integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==",
"dev": true
"integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA=="
},
"node_modules/@webassemblyjs/wasm-edit": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz",
"integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==",
"dev": true,
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
"@webassemblyjs/helper-buffer": "1.11.6",
@@ -34267,7 +34197,6 @@
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz",
"integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==",
"dev": true,
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
"@webassemblyjs/helper-wasm-bytecode": "1.11.6",
@@ -34280,7 +34209,6 @@
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz",
"integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==",
"dev": true,
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
"@webassemblyjs/helper-buffer": "1.11.6",
@@ -34292,7 +34220,6 @@
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz",
"integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==",
"dev": true,
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
"@webassemblyjs/helper-api-error": "1.11.6",
@@ -34306,7 +34233,6 @@
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz",
"integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==",
"dev": true,
"dependencies": {
"@webassemblyjs/ast": "1.11.6",
"@xtuc/long": "4.2.2"
@@ -34364,14 +34290,12 @@
"node_modules/@xtuc/ieee754": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
"integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
"dev": true
"integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="
},
"node_modules/@xtuc/long": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
"dev": true
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="
},
"node_modules/@yarnpkg/lockfile": {
"version": "1.1.0",
@@ -34672,14 +34596,6 @@
"version": "1.0.1",
"license": "MIT"
},
"node_modules/async-mutex": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz",
"integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==",
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/at-least-node": {
"version": "1.0.0",
"dev": true,
@@ -35370,7 +35286,6 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
"integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==",
"dev": true,
"engines": {
"node": ">=6.0"
}
@@ -35801,7 +35716,7 @@
},
"node_modules/csstype": {
"version": "3.1.2",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/date-fns": {
@@ -36401,7 +36316,6 @@
"version": "5.17.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
"integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.2.0"
@@ -36521,8 +36435,7 @@
"node_modules/es-module-lexer": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz",
"integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==",
"dev": true
"integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw=="
},
"node_modules/es-object-atoms": {
"version": "1.0.0",
@@ -36867,7 +36780,6 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
"dev": true,
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^4.1.1"
@@ -36880,7 +36792,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
"dev": true,
"engines": {
"node": ">=4.0"
}
@@ -37044,7 +36955,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
"dev": true,
"dependencies": {
"estraverse": "^5.2.0"
},
@@ -37056,7 +36966,6 @@
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
"engines": {
"node": ">=4.0"
}
@@ -37065,7 +36974,6 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -37825,8 +37733,7 @@
"node_modules/glob-to-regexp": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
"dev": true
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="
},
"node_modules/global": {
"version": "4.4.0",
@@ -39915,8 +39822,7 @@
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"dev": true
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
},
"node_modules/json-schema-ref-resolver": {
"version": "1.0.1",
@@ -40392,7 +40298,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
"integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==",
"dev": true,
"engines": {
"node": ">=6.11.5"
}
@@ -42622,7 +42527,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
"dev": true,
"dependencies": {
"safe-buffer": "^5.1.0"
}
@@ -42644,6 +42548,30 @@
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
},
"peerDependencies": {
"react": "^18.3.1"
}
},
"node_modules/react-dom/node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"node_modules/react-freeze": {
"version": "1.0.3",
"license": "MIT",
@@ -43437,7 +43365,6 @@
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.2.0.tgz",
"integrity": "sha512-JWD+aQ0lh2gvh4NM3bBM42Kx+XybOxCpgYK7F8ugAlpaTSnWsX+39Z4XkOykGZAHrjwwTZT3x3KxswVWxHPUqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"react-is": "^18.2.0",
"react-shallow-renderer": "^16.15.0",
@@ -43563,7 +43490,6 @@
},
"node_modules/regenerator-transform": {
"version": "0.15.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.8.4"
@@ -43916,7 +43842,6 @@
},
"node_modules/serialize-javascript": {
"version": "6.0.1",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"randombytes": "^2.1.0"
@@ -44574,7 +44499,6 @@
},
"node_modules/tapable": {
"version": "2.2.1",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -44652,7 +44576,6 @@
"version": "5.3.10",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz",
"integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==",
"dev": true,
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.17",
"jest-worker": "^27.4.5",
@@ -44686,7 +44609,6 @@
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
"integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
"dev": true,
"dependencies": {
"@types/node": "*",
"merge-stream": "^2.0.0",
@@ -44700,7 +44622,6 @@
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
@@ -45277,7 +45198,6 @@
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
"integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==",
"dev": true,
"dependencies": {
"glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.1.2"
@@ -45301,7 +45221,6 @@
"version": "5.94.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz",
"integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==",
"dev": true,
"dependencies": {
"@types/eslint-scope": "^3.7.3",
"@types/estree": "^1.0.0",
@@ -45417,7 +45336,6 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
"integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
"dev": true,
"engines": {
"node": ">=10.13.0"
}

View File

@@ -54,4 +54,4 @@
"react": "18.2.0",
"react-native": "0.74.5"
}
}
}

View File

@@ -55,7 +55,7 @@ export class AppModel {
}
goBack() {
const goBackButton = this.page.locator(getTestId("route-go-back"));
const goBackButton = this.page.locator(getTestId("go-back"));
return goBackButton.click();
}

View File

@@ -39,12 +39,10 @@ export class BaseItemModel {
return await this.locator.evaluate((el) => el === document.activeElement);
}
async click(options?: { middleClick?: boolean }) {
async click() {
if (!(await this.locator.isVisible()))
await this.locator.scrollIntoViewIfNeeded();
await this.locator.click({
button: options?.middleClick ? "middle" : "left"
});
await this.locator.click();
}
async getId() {

View File

@@ -36,9 +36,6 @@ export class EditorModel {
private readonly dateEditedText: Locator;
private readonly searchButton: Locator;
private readonly tabsList: Locator;
private readonly goBackButton: Locator;
private readonly goForwardButton: Locator;
private readonly newTabButton: Locator;
readonly savedIcon: Locator;
readonly notSavedIcon: Locator;
@@ -62,9 +59,6 @@ export class EditorModel {
this.savedIcon = page.locator(getTestId("editor-save-state-saved"));
this.notSavedIcon = page.locator(getTestId("editor-save-state-notsaved"));
this.tabsList = page.locator(getTestId("tabs"));
this.goBackButton = page.locator(getTestId("go-back"));
this.goForwardButton = page.locator(getTestId("go-forward"));
this.newTabButton = page.locator(getTestId("New tab"));
}
async waitForLoading(title?: string, content?: string) {
@@ -249,26 +243,6 @@ export class EditorModel {
}
}
async getTabs() {
const tabs: TabItemModel[] = [];
for await (const item of iterateList(this.tabsList.locator(".tab"))) {
tabs.push(new TabItemModel(item, this.page));
}
return tabs;
}
async goBack() {
await this.goBackButton.click();
}
async goForward() {
await this.goForwardButton.click();
}
async newTab() {
await this.newTabButton.click();
}
async attachImage() {
await this.page
.context()

View File

@@ -38,8 +38,8 @@ export class NoteItemModel extends BaseItemModel {
this.editor = new EditorModel(this.page);
}
async openNote(openInNewTab?: boolean) {
await this.click({ middleClick: openInNewTab });
async openNote() {
await this.click();
const title = await this.getTitle();
await this.editor.waitForLoading(title);
}

View File

@@ -211,11 +211,6 @@ export class NoteContextMenuModel extends BaseProperties {
this.menu = new ContextMenuModel(page);
}
async openInNewTab() {
await this.open();
await this.menu.clickOnItem("openinnewtab");
}
async isColored(color: string): Promise<boolean> {
await this.open();
await this.menu.clickOnItem("colors");

View File

@@ -30,20 +30,6 @@ export class TabItemModel {
const testId = await this.locator.getAttribute("data-test-id");
return testId?.replace("tab-", "");
}
async click() {
return this.locator.click();
}
async title() {
return this.locator.locator(getTestId("tab-title")).textContent();
}
async isActive() {
const classList = await this.locator.getAttribute("class");
return !!classList?.includes("active");
}
close() {
return this.closeButton.click();
}

View File

@@ -17,11 +17,47 @@ 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 { test, expect } from "@playwright/test";
import { createHistorySession, PASSWORD } from "./utils";
import { test, expect, Page } from "@playwright/test";
import { AppModel } from "./models/app.model";
import { NOTE, PASSWORD } from "./utils";
test.setTimeout(60 * 1000);
async function createSession(page: Page, locked = false) {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
let note = await notes.createNote(NOTE);
if (locked) {
await note?.contextMenu.lock(PASSWORD);
await note?.openLockedNote(PASSWORD);
}
const edits = ["Some edited text.", "Some more edited text."];
for (const edit of edits) {
await notes.editor.setContent(edit);
await page.waitForTimeout(600);
await page.reload().catch(console.error);
await notes.waitForItem(NOTE.title);
note = await notes.findNote(NOTE);
locked ? await note?.openLockedNote(PASSWORD) : await note?.openNote();
}
const contents = [
`${NOTE.content}${edits[0]}${edits[1]}`,
`${NOTE.content}${edits[0]}`
];
return {
note,
notes,
app,
contents
};
}
const sessionTypes = ["locked", "unlocked"] as const;
for (const type of sessionTypes) {
@@ -30,7 +66,7 @@ for (const type of sessionTypes) {
test(`editing a note should create a new ${type} session in its session history`, async ({
page
}) => {
const { note } = await createHistorySession(page, isLocked);
const { note } = await createSession(page, isLocked);
const history = await note?.properties.getSessionHistory();
expect(history?.length).toBeGreaterThan(1);
@@ -45,7 +81,7 @@ for (const type of sessionTypes) {
test(`switching ${type} sessions should change editor content`, async ({
page
}) => {
const { note, contents } = await createHistorySession(page, isLocked);
const { note, contents } = await createSession(page, isLocked);
const history = await note?.properties.getSessionHistory();
let preview = await history?.at(1)?.open();
@@ -55,7 +91,6 @@ for (const type of sessionTypes) {
contents[1]
);
await note?.click();
if (type === "locked") await note?.openLockedNote(PASSWORD);
await note?.properties.close();
preview = await history?.at(0)?.open();
if (type === "locked") await preview?.unlock(PASSWORD);
@@ -68,10 +103,7 @@ for (const type of sessionTypes) {
test(`restoring a ${type} session should change note's content`, async ({
page
}) => {
const { note, notes, contents } = await createHistorySession(
page,
isLocked
);
const { note, notes, contents } = await createSession(page, isLocked);
const history = await note?.properties.getSessionHistory();
const preview = await history?.at(1)?.open();
if (type === "locked") await preview?.unlock(PASSWORD);
@@ -83,3 +115,44 @@ for (const type of sessionTypes) {
expect(await notes.editor.getContent("text")).toBe(contents[1]);
});
}
// test("editing locked note should create locked history sessions", async ({
// page
// }) => {
// const { note } = await createLockedSession(page);
// const history = await note?.properties.getSessionHistory();
// expect(history).toHaveLength(2);
// for (const item of history || []) {
// expect(await item.isLocked()).toBeTruthy();
// }
// });
// test("switching locked sessions should change editor content", async ({
// page
// }) => {
// const { note, notes, contents } = await createLockedSession(page);
// const history = await note?.properties.getSessionHistory();
// await history?.at(1)?.previewLocked(PASSWORD);
// await notes.editor.waitForLoading(NOTE.title, contents[1]);
// const content1 = await notes.editor.getContent("text");
// await history?.at(0)?.previewLocked(PASSWORD);
// await notes.editor.waitForLoading(NOTE.title, contents[0]);
// const content0 = await notes.editor.getContent("text");
// expect(content1).toBe(contents[1]);
// expect(content0).toBe(contents[0]);
// });
// test("restore a locked session", async ({ page }) => {
// const { note, notes, contents } = await createLockedSession(page);
// const history = await note?.properties.getSessionHistory();
// await history?.at(1)?.previewLocked(PASSWORD);
// await notes.editor.waitForLoading(NOTE.title, contents[1]);
// await notes.editor.restoreSession();
// const content = await notes.editor.getContent("text");
// expect(content).toBe(contents[1]);
// });

View File

@@ -20,9 +20,6 @@ import { test, Browser, expect } from "@playwright/test";
import { AppModel } from "./models/app.model";
import { USER } from "./utils";
// run this test file sequentially
test.describe.configure({ mode: "serial" });
async function createDevice(browser: Browser) {
// Create two isolated browser contexts
const context = await browser.newContext();
@@ -51,13 +48,13 @@ async function actAndSync<T>(
return results.slice(0, actions.length) as T[];
}
test(`content edits in a note opened on 2 devices in multiple tabs should sync in real-time`, async ({
const NOTE = {
title: `Note ${makeid(20)}`
};
test(`edits in a note opened on 2 devices should sync in real-time`, async ({
browser
}, info) => {
const NOTE = {
title: `Note ${makeid(20)}`
};
info.setTimeout(70 * 1000);
const newContent = makeid(24).repeat(2);
@@ -74,7 +71,6 @@ test(`content edits in a note opened on 2 devices in multiple tabs should sync i
)[0];
const noteA = await notesA.findNote(NOTE);
await Promise.all([noteA, noteB].map((note) => note?.openNote()));
await noteA?.openNote(true);
if ((await notesB.editor.getContent("text")) !== "")
await actAndSync([deviceA, deviceB], notesB.editor.clear());
@@ -86,52 +82,6 @@ test(`content edits in a note opened on 2 devices in multiple tabs should sync i
expect(noteB).toBeDefined();
await expect(notesA.editor.content).toHaveText(newContent);
await expect(notesB.editor.content).toHaveText(newContent);
const tabsA = await notesA.editor.getTabs();
await tabsA[0].click();
await expect(notesA.editor.content).toHaveText(newContent);
await (await deviceA.goToSettings())?.logout();
await (await deviceB.goToSettings())?.logout();
});
test(`title edits in a note opened on 2 devices in multiple tabs should sync in real-time`, async ({
browser
}, info) => {
const NOTE = {
title: `Note ${makeid(20)}`
};
info.setTimeout(70 * 1000);
const newContent = makeid(24).repeat(2);
const [deviceA, deviceB] = await Promise.all([
createDevice(browser),
createDevice(browser)
]);
const [notesA, notesB] = await Promise.all(
[deviceA, deviceB].map((d) => d.goToNotes())
);
const noteB = (
await actAndSync([deviceA, deviceB], notesB.createNote(NOTE))
)[0];
const noteA = await notesA.findNote(NOTE);
await Promise.all([noteA, noteB].map((note) => note?.openNote()));
await noteA?.openNote(true);
if ((await notesB.editor.getContent("text")) !== "")
await actAndSync([deviceA, deviceB], notesB.editor.clear());
await expect(notesA.editor.content).toBeEmpty();
await expect(notesB.editor.content).toBeEmpty();
await actAndSync([deviceA, deviceB], notesB.editor.setTitle(newContent));
expect(noteA).toBeDefined();
expect(noteB).toBeDefined();
expect(await notesA.editor.getTitle()).toBe(newContent);
expect(await notesB.editor.getTitle()).toBe(newContent);
const tabsA = await notesA.editor.getTabs();
await tabsA[0].click();
expect(await notesA.editor.getTitle()).toBe(newContent);
await (await deviceA.goToSettings())?.logout();
await (await deviceB.goToSettings())?.logout();

View File

@@ -1,256 +0,0 @@
/*
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 { test, expect } from "@playwright/test";
import { AppModel } from "./models/app.model";
import { createHistorySession } from "./utils";
test("notes should open in the same tab", async ({ page }) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
await notes.createNote({ title: "Note 1" });
await notes.createNote({ title: "Note 2" });
await notes.createNote({ title: "Note 3" });
await page.reload();
const note = await notes.findNote({ title: "Note 2" });
await note?.click();
const tabs = await notes.editor.getTabs();
expect(tabs.length).toBe(1);
});
test("new note should open in the same tab", async ({ page }) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
await notes.createNote({ title: "Note 1" });
await notes.newNote();
const tabs = await notes.editor.getTabs();
expect(tabs.length).toBe(1);
});
test("open note in new tab (using context menu)", async ({ page }) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
await notes.createNote({ title: "Note 1" });
await notes.createNote({ title: "Note 2" });
await notes.createNote({ title: "Note 3" });
const note = await notes.findNote({ title: "Note 2" });
await note?.contextMenu.openInNewTab();
await notes.editor.waitForLoading();
const tabs = await notes.editor.getTabs();
expect(tabs.length).toBe(2);
expect(await tabs[1].title()).toBe("Note 2");
});
test("open note in new tab (using middle click)", async ({ page }) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
await notes.createNote({ title: "Note 1" });
await notes.createNote({ title: "Note 2" });
await notes.createNote({ title: "Note 3" });
const note = await notes.findNote({ title: "Note 2" });
await note?.click({ middleClick: true });
await notes.editor.waitForLoading();
const tabs = await notes.editor.getTabs();
expect(tabs.length).toBe(2);
expect(await tabs[1].title()).toBe("Note 2");
});
test("go back should open previous note", async ({ page }) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const tabs = await notes.editor.getTabs();
await notes.createNote({ title: "Note 1" });
await notes.createNote({ title: "Note 2" });
await notes.createNote({ title: "Note 3" });
await notes.editor.goBack();
expect(await tabs[0].title()).toBe("Note 2");
await notes.editor.goBack();
expect(await tabs[0].title()).toBe("Note 1");
});
test("go forward should open next note", async ({ page }) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const tabs = await notes.editor.getTabs();
await notes.createNote({ title: "Note 1" });
await notes.createNote({ title: "Note 2" });
await notes.createNote({ title: "Note 3" });
await notes.editor.goBack();
await notes.editor.goBack();
await notes.editor.goForward();
expect(await tabs[0].title()).toBe("Note 2");
await notes.editor.goForward();
expect(await tabs[0].title()).toBe("Note 3");
});
test("new tab button should open a new tab", async ({ page }) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
await notes.createNote({ title: "Note 1" });
await notes.editor.newTab();
const tabs = await notes.editor.getTabs();
expect(await tabs[0].title()).toBe("Note 1");
expect(await tabs[1].title()).toBe("Untitled");
});
test("changes in a note opened in multiple tabs should sync", async ({
page
}) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const note = await notes.createNote({ title: "Note 1" });
await note?.contextMenu.openInNewTab();
await notes.editor.editAndWait(async () => {
await notes.editor.setContent("This change should sync.");
});
const tabs = await notes.editor.getTabs();
await tabs[0].click();
expect(await notes.editor.getContent("text")).toBe(
"This change should sync."
);
});
test("open same note in 2 tabs and refresh page", async ({ page }) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const note = await notes.createNote({
title: "Note 1",
content: "Some edits."
});
await note?.contextMenu.openInNewTab();
await notes.editor.waitForLoading();
await page.reload();
await notes.waitForList();
const tabs = await notes.editor.getTabs();
await tabs[0].click();
await notes.editor.waitForLoading();
expect(await notes.editor.getContent("text")).toBe("Some edits.");
});
test("reloading with a note diff open in a tab", async ({ page }) => {
const { note, contents } = await createHistorySession(page);
const history = await note?.properties.getSessionHistory();
const preview = await history?.[0].open();
await preview!.firstEditor.waitFor({ state: "visible" });
await page.reload();
await preview!.firstEditor.waitFor({ state: "visible" });
await expect(preview!.firstEditor.locator(".ProseMirror")).toHaveText(
contents[0]
);
await expect(preview!.secondEditor.locator(".ProseMirror")).toHaveText(
contents[0]
);
});
test("navigate back and forth between normal and diff session", async ({
page
}) => {
const { note, contents, notes } = await createHistorySession(page);
const history = await note?.properties.getSessionHistory();
const preview = await history?.[0].open();
await preview!.firstEditor.waitFor({ state: "visible" });
await notes.editor.goBack();
await preview!.firstEditor.waitFor({ state: "hidden" });
expect(await notes.editor.getContent("text")).toBe(contents[0]);
await notes.editor.goForward();
await preview!.firstEditor.waitFor({ state: "visible" });
await expect(preview!.firstEditor.locator(".ProseMirror")).toHaveText(
contents[0]
);
await expect(preview!.secondEditor.locator(".ProseMirror")).toHaveText(
contents[0]
);
});
test("clicking on a note that's already opened in another tab should focus the tab", async ({
page
}) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const note = await notes.createNote({
title: "Note 1"
});
await note?.contextMenu.openInNewTab();
await notes.editor.waitForLoading();
await notes.createNote({
title: "Note 2"
});
await note?.openNote();
const tabs = await notes.editor.getTabs();
expect(await tabs[0].isActive()).toBe(true);
});
test("open a note in 2 tabs then open another note and navigate back", async ({
page
}) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const note = await notes.createNote({
title: "Note 1"
});
await note?.contextMenu.openInNewTab();
await notes.editor.waitForLoading();
await notes.createNote({
title: "Note 2"
});
await notes.editor.goBack();
const tabs = await notes.editor.getTabs();
expect(await tabs[1].isActive()).toBe(true);
expect(await tabs[1].title()).toBe("Note 1");
});
test.skip("TODO: open a locked note, switch to another note and navigate back", () => {});
test.skip("TODO: open a locked note, switch to another note, unlock the note and navigate back", () => {});

View File

@@ -28,7 +28,6 @@ import {
SortByOptions
} from "../models/types";
import { tmpdir } from "os";
import { AppModel } from "../models/app.model";
type Note = {
title: string;
@@ -75,10 +74,7 @@ const PASSWORD = "123abc123abc";
const APP_LOCK_PASSWORD = "lockapporelse🔪";
function getTestId(
id: string,
variant: "data-test-id" | "data-testid" = "data-test-id"
) {
function getTestId(id: string, variant: "data-test-id" | "data-testid" = "data-test-id") {
return `[${variant}="${id}"]`;
}
@@ -157,41 +153,6 @@ const groupByOptions: GroupByOptions[] = [
"week"
];
export async function createHistorySession(page: Page, locked = false) {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
let note = await notes.createNote(NOTE);
if (locked) {
await note?.contextMenu.lock(PASSWORD);
await note?.openLockedNote(PASSWORD);
}
const edits = ["Some edited text.", "Some more edited text."];
for (const edit of edits) {
await notes.editor.setContent(edit);
await page.waitForTimeout(600);
await page.reload().catch(console.error);
await notes.waitForItem(NOTE.title);
note = await notes.findNote(NOTE);
locked ? await note?.openLockedNote(PASSWORD) : await note?.openNote();
}
const contents = [
`${NOTE.content}${edits[0]}${edits[1]}`,
`${NOTE.content}${edits[0]}`
];
return {
note,
notes,
app,
contents
};
}
export {
USER,
NOTE,

View File

@@ -18,16 +18,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import hotkeys from "hotkeys-js";
import { GlobalKeyboard } from "../utils/keyboard";
import { useEditorStore } from "../stores/editor-store";
import { useStore as useSearchStore } from "../stores/search-store";
import { useEditorManager } from "../components/editor/manager";
function isInEditor(e: KeyboardEvent) {
return (
e.target instanceof HTMLElement && !!e.target?.closest(".editor-container")
);
}
const KEYMAP = [
// {
// keys: ["command+n", "ctrl+n", "command+alt+n", "ctrl+alt+n"],
@@ -59,34 +54,15 @@ const KEYMAP = [
// });
// },
// },
{
keys: [
"command+option+right",
"ctrl+alt+right",
"command+option+shift+right",
"ctrl+alt+shift+right"
],
description: "Go to next tab",
global: false,
action: () => useEditorStore.getState().focusNextTab()
},
{
keys: [
"command+option+left",
"ctrl+alt+left",
"command+option+shift+left",
"ctrl+alt+shift+left"
],
description: "Go to next tab",
global: false,
action: () => useEditorStore.getState().focusPreviousTab()
},
{
keys: ["command+f", "ctrl+f"],
description: "Search all notes",
global: false,
action: (e: KeyboardEvent) => {
if (isInEditor(e)) {
const isInEditor =
e.target instanceof HTMLElement &&
!!e.target?.closest(".editor-container");
if (isInEditor) {
const activeSession = useEditorStore.getState().getActiveSession();
if (activeSession?.type === "readonly") {
e.preventDefault();
@@ -173,9 +149,14 @@ export function registerKeyMap() {
};
KEYMAP.forEach((key) => {
hotkeys(key.keys.join(","), (e) => {
e.preventDefault();
key.action?.(e);
});
hotkeys(
key.keys.join(","),
{
element: key.global
? (GlobalKeyboard as unknown as HTMLElement)
: document.body
},
key.action
);
});
}

View File

@@ -17,13 +17,7 @@ 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 {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState
} from "react";
import { useCallback, useLayoutEffect, useRef, useState } from "react";
import { Flex, Text, Button } from "@theme-ui/components";
import { Copy, Restore } from "../icons";
import ContentToggle from "./content-toggle";
@@ -31,7 +25,6 @@ import { store as notesStore } from "../../stores/note-store";
import { db } from "../../common/db";
import {
ConflictedEditorSession,
DiffEditorSession,
useEditorStore
} from "../../stores/editor-store";
import { ScrollSync, ScrollSyncPane } from "react-scroll-sync";
@@ -42,7 +35,7 @@ import { getFormattedDate } from "@notesnook/common";
import { diff } from "diffblazer";
import { strings } from "@notesnook/intl";
type DiffViewerProps = { session: ConflictedEditorSession | DiffEditorSession };
type DiffViewerProps = { session: ConflictedEditorSession };
function DiffViewer(props: DiffViewerProps) {
const { session } = props;
@@ -51,11 +44,7 @@ function DiffViewer(props: DiffViewerProps) {
const [conflictedContent, setConflictedContent] = useState(
content?.conflicted
);
useEffect(() => {
setContent(session.content);
setConflictedContent(session.content?.conflicted);
}, [session.content]);
const root = useRef<HTMLDivElement>(null);
const onResolveContent = useCallback(
(saveCopy: boolean) => {
@@ -82,13 +71,19 @@ function DiffViewer(props: DiffViewerProps) {
});
}, [session]);
useLayoutEffect(() => {
const element = root.current;
element?.classList.add("active");
return () => {
element?.classList.remove("active");
};
}, []);
if (!conflictedContent || !content) return null;
return (
<Flex
ref={(el) => {
if (el) el.classList.add("active");
}}
ref={root}
className="diffviewer"
data-test-id="diff-viewer"
sx={{
@@ -117,15 +112,12 @@ function DiffViewer(props: DiffViewerProps) {
variant="secondary"
data-test-id="restore-session"
onClick={async () => {
const { closeTabs, openSession, getSessionsForNote } =
const { closeSessions, openSession } =
useEditorStore.getState();
await db.noteHistory.restore(session.historySessionId);
await db.noteHistory.restore(session.id);
closeTabs(
session.id,
...getSessionsForNote(session.note.id).map((s) => s.id)
);
closeSessions(session.id, session.note.id);
await notesStore.refresh();
await openSession(session.note.id, { force: true });
@@ -143,11 +135,12 @@ function DiffViewer(props: DiffViewerProps) {
<Button
variant="secondary"
onClick={async () => {
const { closeTabs, openSession } = useEditorStore.getState();
const { closeSessions, openSession } =
useEditorStore.getState();
const noteId = await createCopy(session.note, content);
closeTabs(session.id);
closeSessions(session.id);
await notesStore.refresh();
await openSession(noteId);

View File

@@ -18,10 +18,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Button, Flex, Text } from "@theme-ui/components";
import React, { useEffect, useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import {
ArrowLeft,
ArrowRight,
Cross,
EditorFullWidth,
EditorNormalWidth,
@@ -29,12 +28,10 @@ import {
FocusMode,
Fullscreen,
Lock,
NewTab,
NormalMode,
Note,
NoteRemove,
Pin,
Plus,
Properties,
Publish,
Published,
@@ -89,10 +86,10 @@ import { isMac } from "../../utils/platform";
export function EditorActionBar() {
const { isMaximized, isFullscreen, hasNativeWindowControls } =
useWindowControls();
const editorMargins = useEditorStore((store) => store.editorMargins);
const isFocusMode = useAppStore((store) => store.isFocusMode);
const activeTab = useEditorStore((store) => store.getActiveTab());
const activeSession = useEditorStore((store) =>
activeTab ? store.getSession(activeTab.sessionId) : undefined
store.activeSessionId ? store.getSession(store.activeSessionId) : undefined
);
const editorManager = useEditorManager((store) =>
activeSession?.id ? store.editors[activeSession?.id] : undefined
@@ -100,19 +97,11 @@ export function EditorActionBar() {
const isLoggedIn = useUserStore((store) => store.isLoggedIn);
const monographs = useMonographStore((store) => store.monographs);
const isNotePublished =
activeSession &&
"note" in activeSession &&
db.monographs.isPublished(activeSession.note.id);
activeSession && db.monographs.isPublished(activeSession.id);
const isMobile = useMobile();
const isTablet = useTablet();
const tools = [
{
title: strings.newTab(),
icon: NewTab,
enabled: true,
onClick: () => useEditorStore.getState().addTab()
},
{
title: strings.undo(),
icon: Undo,
@@ -139,6 +128,44 @@ export function EditorActionBar() {
activeSession.type === "readonly") &&
showPublishView(activeSession.note, "top")
},
{
title: editorMargins
? strings.disableEditorMargins()
: strings.enableEditorMargins(),
icon: editorMargins ? EditorNormalWidth : EditorFullWidth,
enabled: true,
hideOnMobile: true,
onClick: () => useEditorStore.getState().toggleEditorMargins()
},
{
title: isFullscreen
? strings.exitFullScreen()
: strings.enterFullScreen(),
icon: isFullscreen ? ExitFullscreen : Fullscreen,
enabled: true,
hidden: !isFocusMode,
hideOnMobile: true,
onClick: () => {
if (isFullscreen) {
exitFullscreen();
} else {
enterFullscreen(document.documentElement);
}
}
},
{
title: isFocusMode ? strings.normalMode() : strings.focusMode(),
icon: isFocusMode ? FocusMode : NormalMode,
enabled: true,
hideOnMobile: true,
onClick: () => {
useAppStore.getState().toggleFocusMode();
if (document.fullscreenElement) exitFullscreen();
const id = useEditorStore.getState().activeSessionId;
const editor = id && useEditorManager.getState().getEditor(id);
if (editor) editor.editor?.focus();
}
},
{
title: strings.toc(),
icon: TableOfContents,
@@ -211,10 +238,7 @@ export function EditorActionBar() {
mr:
hasNativeWindowControls && !isMac() && !isMobile && !isTablet
? `calc(100vw - env(titlebar-area-width))`
: 1,
pl: 1,
borderLeft: "1px solid var(--border)",
flexShrink: 0
: 0
}}
>
{tools.map((tool) => (
@@ -225,13 +249,14 @@ export function EditorActionBar() {
title={tool.title}
key={tool.title}
sx={{
p: 1,
height: "100%",
alignItems: "center",
bg: "transparent",
display: [
"hideOnMobile" in tool && tool.hideOnMobile ? "none" : "flex",
tool.hidden ? "none" : "flex"
],
borderRadius: 0,
flexShrink: 0,
"&:hover svg path": {
fill:
@@ -242,7 +267,7 @@ export function EditorActionBar() {
}}
onClick={tool.onClick}
>
<tool.icon size={16} />
<tool.icon size={18} />
</Button>
))}
</Flex>
@@ -250,209 +275,183 @@ export function EditorActionBar() {
);
}
const TabStrip = React.memo(function TabStrip() {
useEditorStore((store) => store.getActiveSession()); // otherwise the tab title won't update on opening a note
const tabs = useEditorStore((store) => store.tabs);
const currentTab = useEditorStore((store) => store.activeTabId);
const canGoBack = useEditorStore((store) => store.canGoBack);
const canGoForward = useEditorStore((store) => store.canGoForward);
function TabStrip() {
const sessions = useEditorStore((store) => store.sessions);
const activeSessionId = useEditorStore((store) => store.activeSessionId);
return (
<Flex sx={{ flex: 1 }}>
<ScrollContainer
className="tabsScroll"
suppressScrollY
style={{ flex: 1, height: TITLE_BAR_HEIGHT }}
trackStyle={() => ({
backgroundColor: "transparent",
"--ms-track-size": "6px"
})}
thumbStyle={() => ({ height: 3 })}
onWheel={(e) => {
const scrollcontainer = document.querySelector(".tabsScroll");
if (!scrollcontainer) return;
if (e.deltaY > 0) scrollcontainer.scrollLeft += 100;
else if (e.deltaY < 0) scrollcontainer.scrollLeft -= 100;
}}
>
<Flex
sx={{
px: 1,
borderRight: "1px solid var(--border)",
alignItems: "center",
flexShrink: 0
flex: 1,
height: "100%"
}}
onDoubleClick={(e) => e.stopPropagation()}
>
<Button
disabled={!canGoBack}
onClick={() => useEditorStore.getState().goBack()}
variant="secondary"
sx={{ p: 1, bg: "transparent" }}
data-test-id="go-back"
>
<ArrowLeft size={16} />
</Button>
<Button
disabled={!canGoForward}
onClick={() => useEditorStore.getState().goForward()}
variant="secondary"
sx={{ p: 1, bg: "transparent" }}
data-test-id="go-forward"
>
<ArrowRight size={16} />
</Button>
</Flex>
<ScrollContainer
className="tabsScroll"
suppressScrollY
style={{ flex: 1, height: TITLE_BAR_HEIGHT }}
trackStyle={() => ({
backgroundColor: "transparent",
"--ms-track-size": "6px"
})}
thumbStyle={() => ({ height: 3 })}
onWheel={(e) => {
const scrollcontainer = document.querySelector(".tabsScroll");
if (!scrollcontainer) return;
if (e.deltaY > 0) scrollcontainer.scrollLeft += 100;
else if (e.deltaY < 0) scrollcontainer.scrollLeft -= 100;
onDoubleClick={async (e) => {
e.stopPropagation();
useEditorStore.getState().newSession();
}}
data-test-id="tabs"
>
<Flex
sx={{
flex: 1,
height: "100%"
<ReorderableList
items={sessions}
moveItem={(from, to) => {
if (from === to) return;
const sessions = useEditorStore.getState().sessions.slice();
const isToPinned = sessions[to].pinned;
const [fromTab] = sessions.splice(from, 1);
// if the tab where this tab is being dropped is pinned,
// let's pin our tab too.
if (isToPinned) {
fromTab.pinned = true;
fromTab.preview = false;
}
// unpin the tab if it is moved.
else if (fromTab.pinned) fromTab.pinned = false;
sessions.splice(to, 0, fromTab);
useEditorStore.setState({ sessions });
}}
onDoubleClick={async (e) => {
e.stopPropagation();
useEditorStore.getState().addTab();
}}
data-test-id="tabs"
>
<ReorderableList
items={tabs}
moveItem={(from, to) => {
if (from === to) return;
const tabs = useEditorStore.getState().tabs.slice();
const isToPinned = tabs[to].pinned;
const [fromTab] = tabs.splice(from, 1);
// if the tab where this tab is being dropped is pinned,
// let's pin our tab too.
if (isToPinned) {
fromTab.pinned = true;
}
// unpin the tab if it is moved.
else if (fromTab.pinned) fromTab.pinned = false;
tabs.splice(to, 0, fromTab);
useEditorStore.setState({ tabs });
}}
renderItem={({ item: tab, index: i }) => {
const session = useEditorStore
.getState()
.getSession(tab.sessionId);
if (!session) return null;
const isUnsaved =
session.type === "default" &&
session.saveState === SaveState.NotSaved;
return (
<Tab
id={tab.id}
key={tab.sessionId}
title={
session.title ||
("note" in session
? session.note.title
: strings.untitled())
renderItem={({ item: session, index: i }) => {
const isUnsaved =
session.type === "default" &&
session.saveState === SaveState.NotSaved;
return (
<Tab
id={session.id}
key={session.id}
title={
session.title ||
("note" in session ? session.note.title : "Untitled")
}
isUnsaved={isUnsaved}
isTemporary={!!session.preview}
isActive={session.id === activeSessionId}
isPinned={!!session.pinned}
isLocked={isLockedSession(session)}
type={session.type}
onKeepOpen={() =>
useEditorStore
.getState()
.updateSession(
session.id,
[session.type],
(s) => (s.preview = false)
)
}
onFocus={() => {
if (session.id !== activeSessionId) {
useEditorStore.getState().openSession(session.id);
}
isUnsaved={isUnsaved}
isActive={tab.id === currentTab}
isPinned={!!tab.pinned}
isLocked={isLockedSession(session)}
type={session.type}
onFocus={() => {
if (tab.id !== currentTab) {
useEditorStore.getState().activateSession(tab.sessionId);
}
}}
onClose={() => useEditorStore.getState().closeTabs(tab.id)}
onCloseAll={() =>
useEditorStore
.getState()
.closeTabs(
...tabs.filter((s) => !s.pinned).map((s) => s.id)
)
}
onCloseOthers={() =>
useEditorStore
.getState()
.closeTabs(
...tabs
.filter((s) => s.id !== tab.id && !s.pinned)
.map((s) => s.id)
)
}
onCloseToTheRight={() =>
useEditorStore
.getState()
.closeTabs(
...tabs
.filter((s, index) => index > i && !s.pinned)
.map((s) => s.id)
)
}
onCloseToTheLeft={() =>
useEditorStore
.getState()
.closeTabs(
...tabs
.filter((s, index) => index < i && !s.pinned)
.map((s) => s.id)
)
}
onRevealInList={
"note" in session
? () =>
AppEventManager.publish(
AppEvents.revealItemInList,
session.note.id,
true
)
: undefined
}
onPin={() => {
const tabs = useEditorStore.getState().tabs.slice();
const index = tabs.findIndex((t) => t.id === tab.id);
tabs[index].pinned = !tabs[index].pinned;
tabs.sort((a, b) =>
}}
onClose={() =>
useEditorStore.getState().closeSessions(session.id)
}
onCloseAll={() =>
useEditorStore
.getState()
.closeSessions(
...sessions.filter((s) => !s.pinned).map((s) => s.id)
)
}
onCloseOthers={() =>
useEditorStore
.getState()
.closeSessions(
...sessions
.filter((s) => s.id !== session.id && !s.pinned)
.map((s) => s.id)
)
}
onCloseToTheRight={() =>
useEditorStore
.getState()
.closeSessions(
...sessions
.filter((s, index) => index > i && !s.pinned)
.map((s) => s.id)
)
}
onCloseToTheLeft={() =>
useEditorStore
.getState()
.closeSessions(
...sessions
.filter((s, index) => index < i && !s.pinned)
.map((s) => s.id)
)
}
onRevealInList={() =>
AppEventManager.publish(
AppEvents.revealItemInList,
"note" in session ? session.note.id : session.id,
true
)
}
onPin={() => {
useEditorStore.setState((state) => {
// preview tabs can never be pinned.
if (!session.pinned) state.sessions[i].preview = false;
state.sessions[i].pinned = !session.pinned;
state.sessions.sort((a, b) =>
a.pinned === b.pinned ? 0 : a.pinned ? -1 : 1
);
useEditorStore.setState({ tabs });
}}
/>
);
}}
/>
</Flex>
</ScrollContainer>
</Flex>
});
}}
/>
);
}}
/>
</Flex>
</ScrollContainer>
);
});
}
type TabProps = {
id: string;
title: string;
isActive: boolean;
isTemporary: boolean;
isPinned: boolean;
isLocked: boolean;
isUnsaved: boolean;
type: SessionType;
onKeepOpen: () => void;
onFocus: () => void;
onClose: () => void;
onCloseOthers: () => void;
onCloseToTheRight: () => void;
onCloseToTheLeft: () => void;
onCloseAll: () => void;
onRevealInList: () => void;
onPin: () => void;
onRevealInList?: () => void;
};
function Tab(props: TabProps) {
const {
id,
title,
isActive,
isTemporary,
isPinned,
isLocked,
isUnsaved,
type,
onKeepOpen,
onFocus,
onClose,
onCloseAll,
@@ -494,14 +493,13 @@ function Tab(props: TabProps) {
setNodeRef(el);
activeTabRef.current = el;
}}
className={`tab${isActive || active?.id === id ? " active" : ""}`}
className="tab"
data-test-id={`tab-${id}`}
sx={{
height: "100%",
cursor: "pointer",
pl: 2,
px: 2,
borderRight: "1px solid var(--border)",
":last-of-type": { borderRight: 0 },
transform: CSS.Transform.toString(transform),
transition,
@@ -513,7 +511,7 @@ function Tab(props: TabProps) {
flexShrink: 0,
":hover": {
"& .closeTabButton": {
opacity: 1
visibility: "visible"
},
bg: isActive ? "hover-selected" : "hover"
}
@@ -556,10 +554,16 @@ function Tab(props: TabProps) {
type: "button",
title: strings.revealInList(),
key: "reveal-in-list",
onClick: onRevealInList,
isHidden: !onRevealInList
onClick: onRevealInList
},
{ type: "separator", key: "sep" },
{
type: "button",
key: "keep-open",
title: strings.keepOpen(),
onClick: onKeepOpen,
isDisabled: !isTemporary
},
{ type: "separator", key: "sep2" },
{
type: "button",
key: "pin",
@@ -570,6 +574,10 @@ function Tab(props: TabProps) {
}
]);
}}
onDoubleClick={(e) => {
e.stopPropagation();
if (isTemporary) onKeepOpen();
}}
onAuxClick={(e) => {
if (e.button == 1) onClose();
}}
@@ -577,25 +585,26 @@ function Tab(props: TabProps) {
{...attributes}
>
<Flex
mr={1}
onMouseUp={(e) => {
if (e.button == 0) onFocus();
}}
>
<Icon
data-test-id={`tab-icon${isUnsaved ? "-unsaved" : ""}`}
size={14}
size={16}
color={
isUnsaved ? "accent-error" : isActive ? "accent-selected" : "icon"
}
/>
<Text
data-test-id="tab-title"
variant="body"
sx={{
whiteSpace: "nowrap",
textOverflow: "ellipsis",
overflowX: "hidden",
pointerEvents: "none",
fontStyle: isTemporary ? "italic" : "normal",
maxWidth: 120,
color: isActive ? "paragraph-selected" : "paragraph"
}}
@@ -607,33 +616,34 @@ function Tab(props: TabProps) {
{isPinned ? (
<Pin
sx={{
":hover": { bg: "border" },
borderRadius: "default",
flexShrink: 0,
ml: "small",
mr: 1,
"&:hover": {
bg: "hover-secondary"
}
flexShrink: 0
}}
size={14}
onClick={onPin}
onMouseUp={(e) => {
if (e.button == 0) {
e.stopPropagation();
onPin();
}
}}
/>
) : (
<Cross
sx={{
visibility: isActive && active?.id !== id ? "visible" : "hidden",
":hover": { bg: "border" },
borderRadius: "default",
flexShrink: 0,
opacity: isActive || active?.id === id ? 1 : 0,
ml: "small",
mr: 1,
"&:hover": {
bg: "hover-secondary"
flexShrink: 0
}}
onMouseUp={(e) => {
if (e.button == 0) {
onClose();
}
}}
onClick={onClose}
className="closeTabButton"
data-test-id={"tab-close-button"}
size={14}
size={16}
/>
)}
</Flex>
@@ -642,7 +652,7 @@ function Tab(props: TabProps) {
type ReorderableListProps<T> = {
items: T[];
renderItem: (props: { item: T; index: number }) => JSX.Element | null;
renderItem: (props: { item: T; index: number }) => JSX.Element;
moveItem: (from: number, to: number) => void;
};
@@ -704,3 +714,12 @@ function ReorderableList<T extends { id: string }>(
</DndContext>
);
}
function enterFullscreen(elem: HTMLElement) {
elem.requestFullscreen();
}
function exitFullscreen() {
if (!document.fullscreenElement) return;
document.exitFullscreen();
}

View File

@@ -19,31 +19,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Button, Flex, Text } from "@theme-ui/components";
import { SaveState, useEditorStore } from "../../stores/editor-store";
import { useStore as useAppStore } from "../../stores/app-store";
import {
Loading,
Saved,
NotSaved,
FocusMode,
Plus,
Minus,
EditorNormalWidth,
TableOfContents,
ExitFullscreen,
Fullscreen,
EditorFullWidth,
NormalMode
} from "../icons";
import {
useEditorConfig,
useNoteStatistics,
useEditorManager
} from "./manager";
import { Loading, Saved, NotSaved } from "../icons";
import { useEditorConfig, useNoteStatistics } from "./manager";
import { getFormattedDate } from "@notesnook/common";
import { MAX_AUTO_SAVEABLE_WORDS } from "./types";
import { strings } from "@notesnook/intl";
import { EDITOR_ZOOM } from "./common";
import { useWindowControls } from "../../hooks/use-window-controls";
const SAVE_STATE_ICON_MAP = {
"-1": NotSaved,
@@ -52,59 +33,15 @@ const SAVE_STATE_ICON_MAP = {
};
function EditorFooter() {
const { isFullscreen } = useWindowControls();
const { words } = useNoteStatistics();
const session = useEditorStore((store) => store.getActiveSession());
const { editorConfig, setEditorConfig } = useEditorConfig();
const editorMargins = useEditorStore((store) => store.editorMargins);
const isFocusMode = useAppStore((store) => store.isFocusMode);
if (!session) return null;
const saveState =
session.type === "default" ? session.saveState : SaveState.NotSaved;
const dateEdited = "note" in session ? session.note.dateEdited : 0;
const SaveStateIcon = SAVE_STATE_ICON_MAP[saveState];
const tools = [
{
title: editorMargins
? strings.disableEditorMargins()
: strings.enableEditorMargins(),
icon: editorMargins ? EditorNormalWidth : EditorFullWidth,
enabled: true,
hideOnMobile: true,
onClick: () => useEditorStore.getState().toggleEditorMargins()
},
{
title: isFullscreen
? strings.exitFullScreen()
: strings.enterFullScreen(),
icon: isFullscreen ? ExitFullscreen : Fullscreen,
enabled: true,
hidden: !isFocusMode,
hideOnMobile: true,
onClick: () => {
if (isFullscreen) {
exitFullscreen();
} else {
enterFullscreen(document.documentElement);
}
}
},
{
title: isFocusMode ? strings.normalMode() : strings.focusMode(),
icon: isFocusMode ? FocusMode : NormalMode,
enabled: true,
hideOnMobile: true,
onClick: () => {
useAppStore.getState().toggleFocusMode();
if (document.fullscreenElement) exitFullscreen();
const editor =
session && useEditorManager.getState().getEditor(session.id);
if (editor) editor.editor?.focus();
}
}
];
return (
<Flex sx={{ alignItems: "center", justifyContent: "center", gap: 2 }}>
@@ -112,28 +49,9 @@ function EditorFooter() {
sx={{
alignItems: "center",
justifyContent: "center",
flex: 1,
height: "100%"
flex: 1
}}
>
{tools.map((tool) => (
<Button
data-test-id={tool.title}
disabled={!tool.enabled}
title={tool.title}
key={tool.title}
onClick={tool.onClick}
sx={{
py: 0,
px: 1,
height: "100%",
display: tool.hidden ? "none" : "block"
}}
variant="icon"
>
<tool.icon size={13} />
</Button>
))}
<Button
variant="icon"
onClick={() =>
@@ -145,11 +63,15 @@ function EditorFooter() {
})
}
disabled={editorConfig.zoom <= EDITOR_ZOOM.MIN}
sx={{ py: 0, px: 1, height: "100%" }}
sx={{ py: 0, height: "100%" }}
>
<Minus size={13} />
<b>-</b>
</Button>
<Text variant="subBody" sx={{ color: "paragraph" }}>
<Text
className="selectable"
variant="subBody"
sx={{ color: "paragraph" }}
>
{editorConfig.zoom}%
</Text>
<Button
@@ -163,9 +85,9 @@ function EditorFooter() {
})
}
disabled={editorConfig.zoom >= EDITOR_ZOOM.MAX}
sx={{ py: 0, px: 1, height: "100%" }}
sx={{ py: 0, height: "100%" }}
>
<Plus size={13} />
<b>+</b>
</Button>
</Flex>
{words.total > MAX_AUTO_SAVEABLE_WORDS ? (
@@ -220,12 +142,3 @@ function EditorFooter() {
);
}
export default EditorFooter;
function enterFullscreen(elem: HTMLElement) {
elem.requestFullscreen();
}
function exitFullscreen() {
if (!document.fullscreenElement) return;
document.exitFullscreen();
}

View File

@@ -76,7 +76,7 @@ import { TITLE_BAR_HEIGHT } from "../title-bar";
const PDFPreview = React.lazy(() => import("../pdf-preview"));
const autoSaveToast = { show: true, hide: () => {} };
const autoSaveToast = { show: true, hide: () => { } };
async function saveContent(
noteId: string,
@@ -116,15 +116,40 @@ async function saveContent(
const deferredSave = debounceWithId(saveContent, 100);
export default function TabsView() {
const tabs = useEditorStore((store) => store.tabs);
const sessions = useEditorStore((store) => store.sessions);
const documentPreview = useEditorStore((store) => store.documentPreview);
const activeTab = useEditorStore((store) => store.getActiveTab());
const activeSessionId = useEditorStore((store) => store.activeSessionId);
const arePropertiesVisible = useEditorStore(
(store) => store.arePropertiesVisible
);
const isTOCVisible = useEditorStore((store) => store.isTOCVisible);
const [dropRef, overlayRef] = useDragOverlay();
useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
if (
(event.ctrlKey || event.metaKey) &&
event.altKey &&
event.key === "ArrowRight"
) {
event.preventDefault();
useEditorStore.getState().openNextSession();
}
if (
(event.ctrlKey || event.metaKey) &&
event.altKey &&
event.key === "ArrowLeft"
) {
event.preventDefault();
useEditorStore.getState().openPreviousSession();
}
};
document.body.addEventListener("keydown", onKeyDown);
return () => {
document.body.removeEventListener("keydown", onKeyDown);
};
}, []);
return (
<>
<Flex
@@ -151,24 +176,17 @@ export default function TabsView() {
>
<SplitPane direction="vertical" autoSaveId={"editor-panels"}>
<Pane id="editor-panel" className="editor-pane">
{tabs.map((tab) => {
const session = useEditorStore
.getState()
.getSession(tab.sessionId);
if (!session) return null;
return (
<Freeze key={session.id} freeze={tab.id !== activeTab?.id}>
{session.type === "locked" ? (
<UnlockNoteView session={session} />
) : session.type === "conflicted" ||
session.type === "diff" ? (
<DiffViewer session={session} />
) : (
<MemoizedEditorView session={session} />
)}
</Freeze>
);
})}
{sessions.map((session) => (
<Freeze key={session.id} freeze={session.id !== activeSessionId}>
{session.type === "locked" ? (
<UnlockNoteView session={session} />
) : session.type === "conflicted" || session.type === "diff" ? (
<DiffViewer session={session} />
) : (
<MemoizedEditorView session={session} />
)}
</Freeze>
))}
</Pane>
{documentPreview ? (
@@ -208,15 +226,15 @@ export default function TabsView() {
</Pane>
) : null}
{isTOCVisible && activeTab ? (
{isTOCVisible && activeSessionId ? (
<Pane id="table-of-contents-pane" initialSize={300} minSize={300}>
<TableOfContents sessionId={activeTab.sessionId} />
<TableOfContents sessionId={activeSessionId} />
</Pane>
) : null}
</SplitPane>
<DropZone overlayRef={overlayRef} />
{arePropertiesVisible && activeTab && (
<Properties sessionId={activeTab.sessionId} />
{arePropertiesVisible && activeSessionId && (
<Properties sessionId={activeSessionId} />
)}
</ScopedThemeProvider>
</>
@@ -234,10 +252,10 @@ function EditorView({
session
}: {
session:
| DefaultEditorSession
| NewEditorSession
| ReadonlyEditorSession
| DeletedEditorSession;
| DefaultEditorSession
| NewEditorSession
| ReadonlyEditorSession
| DeletedEditorSession;
}) {
const lastChangedTime = useRef<number>(0);
const root = useRef<HTMLDivElement>(null);
@@ -276,7 +294,6 @@ function EditorView({
editor.updateContent(result.data);
} else if (isNote && session.note.title !== item.title) {
AppEventManager.publish(AppEvents.changeNoteTitle, {
sessionId: session.id,
title: item.title,
preventSave: true
});
@@ -302,7 +319,7 @@ function EditorView({
if (!session.needsHydration && session.content) {
editor?.updateContent(session.content.data);
}
}, [editor, session]);
}, [editor, session.needsHydration]);
return (
<Flex
@@ -330,39 +347,19 @@ function EditorView({
onSave={(content, ignoreEdit) => {
const currentSession = useEditorStore
.getState()
.getSession(session.id);
const noteId =
currentSession && "note" in currentSession
? currentSession.note.id
: null;
const sessions = noteId
? useEditorStore.getState().getSessionsForNote(noteId)
: [currentSession];
.getSession(session.id, ["default", "readonly", "new"]);
if (!currentSession) return;
const currentSessionId = session.id;
const data = content();
for (const session of sessions) {
if (
session?.type !== "default" &&
session?.type !== "readonly" &&
session?.type !== "new"
)
continue;
if (!session.content) session.content = { type: "tiptap", data };
else session.content.data = data;
// update content in other tabs
if (session.id !== currentSessionId) {
const editor = useEditorManager.getState().getEditor(session.id);
editor?.editor?.updateContent(data);
}
}
if (!currentSession.content)
currentSession.content = { type: "tiptap", data };
else currentSession.content.data = data;
logger.debug("scheduling save", {
id: session.id,
length: data.length
});
deferredSave(session.id, session.id, ignoreEdit, data);
deferredSave(currentSession.id, currentSession.id, ignoreEdit, data);
}}
options={{
readonly: session?.type === "readonly" || session?.type === "deleted",
@@ -417,9 +414,9 @@ function DownloadAttachmentProgress(props: DownloadAttachmentProgressProps) {
variant="secondary"
mt={2}
onClick={() => {
const note = useEditorStore.getState().getActiveNote();
const id = useEditorStore.getState().activeSessionId;
useEditorStore.setState({ documentPreview: undefined });
if (note) db.fs().cancel(note.id).catch(console.error);
if (id) db.fs().cancel(id).catch(console.error);
}}
>
{strings.cancel()}
@@ -496,14 +493,6 @@ export function Editor(props: EditorProps) {
return () => unsub();
}, []);
useEffect(() => {
return () => {
const editor = useEditorManager.getState().getEditor(id)?.editor;
const selection = editor?.getSelection();
if (selection) Config.set(`${id}:selection`, selection);
};
}, [id]);
return (
<EditorChrome {...props}>
<Tiptap
@@ -895,7 +884,7 @@ function UnlockNoteView(props: UnlockNoteViewProps) {
subtitle={strings.enterPasswordToUnlockNote()}
title={session.note.title}
unlock={async (password) => {
const note = await db.vault.open(session.note.id, password);
const note = await db.vault.open(session.id, password);
if (!note || !note.content)
throw new Error("note with this id does not exist.");
@@ -908,7 +897,8 @@ function UnlockNoteView(props: UnlockNoteViewProps) {
saveState: SaveState.Saved,
sessionId: `${Date.now()}`,
tags,
tabId: session.tabId,
pinned: session.pinned,
preview: session.preview,
content: note.content
});
}}

View File

@@ -604,11 +604,7 @@ function toIEditor(editor: Editor): IEditor {
),
startSearch: () => editor.commands.startSearch(),
getContent: () =>
getHTMLFromFragment(editor.state.doc.content, editor.schema),
getSelection: () => {
const { from, to } = editor.state.selection;
return { from, to };
}
getHTMLFromFragment(editor.state.doc.content, editor.schema)
};
}

View File

@@ -17,14 +17,7 @@ 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, {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState
} from "react";
import React, { useCallback, useEffect, useMemo, useRef } from "react";
import { Input } from "@theme-ui/components";
import { useEditorStore } from "../../stores/editor-store";
import { debounceWithId } from "@notesnook/common";
@@ -47,7 +40,7 @@ function TitleBox(props: TitleBoxProps) {
const inputRef = useRef<HTMLInputElement>(null);
const pendingChanges = useRef(false);
// const id = useStore((store) => store.session.id);
const sessionType = useEditorStore((store) => store.getSession(id)?.type);
const sessionType = useEditorStore((store) => store.getActiveSession()?.type);
const isMobile = useMobile();
const isTablet = useTablet();
const { editorConfig } = useEditorConfig();
@@ -71,15 +64,13 @@ function TitleBox(props: TitleBoxProps) {
[isMobile, isTablet]
);
useLayoutEffect(() => {
useEffect(() => {
const session = useEditorStore.getState().getSession(id);
if (!session || !("note" in session) || !session.note || !inputRef.current)
return;
if (pendingChanges.current) return;
const { title } = session.note;
if (inputRef.current.value === title) return;
withSelectionPersist(
inputRef.current,
(input) => (input.value = title || "")
@@ -95,24 +86,23 @@ function TitleBox(props: TitleBoxProps) {
useEffect(() => {
const { unsubscribe } = AppEventManager.subscribe(
AppEvents.changeNoteTitle,
({
preventSave,
title,
sessionId
}: {
title: string;
preventSave: boolean;
sessionId: string;
}) => {
if (!inputRef.current || sessionId !== id) return;
({ preventSave, title }: { title: string; preventSave: boolean }) => {
if (!inputRef.current) return;
withSelectionPersist(
inputRef.current,
(input) => (input.value = title)
);
updateFontSize(title.length);
if (!preventSave) {
const { activeSessionId } = useEditorStore.getState();
if (!activeSessionId) return;
pendingChanges.current = true;
debouncedOnTitleChange(sessionId, sessionId, title, pendingChanges);
debouncedOnTitleChange(
activeSessionId,
activeSessionId,
title,
pendingChanges
);
}
}
);
@@ -120,7 +110,7 @@ function TitleBox(props: TitleBoxProps) {
return () => {
unsubscribe();
};
}, [updateFontSize, id]);
}, [updateFontSize]);
return (
<Input

View File

@@ -40,5 +40,4 @@ export interface IEditor {
sendAttachmentProgress: (hash: string, progress: number) => void;
startSearch: () => void;
getContent: () => string;
getSelection: () => { from: number; to: number };
}

View File

@@ -219,8 +219,7 @@ import {
mdiOpenInNew,
mdiTagOutline,
mdiChatQuestionOutline,
mdiNoteRemoveOutline,
mdiTabPlus
mdiNoteRemoveOutline
} from "@mdi/js";
import { useTheme } from "@emotion/react";
import { Theme } from "@notesnook/theme";
@@ -274,8 +273,7 @@ const MDIIconWrapper = memo(
(prev, next) =>
prev.rotate === next.rotate &&
prev.color === next.color &&
prev.title === next.title &&
prev.size === next.size
prev.title === next.title
);
export type IconProps = FlexProps & Omit<MDIIconWrapperProps, "path">;
@@ -562,4 +560,3 @@ export const ClearCache = createIcon(mdiBroom);
export const OpenInNew = createIcon(mdiOpenInNew);
export const Coupon = createIcon(mdiTagOutline);
export const Support = createIcon(mdiChatQuestionOutline);
export const NewTab = createIcon(mdiTabPlus);

View File

@@ -118,7 +118,7 @@ function Note(props: NoteProps) {
} = props;
const note = item;
const isOpened = useEditorStore((store) => store.isNoteOpen(item.id));
const isOpened = useEditorStore((store) => store.activeSessionId === item.id);
const primary: SchemeColors = color ? color.colorCode : "accent-selected";
return (
@@ -146,7 +146,7 @@ function Note(props: NoteProps) {
menuItems={menuItems}
onClick={() => useEditorStore.getState().openSession(note)}
onMiddleClick={() =>
useEditorStore.getState().openSession(note, { openInNewTab: true })
useEditorStore.getState().openSession(note, { newSession: true })
}
header={
<Flex
@@ -331,14 +331,6 @@ const menuItems: (
// const isSynced = db.notes.note(note.id)?.synced();
return [
{
type: "button",
key: "openinnewtab",
title: strings.openInNewTab(),
icon: OpenInNew.path,
onClick: () =>
useEditorStore.getState().openSession(note.id, { openInNewTab: true })
},
{
type: "button",
key: "pin",

View File

@@ -140,7 +140,7 @@ function Header(props: RouteContainerProps) {
{buttons?.back ? (
<Button
{...buttons.back}
data-test-id="route-go-back"
data-test-id="go-back"
sx={{ p: 0, flexShrink: 0 }}
>
<ArrowLeft size={20} />

View File

@@ -96,15 +96,14 @@ export const SplitPane = React.forwardRef<
const wrapSize = useRef(0);
const childrenLength = childrenToArray(children).length;
const autoSaveKey = autoSaveId ? `csp:${autoSaveId}` : undefined;
const lastCollapsedPaneSize = useRef(0);
const { sizeName, splitPos, splitAxis } = useMemo(
() =>
({
sizeName: direction === "vertical" ? "width" : "height",
splitPos: direction === "vertical" ? "left" : "top",
splitAxis: direction === "vertical" ? "x" : "y"
} as const),
({
sizeName: direction === "vertical" ? "width" : "height",
splitPos: direction === "vertical" ? "left" : "top",
splitAxis: direction === "vertical" ? "x" : "y"
} as const),
[direction]
);
@@ -257,14 +256,10 @@ export const SplitPane = React.forwardRef<
return {
collapse: (index: number) => {
paneSizes.current[index].collapsed = true;
lastCollapsedPaneSize.current = paneSizes.current[index].size;
setSizes(paneSizes.current, wrapSize.current);
},
expand: (index: number) => {
paneSizes.current[index].collapsed = false;
paneSizes.current[index].size = lastCollapsedPaneSize.current
? lastCollapsedPaneSize.current
: paneSizes.current[index].initialSize;
setSizes(paneSizes.current, wrapSize.current);
}
};

View File

@@ -34,7 +34,7 @@ import { strings } from "@notesnook/intl";
type TrashItemProps = { item: TrashItemType; date: number };
function TrashItem(props: TrashItemProps) {
const { item, date } = props;
const isOpened = useEditorStore((store) => store.isNoteOpen(item.id));
const isOpened = useEditorStore((store) => store.activeSessionId === item.id);
return (
<ListItem

View File

@@ -39,14 +39,11 @@ import {
isTrashItem,
NoteContent
} from "@notesnook/core";
import { Context } from "../components/list-container/types";
import { showToast } from "../utils/toast";
import { getId } from "@notesnook/core";
import { PersistStorage } from "zustand/middleware";
import {
getFormattedHistorySessionDate,
TabHistory,
TabSessionHistory
} from "@notesnook/common";
import { getFormattedHistorySessionDate } from "@notesnook/common";
import { isCipher } from "@notesnook/core";
import { hashNavigate } from "../navigation";
import { AppEventManager, AppEvents } from "../common/app-events";
@@ -67,17 +64,15 @@ enum SESSION_STATES {
conflicted
}
type TabItem = {
id: string;
sessionId: string;
pinned?: boolean;
};
// type ConflictedContentItem = Omit<ContentItem, "conflicted"> & {
// conflicted: ContentItem;
// };
export type BaseEditorSession = {
tabId: string;
id: string;
needsHydration?: boolean;
pinned?: boolean;
preview?: boolean;
title?: string;
/**
@@ -120,29 +115,22 @@ export type DefaultEditorSession = BaseEditorSession & {
export type NewEditorSession = BaseEditorSession & {
type: "new";
context?: Context;
saveState: SaveState;
content?: NoteContent<false>;
};
export type ConflictedEditorSession = BaseEditorSession & {
type: "conflicted";
type: "conflicted" | "diff";
note: Note;
content?: ContentItem;
};
export type DiffEditorSession = BaseEditorSession & {
type: "diff";
note: Note;
content: ContentItem;
historySessionId: string;
};
export type EditorSession =
| DefaultEditorSession
| LockedEditorSession
| NewEditorSession
| ConflictedEditorSession
| DiffEditorSession
| ReadonlyEditorSession
| DeletedEditorSession;
@@ -152,7 +140,7 @@ type SessionTypeMap = {
locked: LockedEditorSession;
new: NewEditorSession;
conflicted: ConflictedEditorSession;
diff: DiffEditorSession;
diff: ConflictedEditorSession;
readonly: ReadonlyEditorSession;
deleted: DeletedEditorSession;
};
@@ -172,33 +160,10 @@ export function isLockedSession(session: EditorSession): boolean {
session.content.locked)
);
}
const tabSessionHistory = new TabSessionHistory({
get() {
return {
tabSessionHistory: useEditorStore.getState().tabHistory,
canGoBack: useEditorStore.getState().canGoBack,
canGoForward: useEditorStore.getState().canGoForward
};
},
set(state) {
useEditorStore.setState({
tabHistory: state.tabSessionHistory,
canGoBack: state.canGoBack,
canGoForward: state.canGoForward
});
}
});
const saveMutex = new Mutex();
class EditorStore extends BaseStore<EditorStore> {
tabs: TabItem[] = [];
tabHistory: TabHistory = {};
activeTabId: string | undefined;
canGoBack = false;
canGoForward = false;
sessions: EditorSession[] = [];
activeSessionId?: string;
arePropertiesVisible = false;
documentPreview?: DocumentPreview;
@@ -213,25 +178,18 @@ class EditorStore extends BaseStore<EditorStore> {
);
};
getSessionsForNote = (noteId: string) => {
return this.get().sessions.filter(
(s) => "note" in s && s.note.id === noteId
);
};
getTabsForNote = (noteId: string) => {
const { tabs, sessions } = this.get();
return tabs.filter((t) =>
sessions.some(
(s) => t.sessionId === s.id && "note" in s && s.note.id === noteId
)
getActiveSession = <T extends SessionType[]>(types?: T) => {
const { activeSessionId, sessions } = this.get();
return sessions.find(
(s): s is SessionTypeMap[T[number]] =>
s.id === activeSessionId && (!types || types.includes(s.type))
);
};
init = () => {
EV.subscribe(EVENTS.userLoggedOut, () => {
const { closeTabs, tabs } = this.get();
closeTabs(...tabs.map((s) => s.id));
const { closeSessions, sessions } = this.get();
closeSessions(...sessions.map((s) => s.id));
});
EV.subscribe(EVENTS.vaultLocked, () => {
@@ -241,7 +199,8 @@ class EditorStore extends BaseStore<EditorStore> {
if (
session.type === "diff" ||
session.type === "deleted" ||
session.type === "new"
// TODO: what's this?
!("note" in session)
)
return session;
@@ -249,7 +208,8 @@ class EditorStore extends BaseStore<EditorStore> {
type: "locked",
id: session.id,
note: session.note,
tabId: session.tabId
pinned: session.pinned,
preview: session.preview
};
}
return session;
@@ -261,7 +221,8 @@ class EditorStore extends BaseStore<EditorStore> {
EVENTS.syncItemMerged,
(item?: MaybeDeletedItem<Item>) => {
if (!item) return;
const { sessions, closeTabs, updateSession, openSession } = this.get();
const { sessions, closeSessions, updateSession, openSession } =
this.get();
const clearIds: string[] = [];
for (const session of sessions) {
if (session.type === "new") continue;
@@ -272,9 +233,9 @@ class EditorStore extends BaseStore<EditorStore> {
: item.type === "tiptap"
? item.noteId
: null;
if (noteId && session.note.id !== noteId) continue;
if (isDeleted(item) || isTrashItem(item))
clearIds.push(session.tabId);
if (noteId && session.id !== noteId && session.note.id !== noteId)
continue;
if (isDeleted(item) || isTrashItem(item)) clearIds.push(session.id);
// if a note becomes conflicted, reopen the session
else if (
session.type !== "conflicted" &&
@@ -326,31 +287,33 @@ class EditorStore extends BaseStore<EditorStore> {
);
}
}
if (clearIds.length > 0) closeTabs(...clearIds);
if (clearIds.length > 0) closeSessions(...clearIds);
}
);
db.eventManager.subscribe(
EVENTS.databaseUpdated,
async (event: DatabaseUpdatedEvent) => {
const { sessions, openSession, closeTabs, updateSession } = this.get();
const { sessions, openSession, closeSessions, updateSession } =
this.get();
const clearIds: string[] = [];
if (event.collection === "notes") {
// when a note is permanently deleted from trash
if (event.type === "softDelete" || event.type === "delete") {
clearIds.push(
...sessions
.filter(
(session) =>
"note" in session && event.ids.includes(session.note.id)
)
.map((s) => s.tabId)
...event.ids.filter(
(id) =>
sessions.findIndex(
(s) => s.id === id || ("note" in s && s.note.id === id)
) > -1
)
);
} else if (event.type === "update") {
for (const session of sessions) {
if (
session.type === "new" ||
!event.ids.includes(session.note.id)
(!event.ids.includes(session.id) &&
!event.ids.includes(session.note.id))
)
continue;
@@ -361,13 +324,13 @@ class EditorStore extends BaseStore<EditorStore> {
// when a note is restored from trash
(session.type === "deleted" && event.item.type !== "trash")
) {
openSession(session.note.id, { force: true, silent: true });
openSession(session.id, { force: true, silent: true });
} else if (
// when a note is moved to trash
session.type !== "deleted" &&
event.item.type === "trash"
) {
clearIds.push(session.tabId);
clearIds.push(session.id);
} else {
updateSession(session.id, [session.type], (session) => {
session.note.pinned =
@@ -402,7 +365,7 @@ class EditorStore extends BaseStore<EditorStore> {
(session.type === "default" && session.locked)) &&
!event.item.locked)
) {
openSession(session.note.id, { force: true, silent: true });
openSession(session.id, { force: true, silent: true });
}
}
}
@@ -431,6 +394,7 @@ class EditorStore extends BaseStore<EditorStore> {
: event.reference.ids;
if (!("color" in session) || !session.color) continue;
if (
!ids.includes(session.id) &&
!ids.includes(session.note.id) &&
!ids.includes(session.color)
)
@@ -451,6 +415,7 @@ class EditorStore extends BaseStore<EditorStore> {
: event.reference.ids;
if (!("tags" in session) || !session.tags) continue;
if (
!ids.includes(session.id) &&
!ids.includes(session.note.id) &&
session.tags.every((t) => !ids.includes(t.id))
)
@@ -479,43 +444,36 @@ class EditorStore extends BaseStore<EditorStore> {
session.tags?.every((t) => !event.ids.includes(t.id))
)
continue;
console.log("UDPATE");
updateSession(session.id, undefined, {
tags: await db.notes.tags(session.note.id)
});
}
}
}
if (clearIds.length > 0) closeTabs(...clearIds);
if (clearIds.length > 0) closeSessions(...clearIds);
}
);
const { rehydrateSession, activeTabId, newSession } = this.get();
if (activeTabId) {
const tab = this.get().tabs.find((t) => t.id === activeTabId);
if (!tab) return;
rehydrateSession(tab.sessionId);
const {
openSession,
openDiffSession,
activateSession,
activeSessionId,
getSession,
newSession
} = this.get();
if (activeSessionId) {
const session = getSession(activeSessionId);
if (!session) return;
if (session.type === "diff" || session.type === "conflicted")
openDiffSession(session.note.id, session.id);
else if (session.type === "new") activateSession(session.id);
else openSession(activeSessionId);
} else newSession();
};
private rehydrateSession = (sessionId: string) => {
const { openSession, openDiffSession, getSession, activateSession } =
this.get();
const session = getSession(sessionId);
if (session?.type === "new") {
activateSession(session.id);
return;
}
if (!session || !session.needsHydration) return;
if (session.type === "diff")
openDiffSession(session.note.id, session.historySessionId);
else
openSession(session.note.id, {
force: true
});
};
updateSession = <T extends SessionType[] = SessionType[]>(
id: string,
types: T | undefined,
@@ -538,13 +496,14 @@ class EditorStore extends BaseStore<EditorStore> {
});
};
activateSession = (id?: string, activeBlockId?: string, silent?: boolean) => {
activateSession = (id?: string, activeBlockId?: string) => {
if (!id) hashNavigate(`/`, { replace: true, notify: false });
const session = this.get().sessions.find((s) => s.id === id);
if (!session) id = undefined;
const activeSession = this.getActiveSession();
if (activeSession) {
this.saveSessionContentIfNotSaved(activeSession.id);
}
@@ -558,9 +517,13 @@ class EditorStore extends BaseStore<EditorStore> {
setDocumentTitle(session.note.title);
} else setDocumentTitle();
this.set({ activeSessionId: id });
AppEventManager.publish(AppEvents.toggleEditor, true);
if (id) {
const { history } = this.get();
if (history.includes(id)) history.splice(history.indexOf(id), 1);
history.push(id);
if (session?.type === "new")
hashNavigate(`/notes/${id}/create`, { replace: true, notify: false });
else hashNavigate(`/notes/${id}/edit`, { replace: true, notify: false });
@@ -570,19 +533,6 @@ class EditorStore extends BaseStore<EditorStore> {
this.updateSession(session.id, [session.type], {
activeBlockId
});
if (session?.tabId) {
const { tabs, activeTabId } = this.get();
const index = tabs.findIndex((t) => t.id === session.tabId);
// no need to focus tab if the same session is already open
if (
index === -1 ||
(activeTabId === session.tabId && tabs[index].sessionId === session.id)
)
return;
this.set((state) => (state.tabs[index].sessionId = session.id));
if (!silent) this.focusTab(session.tabId, session.id);
}
};
openDiffSession = async (noteId: string, sessionId: string) => {
@@ -595,30 +545,12 @@ class EditorStore extends BaseStore<EditorStore> {
if (!oldContent || !currentContent) return;
const { getSession, addSession, sessions, activeTabId, tabs } = this.get();
const tabId = activeTabId ?? this.addTab();
const tab = tabs.find((t) => t.id === tabId);
const activeSession = tab && getSession(tab.sessionId);
const oldSession = sessions.find(
(s) =>
s.type === "diff" &&
s.historySessionId === session.id &&
s.tabId === tabId
);
const tabSessionId =
activeSession?.needsHydration || activeSession?.type === "new"
? activeSession.id
: tabSessionHistory.add(tabId, oldSession?.id);
const label = getFormattedHistorySessionDate(session);
addSession({
this.get().addSession({
type: "diff",
id: tabSessionId,
id: session.id,
note,
tabId,
title: label,
historySessionId: session.id,
content: {
type: oldContent.type,
dateCreated: session.dateCreated,
@@ -641,59 +573,31 @@ class EditorStore extends BaseStore<EditorStore> {
force?: boolean;
activeBlockId?: string;
silent?: boolean;
openInNewTab?: boolean;
newSession?: boolean;
} = {}
): Promise<void> => {
const {
getSession,
sessions,
tabs,
activateSession,
activeTabId,
rehydrateSession,
getTabsForNote,
addTab
} = this.get();
const { getSession, openDiffSession } = this.get();
const noteId = typeof noteOrId === "string" ? noteOrId : noteOrId.id;
const oldTabForNote = options.force ? null : getTabsForNote(noteId).at(0);
const tabId = options.openInNewTab
? addTab(getId())
: oldTabForNote?.id || activeTabId || addTab(getId());
const session = getSession(noteId);
const tab = tabs.find((t) => t.id === tabId);
const activeSession = tab && getSession(tab.sessionId);
const noteAlreadyOpened =
activeSession &&
"note" in activeSession &&
activeSession.note.id === noteId &&
// we should allow opening the same note again if a diff session of a note
// is opened in the same tab. This allows for cases where a user opens a diff
// and then wants to open the note in the same tab again.
activeSession.type !== "diff";
if (noteAlreadyOpened && !options.force) {
return activeSession.needsHydration
? rehydrateSession(activeSession.id)
: activateSession(activeSession.id, options.activeBlockId);
if (session && !options.force) {
if (!session.needsHydration) {
return this.activateSession(noteId, options.activeBlockId);
}
if (session.type === "diff" || session.type === "conflicted") {
return openDiffSession(session.note.id, session.id);
}
}
if (activeSession && "note" in activeSession)
await db.fs().cancel(activeSession.note.id);
if (session && session.id) await db.fs().cancel(session.id);
const note =
typeof noteOrId === "object" && !activeSession?.needsHydration
typeof noteOrId === "object"
? noteOrId
: (await db.notes.note(noteId)) || (await db.notes.trashed(noteId));
if (!note) return;
const oldSessionOfNote = sessions.find(
(s) => "note" in s && s.note.id === noteId && s.tabId === tabId
);
const sessionId =
activeSession?.needsHydration ||
activeSession?.type === "new" ||
noteAlreadyOpened
? activeSession.id
: tabSessionHistory.add(tabId, oldSessionOfNote?.id);
const isPreview = session ? session.preview : !options?.newSession;
const isLocked = await db.vaults.itemExists(note);
if (note.conflicted) {
@@ -722,23 +626,25 @@ class EditorStore extends BaseStore<EditorStore> {
{
type: "conflicted",
content: content,
id: sessionId,
id: note.id,
pinned: session?.pinned,
note,
activeBlockId: options.activeBlockId,
tabId
preview: isPreview,
activeBlockId: options.activeBlockId
},
options.silent
!options.silent
);
} else if (isLocked && note.type !== "trash") {
this.addSession(
{
type: "locked",
id: sessionId,
id: note.id,
pinned: session?.pinned,
note,
activeBlockId: options.activeBlockId,
tabId
preview: isPreview,
activeBlockId: options.activeBlockId
},
options.silent
!options.silent
);
} else {
const content = note.contentId
@@ -755,12 +661,12 @@ class EditorStore extends BaseStore<EditorStore> {
{
type: "deleted",
note,
id: sessionId,
id: note.id,
pinned: session?.pinned,
content,
activeBlockId: options.activeBlockId,
tabId
activeBlockId: options.activeBlockId
},
options.silent
!options.silent
);
} else {
const attachmentsLength = await db.attachments
@@ -773,113 +679,94 @@ class EditorStore extends BaseStore<EditorStore> {
{
type: "readonly",
note,
id: sessionId,
id: note.id,
pinned: session?.pinned,
content,
color: colors[0]?.fromId,
tags,
activeBlockId: options.activeBlockId,
tabId
activeBlockId: options.activeBlockId
},
options.silent
!options.silent
);
} else {
this.addSession(
{
type: "default",
id: sessionId,
id: note.id,
note,
saveState: SaveState.Saved,
sessionId: `${Date.now()}`,
attachmentsLength,
pinned: session?.pinned,
tags,
color: colors[0]?.fromId,
content,
activeBlockId: options.activeBlockId,
tabId
preview: isPreview,
activeBlockId: options.activeBlockId
},
options.silent
!options.silent
);
}
}
}
};
focusNextTab = () => {
const { tabs, activeTabId } = this.get();
if (tabs.length <= 1) return;
openNextSession = () => {
const { sessions, activeSessionId } = this.get();
if (sessions.length === 0 || sessions.length === 1) return;
const index = tabs.findIndex((s) => s.id === activeTabId);
const index = sessions.findIndex((s) => s.id === activeSessionId);
if (index === -1) return;
return this.focusTab(tabs[index === tabs.length - 1 ? 0 : index + 1].id);
if (index === sessions.length - 1) {
return this.openSession(sessions[0].id);
}
return this.openSession(sessions[index + 1].id);
};
focusPreviousTab = () => {
const { tabs, activeTabId } = this.get();
if (tabs.length <= 1) return;
openPreviousSession = () => {
const { sessions, activeSessionId } = this.get();
if (sessions.length === 0 || sessions.length === 1) return;
const index = tabs.findIndex((s) => s.id === activeTabId);
const index = sessions.findIndex((s) => s.id === activeSessionId);
if (index === -1) return;
return this.focusTab(tabs[index === 0 ? tabs.length - 1 : index - 1].id);
if (index === 0) {
return this.openSession(sessions[sessions.length - 1].id);
}
return this.openSession(sessions[index - 1].id);
};
goBack = async () => {
const activeTabId = this.get().activeTabId;
if (!activeTabId || !tabSessionHistory.canGoBack(activeTabId)) return;
const sessionId = tabSessionHistory.back(activeTabId);
if (!sessionId) return;
if (!(await this.goToSession(activeTabId, sessionId))) {
await this.goBack();
}
};
addSession = (session: EditorSession, activate = true) => {
let oldSessionId: string | null = null;
goForward = async () => {
const activeTabId = this.get().activeTabId;
if (!activeTabId || !tabSessionHistory.canGoForward(activeTabId)) return;
const sessionId = tabSessionHistory.forward(activeTabId);
if (!sessionId) return;
if (!(await this.goToSession(activeTabId, sessionId))) {
await this.goForward();
}
};
goToSession = async (tabId: string, sessionId: string) => {
const session = this.get().getSession(sessionId);
if (!session) {
tabSessionHistory.remove(tabId, sessionId);
return false;
}
if (session.type === "new") {
this.activateSession(session.id);
return true;
} else {
if (!(await db.notes.exists(session.note.id))) {
tabSessionHistory.remove(tabId, session.id);
this.set((state) => {
const index = state.sessions.findIndex((s) => s.id === session.id);
state.sessions.splice(index, 1);
});
return false;
}
// we must rehydrate the session as the note's content can be stale
this.updateSession(session.id, undefined, {
needsHydration: true
});
this.activateSession(session.id);
return true;
}
};
addSession = (session: EditorSession, silent = false) => {
this.set((state) => {
const index = state.sessions.findIndex((s) => s.id === session.id);
if (index > -1) {
state.sessions[index] = session;
} else state.sessions.push(session);
const { activeSessionIndex, duplicateSessionIndex, previewSessionIndex } =
findSessionIndices(state.sessions, session, state.activeSessionId);
if (duplicateSessionIndex > -1) {
oldSessionId = state.sessions[duplicateSessionIndex].id;
state.sessions[duplicateSessionIndex] = session;
} else if (previewSessionIndex > -1) {
oldSessionId = state.sessions[previewSessionIndex].id;
state.sessions[previewSessionIndex] = session;
} else if (activeSessionIndex > -1)
state.sessions.splice(activeSessionIndex + 1, 0, session);
else state.sessions.push(session);
state.sessions.sort((a, b) =>
a.pinned === b.pinned ? 0 : a.pinned ? -1 : 1
);
});
this.activateSession(session.id, undefined, silent);
const { history } = this.get();
if (
oldSessionId &&
oldSessionId !== session.id &&
history.includes(oldSessionId)
)
history.splice(history.indexOf(oldSessionId), 1);
if (activate) this.activateSession(session.id);
};
saveSession = async (
@@ -899,10 +786,6 @@ class EditorStore extends BaseStore<EditorStore> {
this.setSaveState(id, 0);
try {
const sessionId = getSessionId(currentSession);
const noteId =
("note" in currentSession
? currentSession.note.id
: partial.note?.id) || id;
if (isLockedSession(currentSession) && partial.content) {
logger.debug("Saving locked content", { id });
@@ -910,7 +793,7 @@ class EditorStore extends BaseStore<EditorStore> {
await db.vault.save({
content: partial.content,
sessionId,
id: noteId
id
});
} else {
if (partial.content)
@@ -930,36 +813,36 @@ class EditorStore extends BaseStore<EditorStore> {
: undefined,
content: partial.content,
sessionId,
id: noteId
id
});
}
const note = await db.notes.note(noteId);
const note = await db.notes.note(id);
if (!note) throw new Error("Note not saved.");
if (currentSession.type === "new") {
const context = useNoteStore.getState().context;
if (context) {
const { type } = context;
if (currentSession.context) {
const { type } = currentSession.context;
if (type === "notebook")
await db.notes.addToNotebook(context.id, noteId);
await db.notes.addToNotebook(currentSession.context.id, id);
else if (type === "color" || type === "tag")
await db.relations.add(
{ type, id: context.id },
{ type, id: currentSession.context.id },
{ id, type: "note" }
);
} else {
const defaultNotebook = db.settings.getDefaultNotebook();
if (defaultNotebook)
await db.notes.addToNotebook(defaultNotebook, noteId);
await db.notes.addToNotebook(defaultNotebook, id);
}
}
const attachmentsLength = await db.attachments
.ofNote(note.id, "all")
.ofNote(id, "all")
.count();
const shouldRefreshNotes =
currentSession.type === "new" ||
!id ||
note.title !== currentSession.note?.title ||
note.headline !== currentSession.note?.headline ||
attachmentsLength !== currentSession.attachmentsLength;
@@ -986,6 +869,7 @@ class EditorStore extends BaseStore<EditorStore> {
}
this.updateSession(id, ["default"], {
preview: false,
attachmentsLength: attachmentsLength,
note,
sessionId
@@ -1005,8 +889,8 @@ class EditorStore extends BaseStore<EditorStore> {
this.setSaveState(id, SaveState.NotSaved);
console.error(err);
if (err instanceof Error) logger.error(err);
if (isLockedSession(currentSession) && "note" in currentSession) {
this.get().openSession(currentSession.note, { force: true });
if (isLockedSession(currentSession)) {
this.get().openSession(id, { force: true });
}
}
});
@@ -1032,55 +916,43 @@ class EditorStore extends BaseStore<EditorStore> {
};
newSession = () => {
const { activeTabId, activateSession } = this.get();
if (!activeTabId) {
this.addTab();
return;
const state = useEditorStore.getState();
const session = state.sessions.find((session) => session.type === "new");
if (session) {
session.context = useNoteStore.getState().context;
this.activateSession(session.id);
} else {
this.addSession({
type: "new",
id: getId(),
context: useNoteStore.getState().context,
saveState: SaveState.NotSaved
});
}
const session = this.getActiveSession();
if (session?.type === "new") return activateSession(session.id);
const sessionId = tabSessionHistory.add(activeTabId);
this.addSession({
type: "new",
id: sessionId,
tabId: activeTabId,
saveState: SaveState.NotSaved
});
};
closeTabs = (...ids: string[]) => {
closeSessions = (...ids: string[]) => {
this.set((state) => {
const tabs: TabItem[] = [];
for (let i = 0; i < state.tabs.length; ++i) {
const tab = state.tabs[i];
if (!ids.includes(tab.id)) {
tabs.push(tab);
const sessions: EditorSession[] = [];
for (let i = 0; i < state.sessions.length; ++i) {
const session = state.sessions[i];
if (!ids.includes(session.id)) {
sessions.push(session);
continue;
}
this.saveSessionContentIfNotSaved(tab.sessionId);
this.saveSessionContentIfNotSaved(session.id);
db.fs().cancel(tab.sessionId).catch(console.error);
if (state.history.includes(tab.id))
state.history.splice(state.history.indexOf(tab.id), 1);
const tabHistory = tabSessionHistory.getTabHistory(tab.id);
state.sessions = state.sessions.filter((session) => {
return (
!tabHistory.back.includes(session.id) &&
!tabHistory.forward.includes(session.id)
);
});
tabSessionHistory.clearStackForTab(tab.id);
db.fs().cancel(session.id).catch(console.error);
if (state.history.includes(session.id))
state.history.splice(state.history.indexOf(session.id), 1);
}
state.tabs = tabs;
state.sessions = sessions;
});
const { history, tabs } = this.get();
this.focusTab(history.pop());
if (tabs.length === 0) this.addTab();
const { history, sessions } = this.get();
this.activateSession(history.pop());
if (sessions.length === 0) this.newSession();
};
setTitle = (id: string, title: string) => {
@@ -1132,90 +1004,23 @@ class EditorStore extends BaseStore<EditorStore> {
this.set({ editorMargins: editorMarginsState });
Config.set("editor:margins", editorMarginsState);
};
getActiveTab = () => {
const activeTabId = this.get().activeTabId;
return this.get().tabs.find((t) => t.id === activeTabId);
};
getActiveNote = () => {
const session = this.getActiveSession();
return session && "note" in session ? session.note : undefined;
};
isNoteOpen = (noteId: string) => {
return this.getActiveNote()?.id === noteId;
};
getActiveSession = <T extends SessionType[]>(
types?: T
): SessionTypeMap[T[number]] | undefined => {
const activeTab = this.getActiveTab();
if (!activeTab) return;
const session = this.getSession(activeTab.sessionId);
if (session && (!types || types.includes(session.type)))
return session as SessionTypeMap[T[number]];
};
addTab = (sessionId?: string) => {
const id = getId();
const newSessionId = sessionId || tabSessionHistory.add(id);
this.set((state) => {
state.tabs.push({
id,
sessionId: newSessionId
});
});
if (!sessionId)
this.addSession({
type: "new",
tabId: id,
id: newSessionId,
saveState: SaveState.NotSaved
});
this.focusTab(id);
return id;
};
focusTab = (tabId: string | undefined, sessionId?: string) => {
if (!tabId) return;
const { history } = this.get();
if (history.includes(tabId)) history.splice(history.indexOf(tabId), 1);
history.push(tabId);
this.set({
activeTabId: tabId,
canGoBack: tabSessionHistory.canGoBack(tabId),
canGoForward: tabSessionHistory.canGoForward(tabId)
});
sessionId =
sessionId || this.get().tabs.find((t) => t.id === tabId)?.sessionId;
if (sessionId) this.rehydrateSession(sessionId);
};
}
const useEditorStore = createPersistedStore(EditorStore, {
name: "editor-sessions-v2",
name: "editor-sessions",
partialize: (state) => ({
history: state.history,
activeSessionId: state.activeSessionId,
arePropertiesVisible: state.arePropertiesVisible,
editorMargins: state.editorMargins,
tabs: state.tabs,
activeTabId: state.activeTabId,
tabHistory: state.tabHistory,
canGoBack: state.canGoBack,
canGoForward: state.canGoForward,
sessions: state.sessions.reduce((sessions, session) => {
sessions.push({
id: session.id,
type: isLockedSession(session) ? "locked" : session.type,
needsHydration: session.type === "new" ? false : true,
preview: session.preview,
pinned: session.pinned,
title: session.title,
historySessionId:
session.type === "diff" ? session.historySessionId : undefined,
tabId: session.tabId,
note:
"note" in session
? {
@@ -1232,6 +1037,28 @@ const useEditorStore = createPersistedStore(EditorStore, {
});
export { useEditorStore, SESSION_STATES };
function findSessionIndices(
sessions: EditorSession[],
session: EditorSession,
activeSessionId?: string
) {
let activeSessionIndex = -1;
let previewSessionIndex = -1;
let duplicateSessionIndex = -1;
for (let i = 0; i < sessions.length; ++i) {
const { id, preview } = sessions[i];
if (id === session.id) duplicateSessionIndex = i;
else if (preview && session.preview) previewSessionIndex = i;
else if (id === activeSessionId) activeSessionIndex = i;
}
return {
activeSessionIndex,
previewSessionIndex,
duplicateSessionIndex
};
}
const MILLISECONDS_IN_A_MINUTE = 60 * 1000;
const SESSION_DURATION = MILLISECONDS_IN_A_MINUTE * 5;
function getSessionId(session: DefaultEditorSession | NewEditorSession) {

View File

@@ -72,9 +72,7 @@ export async function* exportNotes(
const pathTree = new PathTree();
const notePathMap: Map<string, string[]> = new Map();
for await (const note of notes
.fields(["notes.id", "notes.title"])
.iterate()) {
for await (const note of notes.fields(["notes.id", "notes.title"])) {
const filename = `${sanitizeFilename(note.title || "Untitled", {
replacement: "-"
})}.${FORMAT_TO_EXT[format]}`;

View File

@@ -27,4 +27,3 @@ export * from "./resolve-items.js";
export * from "./migrate-toolbar.js";
export * from "./export-notes.js";
export * from "./dataurl.js";
export * from "./tab-session-history.js";

View File

@@ -1,175 +0,0 @@
/*
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 { getId } from "@notesnook/core";
export type TabHistory = Record<
string,
{ backStack: string[]; forwardStack: string[] }
>;
export type TabState = {
tabSessionHistory: TabHistory;
canGoBack?: boolean;
canGoForward?: boolean;
};
export class TabSessionHistory {
constructor(
public options: {
set: (state: TabState) => void;
get: () => TabState;
}
) {}
getBackStack(id: string) {
const tabHistory = this.options.get().tabSessionHistory[id];
if (!tabHistory) return [];
return tabHistory.backStack.slice();
}
getForwardStack(id: string) {
const tabHistory = this.options.get().tabSessionHistory[id];
if (!tabHistory) return [];
return tabHistory.forwardStack.slice();
}
setBackStack(id: string, value: string[]) {
const tabHistory = this.options.get().tabSessionHistory;
this.options.set({
canGoBack: value.length > 1,
tabSessionHistory: {
...tabHistory,
[id]: {
...(tabHistory[id] || {}),
backStack: value
}
}
});
}
setForwardStack(id: string, value: string[]) {
const tabHistory = this.options.get().tabSessionHistory;
this.options.set({
canGoForward: value.length > 0,
tabSessionHistory: {
...tabHistory,
[id]: {
...(tabHistory[id] || {}),
forwardStack: value
}
}
});
}
add(id: string, sessionId?: string) {
sessionId = sessionId || getId();
const back_stack = this.getBackStack(id);
back_stack.push(sessionId);
this.setBackStack(id, back_stack);
this.setForwardStack(id, []);
return sessionId;
}
clearStackForTab(tabId: string) {
this.options.set({
tabSessionHistory: {
...this.options.get().tabSessionHistory,
[tabId]: {
backStack: [],
forwardStack: []
}
}
});
}
back(id: string): string | null {
if (!this.canGoBack(id)) return null;
const backStack = this.getBackStack(id);
const forwardStack = this.getForwardStack(id);
const currentItem = backStack.pop();
const nextItem = backStack[backStack.length - 1];
currentItem && forwardStack.push(currentItem);
this.setForwardStack(id, forwardStack);
this.setBackStack(id, backStack);
return nextItem;
}
remove(tabId: string, sessionId: string) {
const backStack = this.getBackStack(tabId);
let index = backStack.findIndex((item) => item === sessionId);
if (index === -1) {
const forwardStack = this.getForwardStack(tabId);
index = forwardStack.findIndex((item) => item === sessionId);
forwardStack.splice(index, 1);
this.setForwardStack(tabId, forwardStack);
} else {
backStack.splice(index, 1);
this.setBackStack(tabId, backStack);
}
}
forward(id: string): string | null {
if (!this.canGoForward(id)) return null;
const backStack = this.getBackStack(id);
const forwardStack = this.getForwardStack(id);
const item = forwardStack.pop() as string;
this.setForwardStack(id, forwardStack);
backStack.push(item);
this.setBackStack(id, backStack);
return item;
}
currentSessionId(id: string) {
const { back } = this.getTabHistory(id);
return back[back.length - 1];
}
getTabHistory(id: string) {
const tabHistory = this.options.get().tabSessionHistory[id];
if (!tabHistory)
return {
back: [],
forward: []
};
return {
back: tabHistory.backStack?.slice() || [],
forward: tabHistory.forwardStack?.slice() || []
};
}
canGoBack(id: string) {
const tabHistory = this.options.get().tabSessionHistory[id];
if (!tabHistory) return false;
return tabHistory.backStack.length > 1;
}
canGoForward(id: string) {
const tabHistory = this.options.get().tabSessionHistory[id];
if (!tabHistory) return false;
return tabHistory.forwardStack.length >= 1;
}
}

View File

@@ -184,7 +184,7 @@ export default class Lookup {
) {
const results: Map<string, number> = new Map();
const columns = fields.map((f) => f.column);
for await (const item of selector.fields(columns).iterate()) {
for await (const item of selector.fields(columns)) {
if (limit && results.size >= limit) break;
for (const field of fields) {

View File

@@ -422,7 +422,7 @@ export class Attachments implements ICollection {
async cleanup() {
const now = dayjs().unix();
const ids: string[] = [];
for await (const attachment of this.deleted.iterate()) {
for await (const attachment of this.deleted) {
if (dayjs(attachment.dateDeleted).add(7, "days").unix() < now) continue;
const isDeleted = await this.db.fs().deleteFile(attachment.hash);

View File

@@ -285,32 +285,31 @@ export class Notes implements ICollection {
const { format, rawContent } = options;
const contentString =
rawContent === undefined
? await (async () => {
let contentItem = options.contentItem;
if (!contentItem) {
const rawContent = await this.db.content.findByNoteId(note.id);
if (rawContent && rawContent.locked) return false;
contentItem = rawContent || EMPTY_CONTENT(note.id);
}
rawContent ||
(await (async () => {
let contentItem = options.contentItem;
if (!contentItem) {
const rawContent = await this.db.content.findByNoteId(note.id);
if (rawContent && rawContent.locked) return false;
contentItem = rawContent || EMPTY_CONTENT(note.id);
}
const { data, type } =
options?.embedMedia && format !== "txt"
? await this.db.content.downloadMedia(
`export-${note.id}`,
contentItem,
false
)
: contentItem;
const content = await getContentFromData(type, data);
return format === "html"
? content.toHTML()
: format === "md"
? content.toMD()
: content.toTXT();
})()
: rawContent;
if (contentString === false) return false;
const { data, type } =
options?.embedMedia && format !== "txt"
? await this.db.content.downloadMedia(
`export-${note.id}`,
contentItem,
false
)
: contentItem;
const content = await getContentFromData(type, data);
return format === "html"
? content.toHTML()
: format === "md"
? content.toMD()
: content.toTXT();
})());
if (!contentString) return false;
const tags = (await this.db.relations.to(note, "tag").resolve()).map(
(tag) => tag.title

View File

@@ -361,7 +361,7 @@ export default class Backup {
};
let current = 0;
for await (const attachment of this.db.attachments.all.iterate()) {
for await (const attachment of this.db.attachments.all) {
current++;
if (
!(await this.db

View File

@@ -476,7 +476,7 @@ export class FilteredSelector<T extends Item> {
async *map<TReturnType>(
fn: (item: T) => TReturnType
): AsyncIterableIterator<TReturnType> {
for await (const item of this.iterate()) {
for await (const item of this) {
yield fn(item);
}
}
@@ -567,43 +567,37 @@ export class FilteredSelector<T extends Item> {
);
}
iterate() {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const thisArg = this;
return {
async *[Symbol.asyncIterator]() {
let lastRow: any | null = null;
const fields = thisArg._fields.slice();
if (fields.length > 0) {
if (!fields.find((f) => f.includes(".dateCreated")))
fields.push("dateCreated");
if (!fields.find((f) => f.includes(".id"))) fields.push("id");
}
async *[Symbol.asyncIterator]() {
let lastRow: any | null = null;
const fields = this._fields.slice();
if (fields.length > 0) {
if (!fields.find((f) => f.includes(".dateCreated")))
fields.push("dateCreated");
if (!fields.find((f) => f.includes(".id"))) fields.push("id");
}
while (true) {
const rows = await thisArg.filter
.orderBy("dateCreated asc")
.orderBy("id asc")
.$if(lastRow !== null, (qb) =>
qb.where(
(eb) => eb.refTuple("dateCreated", "id"),
">",
(eb) => eb.tuple(lastRow.dateCreated, lastRow.id)
)
)
.limit(thisArg.batchSize)
.$if(fields.length === 0, (eb) => eb.selectAll())
.$if(fields.length > 0, (eb) => eb.select(fields))
.execute();
if (rows.length === 0) break;
for (const row of rows) {
yield row as T;
}
lastRow = rows[rows.length - 1];
}
while (true) {
const rows = await this.filter
.orderBy("dateCreated asc")
.orderBy("id asc")
.$if(lastRow !== null, (qb) =>
qb.where(
(eb) => eb.refTuple("dateCreated", "id"),
">",
(eb) => eb.tuple(lastRow.dateCreated, lastRow.id)
)
)
.limit(this.batchSize)
.$if(fields.length === 0, (eb) => eb.selectAll())
.$if(fields.length > 0, (eb) => eb.select(fields))
.execute();
if (rows.length === 0) break;
for (const row of rows) {
yield row as T;
}
};
lastRow = rows[rows.length - 1];
}
}
private buildSortExpression(options: GroupOptions, hasDueDate?: boolean) {

View File

@@ -14,7 +14,6 @@
"@lingui/react": "5.1.2",
"@mdi/js": "^7.2.96",
"@mdi/react": "^1.6.0",
"@notesnook/common": "file:../common",
"@notesnook/editor": "file:../editor",
"@notesnook/intl": "file:../intl",
"@notesnook/theme": "file:../theme",
@@ -25,7 +24,6 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-freeze": "^1.0.3",
"tinycolor2": "1.6.0",
"zustand": "^4.4.7"
},
"devDependencies": {
@@ -35,28 +33,6 @@
"react-scripts": "^5.0.1"
}
},
"../common": {
"name": "@notesnook/common",
"version": "2.1.3",
"license": "GPL-3.0-or-later",
"dependencies": {
"@notesnook/core": "file:../core",
"@readme/data-urls": "^3.0.0",
"dayjs": "1.11.13",
"pathe": "^1.1.2",
"timeago.js": "4.0.2"
},
"devDependencies": {
"@notesnook/core": "file:../core",
"@types/react": "18.3.5",
"react": "18.3.1",
"vitest": "2.1.8"
},
"peerDependencies": {
"react": ">=18",
"timeago.js": "4.0.2"
}
},
"../editor": {
"name": "@notesnook/editor",
"version": "2.1.3",
@@ -3790,10 +3766,6 @@
"node": ">= 8"
}
},
"node_modules/@notesnook/common": {
"resolved": "../common",
"link": true
},
"node_modules/@notesnook/editor": {
"resolved": "../editor",
"link": true
@@ -17639,11 +17611,6 @@
"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",

View File

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

View File

@@ -22,9 +22,9 @@ import {
getFontById,
getTableOfContents,
TiptapOptions,
toBlobURL,
usePermissionHandler
} from "@notesnook/editor";
import { toBlobURL } from "@notesnook/editor";
import { useThemeColors } from "@notesnook/theme";
import FingerprintIcon from "mdi-react/FingerprintIcon";
import {
@@ -36,11 +36,15 @@ import {
useState
} from "react";
import { useEditorController } from "../hooks/useEditorController";
import { useSafeArea } from "../hooks/useSafeArea";
import { useSettings } from "../hooks/useSettings";
import { TabItem, useTabContext, useTabStore } from "../hooks/useTabStore";
import { postAsyncWithTimeout, Settings } from "../utils";
import { EditorEvents } from "../utils/editor-events";
import {
NoteState,
TabItem,
TabStore,
useTabContext,
useTabStore
} from "../hooks/useTabStore";
import { EventTypes, postAsyncWithTimeout, Settings } from "../utils";
import { pendingSaveRequests } from "../utils/pending-saves";
import Header from "./header";
import StatusBar from "./statusbar";
@@ -78,43 +82,49 @@ const Tiptap = ({
undo,
redo
});
const insets = useSafeArea();
tabRef.current = tab;
valueRef.current = {
undo,
redo
};
logger("info", tabRef.current.id, "rendering");
function restoreNoteSelection(state?: NoteState) {
try {
if (!tabRef.current.noteId) return;
const noteState =
state || useTabStore.getState().noteState[tabRef.current.noteId];
const restoreNoteSelection = useCallback(
(scrollTop?: number, selection?: { to: number; from: number }) => {
if (!tabRef.current.session?.noteId) return;
const sel = selection || tabRef.current.session?.selection;
if (sel && sel.to && sel.from) {
if (noteState && (noteState.to || noteState.from)) {
const size = editors[tabRef.current.id]?.state.doc.content.size || 0;
if (sel.to > 0 && sel.to <= size && sel.from > 0 && sel.from <= size) {
if (
noteState.to > 0 &&
noteState.to <= size &&
noteState.from > 0 &&
noteState.from <= size
) {
editors[tabRef.current.id]?.chain().setTextSelection({
to: sel.to,
from: sel.from
to: noteState.to,
from: noteState.from
});
}
}
containerRef.current?.scrollTo({
left: 0,
top: scrollTop || tabRef.current.session?.scrollTop || 0,
top: noteState?.top || 0,
behavior: "auto"
});
},
[]
);
} catch (e) {
logger("error", (e as Error).message, (e as Error).stack);
}
}
usePermissionHandler({
claims: {
premium: settings.premium
},
onPermissionDenied: () => {
post(EditorEvents.pro, undefined, tabRef.current.id, tab.session?.noteId);
post(EventTypes.pro, undefined, tabRef.current.id, tab.noteId);
}
});
@@ -151,14 +161,14 @@ const Tiptap = ({
) as Promise<string | undefined>;
},
createInternalLink(attributes) {
return postAsyncWithTimeout(EditorEvents.createInternalLink, {
return postAsyncWithTimeout(EventTypes.createInternalLink, {
attributes
});
},
element: getContentDiv(),
editable: !tab.session?.readonly,
editable: !tab.readonly,
editorProps: {
editable: () => !tab.session?.readonly,
editable: () => !tab.readonly,
handlePaste: (view, event) => {
const hasFiles = event.clipboardData?.types?.some((type) =>
type.startsWith("Files")
@@ -201,12 +211,19 @@ const Tiptap = ({
copyToClipboard: (text) => {
globalThis.editorControllers[tab.id]?.copyToClipboard(text);
},
placeholder: strings.startWritingNote(),
onSelectionUpdate: () => {
if (tabRef.current.session?.noteId) {
if (tabRef.current.noteId) {
const noteId = tabRef.current.noteId;
clearTimeout(noteStateUpdateTimer.current);
noteStateUpdateTimer.current = setTimeout(() => {
if (tabRef.current.noteId !== noteId) return;
const { to, from } =
editors[tabRef.current?.id]?.state.selection || {};
useTabStore.getState().setNoteState(noteId, {
to,
from
});
}, 500);
}
},
@@ -225,7 +242,7 @@ const Tiptap = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
getContentDiv,
tab.session?.readonly,
tab.readonly,
settings.doubleSpacedLines,
settings.corsProxy,
settings.dateFormat,
@@ -235,21 +252,15 @@ const Tiptap = ({
tick
]);
const update = useCallback(
(scrollTop?: number, selection?: { to: number; from: number }) => {
setTick((tick) => tick + 1);
globalThis.editorControllers[tabRef.current.id]?.setTitlePlaceholder(
strings.noteTitle()
);
setTimeout(() => {
editorControllers[tabRef.current.id]?.setLoading(false);
setTimeout(() => {
restoreNoteSelection(scrollTop, selection);
}, 300);
}, 1);
},
[restoreNoteSelection]
);
const update = useCallback(() => {
setTick((tick) => tick + 1);
globalThis.editorControllers[tabRef.current.id]?.setTitlePlaceholder(
strings.noteTitle()
);
setTimeout(() => {
editorControllers[tabRef.current.id]?.setLoading(false);
}, 300);
}, []);
const controller = useEditorController({
update,
@@ -290,40 +301,60 @@ const Tiptap = ({
});
}
const updateFocusedTab = () => {
const updateScrollPosition = (state: TabStore) => {
if (isFocusedRef.current) return;
isFocusedRef.current = true;
const noteId = useTabStore
.getState()
.tabs.find((tab) => tab.id === useTabStore.getState().currentTab)
?.session?.noteId;
post(
EditorEvents.tabFocused,
undefined,
useTabStore.getState().currentTab,
noteId
);
editorControllers[tabRef.current.id]?.updateTab();
if (state.currentTab === tabRef.current.id) {
isFocusedRef.current = true;
const noteState = tabRef.current.noteId
? state.noteState[tabRef.current.noteId]
: undefined;
restoreNoteSelection();
post(
EventTypes.tabFocused,
!!globalThis.editorControllers[tabRef.current.id]?.content.current &&
!editorControllers[tabRef.current.id]?.loading,
tabRef.current.id,
state.getCurrentNoteId()
);
editorControllers[tabRef.current.id]?.updateTab();
if (
!globalThis.editorControllers[tabRef.current.id]?.content.current &&
tabRef.current.session?.noteId
) {
editorControllers[tabRef.current.id]?.setLoading(true);
if (noteState) {
if (
containerRef.current &&
containerRef.current?.scrollHeight < noteState.top
) {
console.log("Container too small to scroll.");
return;
}
restoreNoteSelection(noteState);
} else {
containerRef.current?.scrollTo({
left: 0,
top: 0,
behavior: "auto"
});
}
if (
!globalThis.editorControllers[tabRef.current.id]?.content.current &&
tabRef.current.noteId
) {
editorControllers[tabRef.current.id]?.setLoading(true);
}
} else {
isFocusedRef.current = false;
}
};
updateFocusedTab();
updateScrollPosition(useTabStore.getState());
const unsub = useTabStore.subscribe((state, prevState) => {
if (state.currentTab !== tabRef.current.id) {
isFocusedRef.current = false;
}
if (state.currentTab === prevState.currentTab && isFocusedRef.current)
return;
updateFocusedTab();
if (state.currentTab === prevState.currentTab) return;
updateScrollPosition(state);
logger("info", "updating scroll position");
});
logger("info", tabRef.current.id, "active");
@@ -332,7 +363,7 @@ const Tiptap = ({
logger("info", tabRef.current.id, "inactive");
unsub();
};
}, [getContentDiv, restoreNoteSelection]);
}, [getContentDiv]);
const onClickEmptyArea: React.MouseEventHandler<HTMLDivElement> = useCallback(
(event) => {
@@ -526,7 +557,7 @@ const Tiptap = ({
position: "relative"
}}
>
{settings.noHeader || tab.session?.locked ? null : (
{settings.noHeader || tab.locked ? null : (
<>
<Tags settings={settings} loading={controller.loading} />
<Title
@@ -547,7 +578,7 @@ const Tiptap = ({
</>
)}
{controller.loading || tab.session?.locked ? (
{controller.loading || tab.locked ? (
<div
style={{
width: "100%",
@@ -559,13 +590,13 @@ const Tiptap = ({
paddingLeft: 12,
display: "flex",
flexDirection: "column",
alignItems: tab.session?.locked ? "center" : "flex-start",
justifyContent: tab.session?.locked ? "center" : "flex-start",
alignItems: tab.locked ? "center" : "flex-start",
justifyContent: tab.locked ? "center" : "flex-start",
boxSizing: "border-box",
rowGap: 10
}}
>
{tab.session?.locked ? (
{tab.locked ? (
<>
<p
style={{
@@ -818,7 +849,7 @@ const Tiptap = ({
<div
style={{
display: tab.session?.locked ? "none" : "block"
display: tab.locked ? "none" : "block"
}}
ref={contentPlaceholderRef}
className="theme-scope-editor"
@@ -826,11 +857,11 @@ const Tiptap = ({
<div
onClick={(e) => {
if (tab.session?.locked) return;
if (tab.locked) return;
onClickBottomArea();
}}
onMouseDown={(e) => {
if (tab.session?.locked) return;
if (tab.locked) return;
if (globalThis.keyboardShown) {
e.preventDefault();
}

View File

@@ -19,22 +19,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { ControlledMenu, MenuItem as MenuItemInner } from "@szhsin/react-menu";
import ArrowBackIcon from "mdi-react/ArrowBackIcon";
import ArrowForwardIcon from "mdi-react/ArrowForwardIcon";
import ArrowULeftTopIcon from "mdi-react/ArrowULeftTopIcon";
import ArrowURightTopIcon from "mdi-react/ArrowURightTopIcon";
import DotsHorizontalIcon from "mdi-react/DotsHorizontalIcon";
import DotsVerticalIcon from "mdi-react/DotsVerticalIcon";
import FullscreenIcon from "mdi-react/FullscreenIcon";
import MagnifyIcon from "mdi-react/MagnifyIcon";
import PlusIcon from "mdi-react/PlusIcon";
import PencilLockIcon from "mdi-react/PencilLockIcon";
import TableOfContentsIcon from "mdi-react/TableOfContentsIcon";
import React, { useRef, useState } from "react";
import { useSafeArea } from "../hooks/useSafeArea";
import { useTabContext, useTabStore } from "../hooks/useTabStore";
import { Settings } from "../utils";
import { EditorEvents } from "../utils/editor-events";
import { EventTypes, Settings } from "../utils";
import styles from "./styles.module.css";
import { strings } from "@notesnook/intl";
@@ -104,10 +100,6 @@ function Header({
const openedTabsCount = useTabStore((state) => state.tabs.length);
const [isOpen, setOpen] = useState(false);
const btnRef = useRef(null);
const [canGoBack, canGoForward] = useTabStore((state) => [
state.canGoBack,
state.canGoForward
]);
return (
<div
@@ -139,7 +131,7 @@ function Header({
) : (
<Button
onPress={() => {
post(EditorEvents.back, undefined, tab.id, tab.session?.noteId);
post(EventTypes.back, undefined, tab.id, tab.noteId);
}}
preventDefault={false}
style={{
@@ -173,15 +165,75 @@ function Header({
flexDirection: "row"
}}
>
{tab.locked ? null : (
<>
<Button
onPress={() => {
editor?.commands.undo();
}}
style={{
borderWidth: 0,
borderRadius: 100,
color: "var(--nn_primary_icon)",
marginRight: 10,
width: 39,
height: 39,
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "relative"
}}
>
<ArrowULeftTopIcon
color={
!hasUndo
? "var(--nn_secondary_border)"
: "var(--nn_primary_icon)"
}
size={25 * settings.fontScale}
style={{
position: "absolute"
}}
/>
</Button>
<Button
onPress={() => {
if (tab.locked) return;
editor?.commands.redo();
}}
style={{
borderWidth: 0,
borderRadius: 100,
color: "var(--nn_primary_icon)",
marginRight: 10,
width: 39,
height: 39,
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "relative"
}}
>
<ArrowURightTopIcon
color={
!hasRedo
? "var(--nn_secondary_border)"
: "var(--nn_primary_icon)"
}
size={25 * settings.fontScale}
style={{
position: "absolute"
}}
/>
</Button>
</>
)}
{settings.deviceMode !== "mobile" && !settings.fullscreen ? (
<Button
onPress={() => {
post(
EditorEvents.fullscreen,
undefined,
tab.id,
tab.session?.noteId
);
post(EventTypes.fullscreen, undefined, tab.id, tab.noteId);
}}
preventDefault={false}
style={{
@@ -207,12 +259,14 @@ function Header({
</Button>
) : null}
{tab.session?.readonly ? (
{tab.readonly ? (
<Button
onPress={() => {
post(
"editor-events:disable-readonly-mode",
tab.session?.noteId
useTabStore
.getState()
.getNoteIdForTab(useTabStore.getState().currentTab)
);
}}
fwdRef={btnRef}
@@ -242,72 +296,7 @@ function Header({
<Button
onPress={() => {
editor?.commands.undo();
}}
style={{
borderWidth: 0,
borderRadius: 100,
color: "var(--nn_primary_icon)",
marginRight: 10,
width: 39,
height: 39,
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "relative"
}}
>
<ArrowULeftTopIcon
color={
!hasUndo
? "var(--nn_secondary_border)"
: "var(--nn_primary_icon)"
}
size={25 * settings.fontScale}
style={{
position: "absolute"
}}
/>
</Button>
<Button
onPress={() => {
editor?.commands.redo();
}}
style={{
borderWidth: 0,
borderRadius: 100,
color: "var(--nn_primary_icon)",
marginRight: 10,
width: 39,
height: 39,
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "relative"
}}
>
<ArrowURightTopIcon
color={
!hasRedo
? "var(--nn_secondary_border)"
: "var(--nn_primary_icon)"
}
size={25 * settings.fontScale}
style={{
position: "absolute"
}}
/>
</Button>
<Button
onPress={() => {
post(
EditorEvents.showTabs,
undefined,
tab.id,
tab.session?.noteId
);
post(EventTypes.showTabs, undefined, tab.id, tab.noteId);
}}
preventDefault={false}
style={{
@@ -351,13 +340,8 @@ function Header({
<Button
fwdRef={btnRef}
onPress={() => {
if (tab.session?.locked) {
post(
EditorEvents.properties,
undefined,
tab.id,
tab.session?.noteId
);
if (tab.locked) {
post(EventTypes.properties, undefined, tab.id, tab.noteId);
} else {
setOpen(!isOpen);
}
@@ -376,7 +360,7 @@ function Header({
position: "relative"
}}
>
{tab.session?.locked ? (
{tab.locked ? (
<DotsHorizontalIcon
size={25 * settings.fontScale}
style={{
@@ -411,153 +395,33 @@ function Header({
switch (e.value) {
case "toc":
post(
EditorEvents.toc,
EventTypes.toc,
editorControllers[tab.id]?.getTableOfContents(),
tab.id,
tab.session?.noteId
tab.noteId
);
break;
case "search":
editor?.commands.startSearch();
break;
case "newNote":
post(
EditorEvents.newNote,
undefined,
tab.id,
tab.session?.noteId
);
break;
case "properties":
post(
EditorEvents.properties,
undefined,
tab.id,
tab.session?.noteId
);
logger("info", "post properties...");
post(EventTypes.properties, undefined, tab.id, tab.noteId);
break;
default:
break;
}
}}
>
<div
style={{
display: "flex",
gap: 10,
alignItems: "center",
flexDirection: "row",
justifyContent: "center",
flex: 1,
paddingTop: 5
}}
>
<Button
onPress={() => {
post(
EditorEvents.goBack,
undefined,
tab.id,
tab.session?.noteId
);
setOpen(false);
}}
style={{
borderWidth: 0,
borderRadius: 100,
color: "var(--nn_primary_icon)",
width: 39,
height: 39,
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "relative"
}}
>
<ArrowBackIcon
color={
!canGoBack
? "var(--nn_secondary_border)"
: "var(--nn_primary_icon)"
}
size={25 * settings.fontScale}
style={{
position: "absolute"
}}
/>
</Button>
<Button
onPress={() => {
post(
EditorEvents.goForward,
undefined,
tab.id,
tab.session?.noteId
);
setOpen(false);
}}
style={{
borderWidth: 0,
borderRadius: 100,
color: "var(--nn_primary_icon)",
width: 39,
height: 39,
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "relative"
}}
>
<ArrowForwardIcon
color={
!canGoForward
? "var(--nn_secondary_border)"
: "var(--nn_primary_icon)"
}
size={25 * settings.fontScale}
style={{
position: "absolute"
}}
/>
</Button>
<Button
onPress={() => {
editor?.commands.startSearch();
setOpen(false);
}}
style={{
borderWidth: 0,
borderRadius: 100,
color: "var(--nn_primary_icon)",
width: 39,
height: 39,
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "relative"
}}
>
<MagnifyIcon
size={28 * settings.fontScale}
style={{
position: "absolute"
}}
color="var(--nn_primary_icon)"
/>
</Button>
</div>
<MenuItem
value="newNote"
value="search"
style={{
display: "flex",
gap: 10,
alignItems: "center"
}}
>
<PlusIcon
<MagnifyIcon
size={22 * settings.fontScale}
color="var(--nn_primary_icon)"
/>
@@ -566,7 +430,7 @@ function Header({
color: "var(--nn_primary_paragraph)"
}}
>
New note
{strings.search()}
</span>
</MenuItem>

View File

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

View File

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

View File

@@ -36,7 +36,7 @@ export default function TiptapEditorWrapper(props: {
return (
<>
{tab.session?.locked ? null : (
{tab.locked ? null : (
<EmotionEditorToolbarTheme>
<Toolbar
className="theme-scope-editorToolbar"

View File

@@ -17,7 +17,8 @@ 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 { getFontById, replaceDateTime } from "@notesnook/editor";
import { getFontById } from "@notesnook/editor";
import { replaceDateTime } from "@notesnook/editor";
import React, { RefObject, useCallback, useEffect, useRef } from "react";
import { EditorController } from "../hooks/useEditorController";
import { useTabContext } from "../hooks/useTabStore";

View File

@@ -32,6 +32,7 @@ import {
useState
} from "react";
import {
EventTypes,
getRoot,
isReactNative,
post,
@@ -39,7 +40,6 @@ import {
saveTheme
} from "../utils";
import { injectCss, transform } from "../utils/css";
import { EditorEvents } from "../utils/editor-events";
import { pendingSaveRequests } from "../utils/pending-saves";
import { useTabContext, useTabStore } from "./useTabStore";
@@ -133,10 +133,7 @@ export function useEditorController({
scrollTo,
scrollTop
}: {
update: (
scrollTop?: number,
selection?: { to: number; from: number }
) => void;
update: () => void;
getTableOfContents: () => any[];
scrollTo: (top: number) => void;
scrollTop: () => number;
@@ -146,7 +143,7 @@ export function useEditorController({
const tabRef = useRef(tab);
tabRef.current = tab;
const [loading, setLoading] = useState(false);
const [loading, setLoading] = useState(true);
const setTheme = useThemeEngineStore((store) => store.setTheme);
const { colors } = useThemeColors("editor");
const [title, setTitle] = useState("");
@@ -160,12 +157,8 @@ export function useEditorController({
scroll: null
});
if (!tabRef.current.session?.noteId && loading) {
setTimeout(() => {
if (!tabRef.current.session?.noteId && loading) {
setLoading(false);
}
}, 3000);
if (!tabRef.current.noteId && loading) {
setLoading(false);
}
const selectionChange = useCallback((_editor: Editor) => {}, []);
@@ -174,22 +167,21 @@ export function useEditorController({
if (!isReactNative()) return;
const currentSessionId = globalThis.sessionId;
post(
EditorEvents.contentchange,
EventTypes.contentchange,
undefined,
tabRef.current.id,
tabRef.current.session?.noteId
tabRef.current.noteId
);
const params = [
{
title
},
tabRef.current.id,
tabRef.current.session?.noteId,
currentSessionId,
1000
tabRef.current.noteId,
currentSessionId
];
const pendingTitleIds = await pendingSaveRequests.getPendingTitleIds();
postAsyncWithTimeout(EditorEvents.title, ...params)
postAsyncWithTimeout(EventTypes.title, ...params, 1000)
.then(() => {
if (pendingTitleIds.length) {
dbLogger(
@@ -238,32 +230,30 @@ export function useEditorController({
}
const currentSessionId = globalThis.sessionId;
post(
EditorEvents.contentchange,
EventTypes.contentchange,
undefined,
tabRef.current.id,
tabRef.current.session?.noteId
tabRef.current.noteId
);
if (!editor) return;
if (typeof timers.current.change === "number") {
clearTimeout(timers.current?.change);
}
timers.current.change = setTimeout(async () => {
htmlContentRef.current = editor.getHTML();
const params = [
{
html: htmlContentRef.current,
ignoreEdit: ignoreEdit
},
tabRef.current.id,
tabRef.current.session?.noteId,
currentSessionId,
5000
tabRef.current.noteId,
currentSessionId
];
const pendingContentIds =
await pendingSaveRequests.getPendingContentIds();
postAsyncWithTimeout(EditorEvents.content, ...params)
postAsyncWithTimeout(EventTypes.content, ...params, 5000)
.then(() => {
if (pendingContentIds.length) {
dbLogger(
@@ -294,7 +284,12 @@ export function useEditorController({
}
});
logger("info", "Editor saving content", params[1], params[2]);
logger(
"info",
"Editor saving content",
tabRef.current.id,
tabRef.current.noteId
);
}, 300);
countWords(5000);
@@ -308,24 +303,14 @@ export function useEditorController({
if (timers.current.scroll !== null) clearTimeout(timers.current.scroll);
timers.current.scroll = setTimeout(() => {
if (
tabRef.current.session?.noteId &&
tabRef.current.session?.noteId ===
useTabStore.getState().getCurrentNoteId()
tabRef.current.noteId &&
tabRef.current.noteId === useTabStore.getState().getCurrentNoteId()
) {
post(
EditorEvents.saveScroll,
{
scrollTop: value,
selection: {
to: editors[tabRef.current.id]?.state.selection.to,
from: editors[tabRef.current.id]?.state.selection.from
}
},
tabRef.current.id,
tabRef.current.session?.noteId
);
useTabStore.getState().setNoteState(tabRef.current.noteId, {
top: value
});
}
}, 300);
}, 16);
},
[]
);
@@ -336,12 +321,12 @@ export function useEditorController({
}, [update]);
useEffect(() => {
if (tab.session?.locked) {
if (tab.locked) {
htmlContentRef.current = "";
setLoading(true);
onUpdate();
}
}, [tab.session?.locked, onUpdate]);
}, [tab.locked, onUpdate]);
const onMessage = useCallback(
(event: Event & { data?: string }) => {
@@ -357,35 +342,38 @@ export function useEditorController({
const editor = editors[tabRef.current.id];
switch (type) {
case "native:updatehtml": {
htmlContentRef.current = value.data;
htmlContentRef.current = value;
logger("info", "UPDATING NOTE HTML");
if (tabRef.current.id !== useTabStore.getState().currentTab) {
updateTabOnFocus.current = true;
} else {
if (!editor) break;
const noteState = tabRef.current?.noteId
? useTabStore.getState().noteState[tabRef.current?.noteId]
: null;
const top = scrollTop() || noteState?.top || 0;
editor?.commands.setContent(htmlContentRef.current, false, {
preserveWhitespace: true
});
if (value.selection) {
editor.commands.setTextSelection(value.selection);
if (noteState && editor.isFocused) {
editor.commands.setTextSelection({
from: noteState.from,
to: noteState.to
});
}
scrollTo?.(value.scrollTop || 0);
setLoading(false);
scrollTo?.(top || 0);
countWords(0);
}
break;
}
case "native:html":
if (htmlContentRef.current === value.data) {
setLoading(false);
break;
}
htmlContentRef.current = value.data;
htmlContentRef.current = value;
logger("info", "LOADING NOTE HTML");
if (!editor) break;
update(value.scrollTop, value.selection);
update();
setTimeout(() => {
countWords(0);
}, 300);
@@ -419,7 +407,7 @@ export function useEditorController({
}
post(type); // Notify that message was delivered successfully.
},
[update, setTheme, scrollTo, countWords]
[update, countWords, setTheme]
);
useEffect(() => {
@@ -430,46 +418,36 @@ export function useEditorController({
}, [onMessage]);
const openFilePicker = useCallback((type: "image" | "file" | "camera") => {
post(
EditorEvents.filepicker,
type,
tabRef.current.id,
tabRef.current.session?.noteId
);
post(EventTypes.filepicker, type, tabRef.current.id, tabRef.current.noteId);
}, []);
const downloadAttachment = useCallback((attachment: Attachment) => {
post(
EditorEvents.download,
EventTypes.download,
attachment,
tabRef.current.id,
tabRef.current.session?.noteId
tabRef.current.noteId
);
}, []);
const previewAttachment = useCallback((attachment: Attachment) => {
post(
EditorEvents.previewAttachment,
EventTypes.previewAttachment,
attachment,
tabRef.current.id,
tabRef.current.session?.noteId
tabRef.current.noteId
);
}, []);
const openLink = useCallback((url: string) => {
post(
EditorEvents.link,
url,
tabRef.current.id,
tabRef.current.session?.noteId
);
post(EventTypes.link, url, tabRef.current.id, tabRef.current.noteId);
return true;
}, []);
const copyToClipboard = (text: string) => {
post(EditorEvents.copyToClipboard, text);
post(EventTypes.copyToClipboard, text);
};
const getAttachmentData = (attachment: Partial<Attachment>) => {
return postAsyncWithTimeout(EditorEvents.getAttachmentData, {
return postAsyncWithTimeout(EventTypes.getAttachmentData, {
attachment
});
};

View File

@@ -27,51 +27,187 @@ globalThis.editorTitles = {};
globalThis.statusBars = {};
export type TabItem = {
id: string;
session?: {
noteId?: string;
readonly?: boolean;
locked?: boolean;
noteLocked?: boolean;
scrollTop?: number;
selection?: { to: number; from: number };
};
id: number;
noteId?: string;
previewTab?: boolean;
readonly?: boolean;
locked?: boolean;
noteLocked?: boolean;
pinned?: boolean;
needsRefresh?: boolean;
};
export type NoteState = {
top: number;
to: number;
from: number;
};
export type TabStore = {
tabs: TabItem[];
currentTab?: string;
currentTab: number;
scrollPosition: Record<number, number>;
noteState: Record<string, NoteState>;
updateTab: (id: number, options: Omit<Partial<TabItem>, "id">) => void;
removeTab: (index: number) => void;
moveTab: (index: number, toIndex: number) => void;
newTab: (noteId?: string, previewTab?: boolean) => 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;
getCurrentNoteId: () => string | undefined;
getTab: (tabId: number) => TabItem | undefined;
setNoteState: (noteId: string, state: Partial<NoteState>) => void;
biometryAvailable?: boolean;
biometryEnrolled?: boolean;
canGoBack?: boolean;
canGoForward?: boolean;
sessionId?: string;
getCurrentNoteId: () => string | undefined;
};
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) => ({
noteState: {},
tabs: [
{
id: "679da59a3924d4bd56d16d3f",
session: {
id: "679da5a5667a16db2353a062"
}
id: 0,
previewTab: true
}
],
currentTab: "679da59a3924d4bd56d16d3f",
currentTab: 0,
scrollPosition: {},
setNoteState: (noteId: string, state: Partial<NoteState>) => {
if (editorControllers[get().currentTab]?.loading) return;
const noteState = {
...get().noteState
};
noteState[noteId] = {
...get().noteState[noteId],
...state
};
set({
noteState
});
},
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 scrollPosition = { ...get().scrollPosition };
if (scrollPosition[index]) {
delete scrollPosition[index];
}
globalThis.editorControllers[index] = undefined;
globalThis.editors[index] = null;
set({
scrollPosition
});
},
focusPreviewTab: (noteId: string, options) => {
const index = get().tabs.findIndex((t) => t.previewTab);
if (index == -1) return get().newTab(noteId, true);
const tabs = [...get().tabs];
tabs[index] = {
...tabs[index],
noteId: noteId,
previewTab: true,
...options
};
set({
currentTab: tabs[index].id
});
},
focusEmptyTab: () => {
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, previewTab?: boolean) => {
const id = getId(get().tabs.length, get().tabs);
const nextTabs = [
...get().tabs,
{
id: id,
noteId,
previewTab: previewTab
}
];
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;
},
getCurrentNoteId: () => {
return get().tabs.find((t) => t.id === get().currentTab)?.session
?.noteId;
return get().tabs.find((t) => t.id === get().currentTab)?.noteId;
},
getTab: (tabId) => {
return get().tabs.find((t) => t.id === tabId);
}
}),
{
name: "tab-storage-v3",
name: "tab-storage",
storage: createJSONStorage(() => localStorage)
}
)
@@ -79,7 +215,9 @@ export const useTabStore = create(
globalThis.tabStore = useTabStore;
export const TabContext = createContext<TabItem>({} as TabItem);
export const TabContext = createContext<TabItem>({
id: 0
});
export const useTabContext = () => {
const tab = useContext(TabContext);

View File

@@ -17,7 +17,6 @@ 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 "./utils/index";
import "./utils/commands";
global.Buffer = require("buffer").Buffer;
import { i18n } from "@lingui/core";
import "@notesnook/editor/styles/fonts.mobile.css";

View File

@@ -1,200 +0,0 @@
/*
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 { Attachment, ImageAttributes, LinkAttributes } from "@notesnook/editor";
import { Settings } from ".";
globalThis.commands = {
clearContent: (tabId: string) => {
try {
const editor = editors[tabId];
const editorController = editorControllers[tabId];
const editorTitle = editorTitles[tabId];
const statusBar = statusBars[tabId];
if (editor) {
editor?.commands.blur();
editor?.commands.clearContent(false);
}
if (editorController) {
editorController.content.current = "";
editorController.onUpdate();
editorController.setTitle("");
}
if (editorTitle?.current) {
editorTitle.current?.blur();
editorTitle.current.value = "";
}
if (statusBar) {
statusBar.current.resetWords();
statusBar.current.set({ date: "", saved: "" });
}
} catch (error) {
logger("error", "clearContent", error, (error as Error).stack);
}
},
focus: (tabId: string, locked: boolean) => {
const editorController = editorControllers[tabId];
if (locked) {
editorController?.focusPassInput();
} else {
editors[tabId]?.commands.focus();
}
},
blur: (tabId: string) => {
const editor = editors[tabId];
const editorTitle = editorTitles[tabId];
if (editor) editor.commands.blur();
if (editorTitle?.current) editorTitle.current.blur();
editorControllers[tabId]?.blurPassInput();
},
setSessionId: (id: string | undefined) => {
globalThis.sessionId = id;
},
setStatus: (date: string | undefined, saved: string, tabId: string) => {
const statusBar = statusBars[tabId];
if (statusBar?.current) {
statusBar.current.set({ date: date || "", saved });
}
},
setLoading: (loading?: boolean, tabId?: string) => {
if (tabId) {
const editorController = editorControllers[tabId];
editorController?.setLoading(loading || false);
logger("info", editorController?.setLoading);
}
},
setInsets: (insets: any) => {
if (typeof safeAreaController !== "undefined") {
safeAreaController.update(insets);
}
},
updateSettings: (settings?: Partial<Settings>) => {
if (typeof globalThis.settingsController !== "undefined") {
globalThis.settingsController.update(settings as Settings);
}
},
setSettings: (settings?: Partial<Settings>) => {
if (typeof globalThis.settingsController !== "undefined") {
globalThis.settingsController.update(settings as Settings);
}
},
setTags: async (tabId: string, tags: any) => {
const current = globalThis.editorTags[tabId];
if (current?.current) {
current.current.setTags(
tags.map((tag: any) => ({
title: tag.title,
alias: tag.title,
id: tag.id,
type: tag.type
}))
);
}
},
clearTags: (tabId: string) => {
const tags = editorTags[tabId];
if (tags?.current) {
tags.current.setTags([]);
}
},
insertAttachment: (attachment: Attachment, tabId: number) => {
const editor = editors[tabId];
if (editor) {
editor.commands.insertAttachment(attachment);
}
},
setAttachmentProgress: (
attachmentProgress: Partial<Attachment>,
tabId: number
) => {
const editor = editors[tabId];
if (editor) {
editor.commands.updateAttachment(attachmentProgress, {
preventUpdate: true,
query: (attachment) => attachment.hash === attachmentProgress.hash
});
}
},
insertImage: (
image: Omit<ImageAttributes, "bloburl"> & { dataurl: string },
tabId: number
) => {
const editor = editors[tabId];
if (editor) {
editor.commands.insertImage({
...image
});
}
},
handleBack: () => {
return window.dispatchEvent(
new Event("handleBackPress", { cancelable: true })
);
},
keyboardShown: (keyboardShown: boolean) => {
globalThis["keyboardShown"] = keyboardShown;
},
getTableOfContents: (tabId: string) => {
return editorControllers[tabId]?.getTableOfContents() || [];
},
focusPassInput: (tabId: string) => {
return editorControllers[tabId]?.focusPassInput() || [];
},
blurPassInput: (tabId: string) => {
return editorControllers[tabId]?.blurPassInput() || [];
},
createInternalLink: (attributes: LinkAttributes, resolverId: string) => {
if (globalThis.pendingResolvers[resolverId]) {
globalThis.pendingResolvers[resolverId](attributes);
}
},
dismissCreateInternalLinkRequest: (resolverId: string) => {
if (globalThis.pendingResolvers[resolverId]) {
globalThis.pendingResolvers[resolverId](undefined);
}
},
scrollIntoViewById: (id: string, tabId: string) => {
return editorControllers[tabId]?.scrollIntoView(id) || [];
}
};

View File

@@ -22,8 +22,6 @@ import { ThemeDefinition } from "@notesnook/theme";
import { Dispatch, MutableRefObject, RefObject, SetStateAction } from "react";
import { EditorController } from "../hooks/useEditorController";
import { EditorEvents } from "./editor-events";
globalThis.sessionId = "notesnook-editor";
globalThis.pendingResolvers = {};
@@ -64,7 +62,7 @@ declare global {
var readonlyEditor: boolean;
var statusBars: Record<
string,
number,
| React.MutableRefObject<{
set: React.Dispatch<
React.SetStateAction<{
@@ -83,22 +81,20 @@ declare global {
var noHeader: boolean;
function toBlobURL(dataurl: string, id?: string): string | undefined;
var pendingResolvers: { [name: string]: (value: any) => void };
var commands: any;
/**
* Id of current session
*/
var sessionId: string | undefined;
var sessionId: string;
var tabStore: any;
/**
* Current tiptap editors
*/
var editors: Record<string, Editor | null>;
var editors: Record<number, Editor | null>;
/**
* Current editor controllers
*/
var editorControllers: Record<string, EditorController | undefined>;
var editorControllers: Record<number, EditorController | undefined>;
var settingsController: {
update: (settings: Settings) => void;
@@ -126,12 +122,12 @@ declare global {
>;
};
var editorTitles: Record<string, RefObject<HTMLTextAreaElement> | undefined>;
var editorTitles: Record<number, RefObject<HTMLTextAreaElement> | undefined>;
/**
* Global ref to manage tags in editor.
*/
var editorTags: Record<
string,
number,
| MutableRefObject<{
setTags: React.Dispatch<
React.SetStateAction<
@@ -154,10 +150,10 @@ declare global {
* @param value
*/
function post<T extends keyof typeof EditorEvents>(
type: (typeof EditorEvents)[T],
function post<T extends keyof typeof EventTypes>(
type: (typeof EventTypes)[T],
value?: unknown,
tabId?: string,
tabId?: number,
noteId?: string,
sessionId?: string
): void;
@@ -188,6 +184,44 @@ export function getOnMessageListener(callback: () => void) {
};
}
/* eslint-enable no-var */
export const EventTypes = {
selection: "editor-event:selection",
content: "editor-event:content",
title: "editor-event:title",
scroll: "editor-event:scroll",
history: "editor-event:history",
newtag: "editor-event:newtag",
tag: "editor-event:tag",
filepicker: "editor-event:picker",
download: "editor-event:download-attachment",
logger: "native:logger",
back: "editor-event:back",
pro: "editor-event:pro",
monograph: "editor-event:monograph",
properties: "editor-event:properties",
fullscreen: "editor-event:fullscreen",
link: "editor-event:link",
contentchange: "editor-event:content-change",
reminders: "editor-event:reminders",
previewAttachment: "editor-event:preview-attachment",
copyToClipboard: "editor-events:copy-to-clipboard",
getAttachmentData: "editor-events:get-attachment-data",
tabsChanged: "editor-events:tabs-changed",
showTabs: "editor-events:show-tabs",
tabFocused: "editor-events:tab-focused",
toc: "editor-events:toc",
createInternalLink: "editor-events:create-internal-link",
load: "editor-events:load",
unlock: "editor-events:unlock",
unlockWithBiometrics: "editor-events:unlock-biometrics",
disableReadonlyMode: "editor-events:disable-readonly-mode",
readonlyEditorLoaded: "readonlyEditorLoaded",
error: "editorError",
dbLogger: "editor-events:dbLogger"
} as const;
export function randId(prefix: string) {
return Math.random()
.toString(36)
@@ -210,7 +244,7 @@ export function logger(
})
.join(" ");
post(EditorEvents.logger, `[${type}]: ` + logString);
post(EventTypes.logger, `[${type}]: ` + logString);
}
export function dbLogger(type: "error" | "log", ...logs: unknown[]): void {
@@ -220,7 +254,7 @@ export function dbLogger(type: "error" | "log", ...logs: unknown[]): void {
})
.join(" ");
post(EditorEvents.dbLogger, {
post(EventTypes.dbLogger, {
message: `[${type}]: ` + logString,
error: logs[0] instanceof Error ? logs[0] : undefined
});
@@ -229,7 +263,7 @@ export function dbLogger(type: "error" | "log", ...logs: unknown[]): void {
export function post(
type: string,
value?: unknown,
tabId?: string,
tabId?: number,
noteId?: string,
sessionId?: string,
hasTimeout?: boolean
@@ -256,7 +290,7 @@ export function post(
export async function postAsyncWithTimeout<R = any>(
type: string,
value?: unknown,
tabId?: string,
tabId?: number,
noteId?: string,
sessionId?: string,
waitFor?: number

View File

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

View File

@@ -16,8 +16,7 @@ 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 { postAsyncWithTimeout, randId } from ".";
import { EditorEvents } from "./editor-events";
import { EventTypes, postAsyncWithTimeout, randId } from ".";
class PendingSaveRequests {
static TITLES = "pendingTitles";
@@ -119,7 +118,7 @@ class PendingSaveRequests {
this.remove(PendingSaveRequests.TITLES);
for (const pending of pendingTitles) {
if (pending.params[0]) pending.params[0].pendingChanges = true;
await postAsyncWithTimeout(EditorEvents.title, ...pending.params);
await postAsyncWithTimeout(EventTypes.title, ...pending.params, 5000);
}
};
@@ -128,7 +127,7 @@ class PendingSaveRequests {
this.remove(PendingSaveRequests.CONTENT);
for (const pending of pendingContents) {
if (pending.params[0]) pending.params[0].pendingChanges = true;
await postAsyncWithTimeout(EditorEvents.content, ...pending.params);
await postAsyncWithTimeout(EventTypes.content, ...pending.params, 5000);
}
};

View File

@@ -1,17 +1,7 @@
diff --git a/node_modules/prosemirror-view/dist/index.cjs b/node_modules/prosemirror-view/dist/index.cjs
index 8ea57c7..c289489 100644
index 8ea57c7..aeda01d 100644
--- a/node_modules/prosemirror-view/dist/index.cjs
+++ b/node_modules/prosemirror-view/dist/index.cjs
@@ -980,8 +980,8 @@ var ViewDesc = function () {
if (!(force || brKludge && safari) && isEquivalentPosition(anchorDOM.node, anchorDOM.offset, domSel.anchorNode, domSel.anchorOffset) && isEquivalentPosition(headDOM.node, headDOM.offset, domSel.focusNode, domSel.focusOffset)) return;
var domSelExtended = false;
if ((domSel.extend || anchor == head) && !brKludge) {
- domSel.collapse(anchorDOM.node, anchorDOM.offset);
try {
+ domSel.collapse(anchorDOM.node, anchorDOM.offset);
if (anchor != head) domSel.extend(headDOM.node, headDOM.offset);
domSelExtended = true;
} catch (_) {}
@@ -3456,7 +3456,7 @@ editHandlers.drop = function (view, _event) {
});
tr.setSelection(selectionBetween(view, $pos, tr.doc.resolve(end)));
@@ -22,19 +12,9 @@ index 8ea57c7..c289489 100644
};
handlers.focus = function (view) {
diff --git a/node_modules/prosemirror-view/dist/index.js b/node_modules/prosemirror-view/dist/index.js
index 9583dc3..6899e62 100644
index 9583dc3..991bf0a 100644
--- a/node_modules/prosemirror-view/dist/index.js
+++ b/node_modules/prosemirror-view/dist/index.js
@@ -1052,8 +1052,8 @@ class ViewDesc {
// browsers support it yet.
let domSelExtended = false;
if ((domSel.extend || anchor == head) && !brKludge) {
- domSel.collapse(anchorDOM.node, anchorDOM.offset);
try {
+ domSel.collapse(anchorDOM.node, anchorDOM.offset);
if (anchor != head)
domSel.extend(headDOM.node, headDOM.offset);
domSelExtended = true;
@@ -3731,7 +3731,7 @@ editHandlers.drop = (view, _event) => {
tr.mapping.maps[tr.mapping.maps.length - 1].forEach((_from, _to, _newFrom, newTo) => end = newTo);
tr.setSelection(selectionBetween(view, $pos, tr.doc.resolve(end)));

View File

@@ -17,7 +17,7 @@ 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 { Input, Text } from "@theme-ui/components";
import { Input } from "@theme-ui/components";
import { Flex } from "@theme-ui/components";
import { useRefValue } from "../../hooks/use-ref-value.js";
import { Popup } from "../components/popup.js";
@@ -64,41 +64,19 @@ export function LinkPopup(props: LinkPopupProps) {
}}
>
{!isImageActive && (
<>
<Text
sx={{
mb: 1,
ml: 1,
fontSize: "body",
color: "paragraph"
}}
>
{strings.linkText()}
</Text>
<Input
type="text"
placeholder={strings.linkText()}
defaultValue={link.current?.title}
sx={{ mb: 2 }}
onChange={(e) =>
(link.current = {
...link.current,
title: e.target.value
})
}
/>
</>
<Input
type="text"
placeholder={strings.linkText()}
defaultValue={link.current?.title}
sx={{ mb: 1 }}
onChange={(e) =>
(link.current = {
...link.current,
title: e.target.value
})
}
/>
)}
<Text
sx={{
mb: 1,
ml: 1,
fontSize: "body",
color: "paragraph"
}}
>
{strings.url()}
</Text>
<Input
type="url"
autoFocus

View File

@@ -13,7 +13,7 @@ msgstr ""
"Language-Team: \n"
"Plural-Forms: \n"
#: src/strings.ts:2390
#: src/strings.ts:2389
msgid " \"Notebook > Notes\""
msgstr " \"Notebook > Notes\""
@@ -279,7 +279,7 @@ msgstr "{count, plural, one {Item restored} other {# items restored}}"
msgid "{count, plural, one {Item unpublished} other {# items unpublished}}"
msgstr "{count, plural, one {Item unpublished} other {# items unpublished}}"
#: src/strings.ts:2407
#: src/strings.ts:2406
msgid "{count, plural, one {Move all notes in this notebook to trash} other {Move all notes in these notebooks to trash}}"
msgstr "{count, plural, one {Move all notes in this notebook to trash} other {Move all notes in these notebooks to trash}}"
@@ -724,7 +724,7 @@ msgstr "All"
msgid "All attachments are end-to-end encrypted."
msgstr "All attachments are end-to-end encrypted."
#: src/strings.ts:2384
#: src/strings.ts:2383
msgid "All cached attachments have been cleared."
msgstr "All cached attachments have been cleared."
@@ -911,7 +911,7 @@ msgstr "Attachment"
msgid "Attachment preview failed"
msgstr "Attachment preview failed"
#: src/strings.ts:2377
#: src/strings.ts:2376
msgid "Attachment recheck cancelled"
msgstr "Attachment recheck cancelled"
@@ -932,7 +932,7 @@ msgstr "Attachments"
msgid "Attachments cache cleared!"
msgstr "Attachments cache cleared!"
#: src/strings.ts:2379
#: src/strings.ts:2378
msgid "Attachments recheck complete"
msgstr "Attachments recheck complete"
@@ -1149,7 +1149,7 @@ msgstr "Biometrics not enrolled"
msgid "Bold"
msgstr "Bold"
#: src/strings.ts:2389
#: src/strings.ts:2388
msgid "Boost your productivity with Notebooks and organize your notes."
msgstr "Boost your productivity with Notebooks and organize your notes."
@@ -1310,7 +1310,7 @@ msgstr "Check roadmap"
msgid "Check your spam folder if you haven't received an email yet."
msgstr "Check your spam folder if you haven't received an email yet."
#: src/strings.ts:2381
#: src/strings.ts:2380
msgid "Checking all attachments"
msgstr "Checking all attachments"
@@ -1322,7 +1322,7 @@ msgstr "Checking for new version"
msgid "Checking for updates"
msgstr "Checking for updates"
#: src/strings.ts:2380
#: src/strings.ts:2379
msgid "Checking note attachments"
msgstr "Checking note attachments"
@@ -1468,7 +1468,7 @@ msgstr "Click to preview"
msgid "Click to remove"
msgstr "Click to remove"
#: src/strings.ts:2372
#: src/strings.ts:2371
msgid "Click to reset {title}"
msgstr "Click to reset {title}"
@@ -1569,7 +1569,7 @@ msgstr "Compressed images are uploaded in Full HD resolution and usually are goo
msgid "Configure"
msgstr "Configure"
#: src/strings.ts:2386
#: src/strings.ts:2385
msgid "Configure server URLs for Notesnook"
msgstr "Configure server URLs for Notesnook"
@@ -1892,7 +1892,7 @@ msgstr "Debug logs downloaded"
msgid "Debugging"
msgstr "Debugging"
#: src/strings.ts:2374
#: src/strings.ts:2373
msgid "Decrease {title}"
msgstr "Decrease {title}"
@@ -2190,7 +2190,7 @@ msgstr "Duplicate"
msgid "Earliest first"
msgstr "Earliest first"
#: src/strings.ts:2398
#: src/strings.ts:2397
msgid "Easy access"
msgstr "Easy access"
@@ -2224,7 +2224,7 @@ msgstr "Editor"
msgid "Email"
msgstr "Email"
#: src/strings.ts:2411
#: src/strings.ts:2410
msgid "Email copied"
msgstr "Email copied"
@@ -2392,7 +2392,7 @@ msgstr "Enter the 6 digit code sent to your email to continue logging in"
msgid "Enter the 6 digit code sent to your phone number to continue logging in"
msgstr "Enter the 6 digit code sent to your phone number to continue logging in"
#: src/strings.ts:2413
#: src/strings.ts:2412
msgid "Enter the gift code to redeem your subscription."
msgstr "Enter the gift code to redeem your subscription."
@@ -2412,7 +2412,7 @@ msgstr "Enter your new email"
msgid "Enter your username"
msgstr "Enter your username"
#: src/strings.ts:2405
#: src/strings.ts:2404
msgid "Error"
msgstr "Error"
@@ -2452,15 +2452,15 @@ msgstr "Errors in {count} attachments"
msgid "Events server"
msgstr "Events server"
#: src/strings.ts:2391
#: src/strings.ts:2390
msgid "Every Notebook can have notes and sub notebooks."
msgstr "Every Notebook can have notes and sub notebooks."
#: src/strings.ts:2393
#: src/strings.ts:2392
msgid "Everything related to my job in one place."
msgstr "Everything related to my job in one place."
#: src/strings.ts:2402
#: src/strings.ts:2401
msgid "Everything related to my school in one place."
msgstr "Everything related to my school in one place."
@@ -2517,7 +2517,7 @@ msgstr "EXTREMELY DANGEROUS! This action is irreversible. All your data includin
msgid "Faced an issue or have a suggestion? Click here to create a bug report"
msgstr "Faced an issue or have a suggestion? Click here to create a bug report"
#: src/strings.ts:2383
#: src/strings.ts:2382
msgid "Failed"
msgstr "Failed"
@@ -2602,11 +2602,11 @@ msgstr "Favorite"
msgid "Favorites"
msgstr "Favorites"
#: src/strings.ts:2395
#: src/strings.ts:2394
msgid "February 2022 Week 2"
msgstr "February 2022 Week 2"
#: src/strings.ts:2396
#: src/strings.ts:2395
msgid "February 2022 Week 3"
msgstr "February 2022 Week 3"
@@ -2784,7 +2784,7 @@ msgstr "Get Notesnook Pro"
msgid "Get Notesnook Pro to enable automatic backups"
msgstr "Get Notesnook Pro to enable automatic backups"
#: src/strings.ts:2387
#: src/strings.ts:2386
msgid "Get Priority support"
msgstr "Get Priority support"
@@ -2956,7 +2956,7 @@ msgstr "I have a recovery code"
msgid "I have saved my key"
msgstr "I have saved my key"
#: src/strings.ts:2404
#: src/strings.ts:2403
msgid "I love cooking and collecting recipes."
msgstr "I love cooking and collecting recipes."
@@ -3064,7 +3064,7 @@ msgstr "Incoming note"
msgid "Incorrect {type}"
msgstr "Incorrect {type}"
#: src/strings.ts:2373
#: src/strings.ts:2372
msgid "Increase {title}"
msgstr "Increase {title}"
@@ -3072,7 +3072,7 @@ msgstr "Increase {title}"
msgid "Insert"
msgstr "Insert"
#: src/strings.ts:2370
#: src/strings.ts:2369
msgid "Insert a {rows}x{columns} table"
msgstr "Insert a {rows}x{columns} table"
@@ -3527,7 +3527,7 @@ msgstr "Maximize"
msgid "Meet other privacy-minded people & talk to us directly about your concerns, issues and suggestions."
msgstr "Meet other privacy-minded people & talk to us directly about your concerns, issues and suggestions."
#: src/strings.ts:2397
#: src/strings.ts:2396
msgid "Meetings"
msgstr "Meetings"
@@ -4114,7 +4114,7 @@ msgstr "Partial backups contain all your data except attachments. They are creat
msgid "Partially refunded"
msgstr "Partially refunded"
#: src/strings.ts:2382
#: src/strings.ts:2381
msgid "Passed"
msgstr "Passed"
@@ -4308,7 +4308,7 @@ msgstr "Please select the day to repeat the reminder on"
msgid "please send us an email from your registered email address"
msgstr "please send us an email from your registered email address"
#: src/strings.ts:2371
#: src/strings.ts:2370
msgid "Please set a table size"
msgstr "Please set a table size"
@@ -4602,7 +4602,7 @@ msgstr "Receipt"
msgid "RECENT BACKUPS"
msgstr "RECENT BACKUPS"
#: src/strings.ts:2378
#: src/strings.ts:2377
msgid "Recheck all"
msgstr "Recheck all"
@@ -4610,7 +4610,7 @@ msgstr "Recheck all"
msgid "Rechecking failed"
msgstr "Rechecking failed"
#: src/strings.ts:2403
#: src/strings.ts:2402
msgid "Recipes"
msgstr "Recipes"
@@ -4654,15 +4654,15 @@ msgstr "Recovery key text file saved"
msgid "Recovery successful!"
msgstr "Recovery successful!"
#: src/strings.ts:2415
#: src/strings.ts:2414
msgid "Redeem"
msgstr "Redeem"
#: src/strings.ts:2412
#: src/strings.ts:2411
msgid "Redeem gift code"
msgstr "Redeem gift code"
#: src/strings.ts:2414
#: src/strings.ts:2413
msgid "Redeeming gift code"
msgstr "Redeeming gift code"
@@ -4913,7 +4913,7 @@ msgstr "Restore"
msgid "Restore backup"
msgstr "Restore backup"
#: src/strings.ts:2385
#: src/strings.ts:2384
msgid "Restore backup?"
msgstr "Restore backup?"
@@ -5073,11 +5073,11 @@ msgstr "Save your data recovery key in a safe place. You will need it to recover
msgid "Save your recovery codes in a safe place. You will need them to recover your account in case you lose access to your two-factor authentication methods."
msgstr "Save your recovery codes in a safe place. You will need them to recover your account in case you lose access to your two-factor authentication methods."
#: src/strings.ts:2375
#: src/strings.ts:2374
msgid "Saved"
msgstr "Saved"
#: src/strings.ts:2376
#: src/strings.ts:2375
msgid "Saving"
msgstr "Saving"
@@ -5097,7 +5097,7 @@ msgstr "Saving zip file. Please wait..."
msgid "Scan the QR code with your authenticator app"
msgstr "Scan the QR code with your authenticator app"
#: src/strings.ts:2401
#: src/strings.ts:2400
msgid "School work"
msgstr "School work"
@@ -5857,7 +5857,7 @@ msgstr "Tap twice to confirm you have saved the recovery key."
msgid "Task list"
msgstr "Task list"
#: src/strings.ts:2394
#: src/strings.ts:2393
msgid "Tasks"
msgstr "Tasks"
@@ -6322,10 +6322,6 @@ msgstr "Uploads"
msgid "Urgent"
msgstr "Urgent"
#: src/strings.ts:2367
msgid "URL"
msgstr "URL"
#: src/strings.ts:1770
msgid "Use"
msgstr "Use"
@@ -6599,7 +6595,7 @@ msgstr "What went wrong?"
msgid "Width"
msgstr "Width"
#: src/strings.ts:2392
#: src/strings.ts:2391
msgid "Work & Office"
msgstr "Work & Office"
@@ -6664,7 +6660,7 @@ msgstr "You can add as many tags as you want."
msgid "You can change the theme at any time from Settings or the side menu."
msgstr "You can change the theme at any time from Settings or the side menu."
#: src/strings.ts:2400
#: src/strings.ts:2399
msgid "You can create shortcuts of frequently accessed notebooks in the side menu"
msgstr "You can create shortcuts of frequently accessed notebooks in the side menu"

View File

@@ -13,7 +13,7 @@ msgstr ""
"Language-Team: \n"
"Plural-Forms: \n"
#: src/strings.ts:2390
#: src/strings.ts:2389
msgid " \"Notebook > Notes\""
msgstr ""
@@ -279,7 +279,7 @@ msgstr ""
msgid "{count, plural, one {Item unpublished} other {# items unpublished}}"
msgstr ""
#: src/strings.ts:2407
#: src/strings.ts:2406
msgid "{count, plural, one {Move all notes in this notebook to trash} other {Move all notes in these notebooks to trash}}"
msgstr ""
@@ -724,7 +724,7 @@ msgstr ""
msgid "All attachments are end-to-end encrypted."
msgstr ""
#: src/strings.ts:2384
#: src/strings.ts:2383
msgid "All cached attachments have been cleared."
msgstr ""
@@ -911,7 +911,7 @@ msgstr ""
msgid "Attachment preview failed"
msgstr ""
#: src/strings.ts:2377
#: src/strings.ts:2376
msgid "Attachment recheck cancelled"
msgstr ""
@@ -932,7 +932,7 @@ msgstr ""
msgid "Attachments cache cleared!"
msgstr ""
#: src/strings.ts:2379
#: src/strings.ts:2378
msgid "Attachments recheck complete"
msgstr ""
@@ -1149,7 +1149,7 @@ msgstr ""
msgid "Bold"
msgstr ""
#: src/strings.ts:2389
#: src/strings.ts:2388
msgid "Boost your productivity with Notebooks and organize your notes."
msgstr ""
@@ -1310,7 +1310,7 @@ msgstr ""
msgid "Check your spam folder if you haven't received an email yet."
msgstr ""
#: src/strings.ts:2381
#: src/strings.ts:2380
msgid "Checking all attachments"
msgstr ""
@@ -1322,7 +1322,7 @@ msgstr ""
msgid "Checking for updates"
msgstr ""
#: src/strings.ts:2380
#: src/strings.ts:2379
msgid "Checking note attachments"
msgstr ""
@@ -1457,7 +1457,7 @@ msgstr ""
msgid "Click to remove"
msgstr ""
#: src/strings.ts:2372
#: src/strings.ts:2371
msgid "Click to reset {title}"
msgstr ""
@@ -1558,7 +1558,7 @@ msgstr ""
msgid "Configure"
msgstr ""
#: src/strings.ts:2386
#: src/strings.ts:2385
msgid "Configure server URLs for Notesnook"
msgstr ""
@@ -1881,7 +1881,7 @@ msgstr ""
msgid "Debugging"
msgstr ""
#: src/strings.ts:2374
#: src/strings.ts:2373
msgid "Decrease {title}"
msgstr ""
@@ -2179,7 +2179,7 @@ msgstr ""
msgid "Earliest first"
msgstr ""
#: src/strings.ts:2398
#: src/strings.ts:2397
msgid "Easy access"
msgstr ""
@@ -2213,7 +2213,7 @@ msgstr ""
msgid "Email"
msgstr ""
#: src/strings.ts:2411
#: src/strings.ts:2410
msgid "Email copied"
msgstr ""
@@ -2381,7 +2381,7 @@ msgstr ""
msgid "Enter the 6 digit code sent to your phone number to continue logging in"
msgstr ""
#: src/strings.ts:2413
#: src/strings.ts:2412
msgid "Enter the gift code to redeem your subscription."
msgstr ""
@@ -2401,7 +2401,7 @@ msgstr ""
msgid "Enter your username"
msgstr ""
#: src/strings.ts:2405
#: src/strings.ts:2404
msgid "Error"
msgstr ""
@@ -2441,15 +2441,15 @@ msgstr ""
msgid "Events server"
msgstr ""
#: src/strings.ts:2391
#: src/strings.ts:2390
msgid "Every Notebook can have notes and sub notebooks."
msgstr ""
#: src/strings.ts:2393
#: src/strings.ts:2392
msgid "Everything related to my job in one place."
msgstr ""
#: src/strings.ts:2402
#: src/strings.ts:2401
msgid "Everything related to my school in one place."
msgstr ""
@@ -2506,7 +2506,7 @@ msgstr ""
msgid "Faced an issue or have a suggestion? Click here to create a bug report"
msgstr ""
#: src/strings.ts:2383
#: src/strings.ts:2382
msgid "Failed"
msgstr ""
@@ -2591,11 +2591,11 @@ msgstr ""
msgid "Favorites"
msgstr ""
#: src/strings.ts:2395
#: src/strings.ts:2394
msgid "February 2022 Week 2"
msgstr ""
#: src/strings.ts:2396
#: src/strings.ts:2395
msgid "February 2022 Week 3"
msgstr ""
@@ -2766,7 +2766,7 @@ msgstr ""
msgid "Get Notesnook Pro to enable automatic backups"
msgstr ""
#: src/strings.ts:2387
#: src/strings.ts:2386
msgid "Get Priority support"
msgstr ""
@@ -2938,7 +2938,7 @@ msgstr ""
msgid "I have saved my key"
msgstr ""
#: src/strings.ts:2404
#: src/strings.ts:2403
msgid "I love cooking and collecting recipes."
msgstr ""
@@ -3044,7 +3044,7 @@ msgstr ""
msgid "Incorrect {type}"
msgstr ""
#: src/strings.ts:2373
#: src/strings.ts:2372
msgid "Increase {title}"
msgstr ""
@@ -3052,7 +3052,7 @@ msgstr ""
msgid "Insert"
msgstr ""
#: src/strings.ts:2370
#: src/strings.ts:2369
msgid "Insert a {rows}x{columns} table"
msgstr ""
@@ -3507,7 +3507,7 @@ msgstr ""
msgid "Meet other privacy-minded people & talk to us directly about your concerns, issues and suggestions."
msgstr ""
#: src/strings.ts:2397
#: src/strings.ts:2396
msgid "Meetings"
msgstr ""
@@ -4088,7 +4088,7 @@ msgstr ""
msgid "Partially refunded"
msgstr ""
#: src/strings.ts:2382
#: src/strings.ts:2381
msgid "Passed"
msgstr ""
@@ -4282,7 +4282,7 @@ msgstr ""
msgid "please send us an email from your registered email address"
msgstr ""
#: src/strings.ts:2371
#: src/strings.ts:2370
msgid "Please set a table size"
msgstr ""
@@ -4576,7 +4576,7 @@ msgstr ""
msgid "RECENT BACKUPS"
msgstr ""
#: src/strings.ts:2378
#: src/strings.ts:2377
msgid "Recheck all"
msgstr ""
@@ -4584,7 +4584,7 @@ msgstr ""
msgid "Rechecking failed"
msgstr ""
#: src/strings.ts:2403
#: src/strings.ts:2402
msgid "Recipes"
msgstr ""
@@ -4628,15 +4628,15 @@ msgstr ""
msgid "Recovery successful!"
msgstr ""
#: src/strings.ts:2415
#: src/strings.ts:2414
msgid "Redeem"
msgstr ""
#: src/strings.ts:2412
#: src/strings.ts:2411
msgid "Redeem gift code"
msgstr ""
#: src/strings.ts:2414
#: src/strings.ts:2413
msgid "Redeeming gift code"
msgstr ""
@@ -4887,7 +4887,7 @@ msgstr ""
msgid "Restore backup"
msgstr ""
#: src/strings.ts:2385
#: src/strings.ts:2384
msgid "Restore backup?"
msgstr ""
@@ -5047,11 +5047,11 @@ msgstr ""
msgid "Save your recovery codes in a safe place. You will need them to recover your account in case you lose access to your two-factor authentication methods."
msgstr ""
#: src/strings.ts:2375
#: src/strings.ts:2374
msgid "Saved"
msgstr ""
#: src/strings.ts:2376
#: src/strings.ts:2375
msgid "Saving"
msgstr ""
@@ -5071,7 +5071,7 @@ msgstr ""
msgid "Scan the QR code with your authenticator app"
msgstr ""
#: src/strings.ts:2401
#: src/strings.ts:2400
msgid "School work"
msgstr ""
@@ -5823,7 +5823,7 @@ msgstr ""
msgid "Task list"
msgstr ""
#: src/strings.ts:2394
#: src/strings.ts:2393
msgid "Tasks"
msgstr ""
@@ -6281,10 +6281,6 @@ msgstr ""
msgid "Urgent"
msgstr ""
#: src/strings.ts:2367
msgid "URL"
msgstr ""
#: src/strings.ts:1770
msgid "Use"
msgstr ""
@@ -6550,7 +6546,7 @@ msgstr ""
msgid "Width"
msgstr ""
#: src/strings.ts:2392
#: src/strings.ts:2391
msgid "Work & Office"
msgstr ""
@@ -6615,7 +6611,7 @@ msgstr ""
msgid "You can change the theme at any time from Settings or the side menu."
msgstr ""
#: src/strings.ts:2400
#: src/strings.ts:2399
msgid "You can create shortcuts of frequently accessed notebooks in the side menu"
msgstr ""

View File

@@ -2364,7 +2364,6 @@ Use this if changes from other devices are not appearing on this device. This wi
height: () => t`Height`,
pasteImageURL: () => t`Paste image URL here`,
linkText: () => t`Link text`,
url: () => t`URL`,
insertTableOfSize: (rows: number, columns: number) =>
t`Insert a ${rows}x${columns} table`,