mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-01 11:39:21 +02:00
Compare commits
2 Commits
core/diagn
...
fix/confir
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d713b8a2f2 | ||
|
|
f13217e7f5 |
@@ -143,9 +143,6 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
}, [hide, show]);
|
||||
|
||||
const onNegativePress = async () => {
|
||||
if (dialogInfo?.onClose) {
|
||||
await dialogInfo.onClose();
|
||||
}
|
||||
hide();
|
||||
};
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ import { IconButton } from "../../ui/icon-button";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { confirmationDialog } from "../../../utils/functions";
|
||||
import { Dialog } from "../../dialog";
|
||||
|
||||
const TabItemComponent = (props: {
|
||||
tab: TabItem;
|
||||
@@ -163,7 +165,21 @@ const TabItemComponent = (props: {
|
||||
name="close"
|
||||
size={AppFontSize.lg}
|
||||
color={colors.primary.icon}
|
||||
onPress={() => {
|
||||
onPress={async () => {
|
||||
if (
|
||||
useTabStore.getState().getTab(props.tab.id)?.session
|
||||
?.hasUnsavedChanges &&
|
||||
!(await confirmationDialog({
|
||||
title: strings.unsavedChanges(),
|
||||
paragraph: strings.unsavedNoteDesc(),
|
||||
positiveText: "Yes",
|
||||
negativeText: "No",
|
||||
context: "local"
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isLastTab = useTabStore.getState().tabs.length === 1;
|
||||
useTabStore.getState().removeTab(props.tab.id);
|
||||
// The last tab is not actually removed, it is just cleaned up.
|
||||
@@ -216,6 +232,7 @@ export default function EditorTabs({
|
||||
maxHeight: "100%"
|
||||
}}
|
||||
>
|
||||
<Dialog context="local" />
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
@@ -227,8 +244,8 @@ export default function EditorTabs({
|
||||
<Heading size={AppFontSize.lg}>{strings.tabs()}</Heading>
|
||||
<View style={{ flexDirection: "row", gap: DefaultAppStyles.GAP_SMALL }}>
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
useTabStore.getState().clearAllTabs();
|
||||
onPress={async () => {
|
||||
await useTabStore.getState().clearAllTabs();
|
||||
close?.();
|
||||
}}
|
||||
name="close-box-multiple-outline"
|
||||
@@ -263,4 +280,4 @@ EditorTabs.present = () => {
|
||||
presentSheet({
|
||||
component: (ref, close, update) => <EditorTabs close={close} />
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -69,6 +69,8 @@ import { SessionHistory } from "./session-history";
|
||||
import { EditorState, SavePayload } from "./types";
|
||||
import { TabSessionItem, syncTabs, useTabStore } from "./use-tab-store";
|
||||
import { defaultState, isContentInvalid, isEditorLoaded, post } from "./utils";
|
||||
import { presentDialog } from "../../../components/dialog/functions";
|
||||
import { confirmationDialog } from "../../../utils/functions";
|
||||
|
||||
const loadNoteMutex = new Mutex();
|
||||
|
||||
@@ -311,6 +313,31 @@ export const useEditor = (
|
||||
noteData.dateEdited = note?.dateEdited;
|
||||
}
|
||||
|
||||
if (noteData?.id) {
|
||||
const dbNote = await db.notes.all
|
||||
.fields(["notes.id", "notes.dateEdited"])
|
||||
.find((e) => e("notes.id", "==", noteData.id!));
|
||||
if (
|
||||
dbNote &&
|
||||
dbNote?.dateEdited !== currentNotes.current[noteData.id]?.dateEdited
|
||||
) {
|
||||
if (
|
||||
!(await confirmationDialog({
|
||||
title: strings.conflictDetected(),
|
||||
paragraph: strings.conflictDetectedDesc(),
|
||||
positiveText: strings.overwrite()
|
||||
}))
|
||||
) {
|
||||
useTabStore.getState().updateTab(tabId, {
|
||||
session: {
|
||||
hasUnsavedChanges: true
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data) {
|
||||
noteData.content = {
|
||||
data: data,
|
||||
@@ -458,9 +485,19 @@ export const useEditor = (
|
||||
NotePreviewWidget.updateNote(id, note);
|
||||
}, 500);
|
||||
|
||||
useTabStore.getState().updateTab(tabId, {
|
||||
session: {
|
||||
hasUnsavedChanges: false
|
||||
}
|
||||
});
|
||||
|
||||
return id;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
useTabStore.getState().updateTab(tabId, {
|
||||
session: {
|
||||
hasUnsavedChanges: true
|
||||
}
|
||||
});
|
||||
DatabaseLogger.error(e as Error);
|
||||
}
|
||||
},
|
||||
@@ -525,11 +562,34 @@ export const useEditor = (
|
||||
) {
|
||||
return;
|
||||
}
|
||||
DatabaseLogger.log(
|
||||
`Loading note: ${event.item?.id || "new-note"}, block: ${
|
||||
event.blockId
|
||||
}`
|
||||
);
|
||||
if (event.blockId) {
|
||||
blockIdRef.current = event.blockId;
|
||||
}
|
||||
state.current.currentlyEditing = true;
|
||||
|
||||
if (
|
||||
useTabStore
|
||||
.getState()
|
||||
.getTab(event.tabId || useTabStore.getState().currentTab)?.session
|
||||
?.hasUnsavedChanges &&
|
||||
!(await confirmationDialog({
|
||||
title: strings.unsavedChanges(),
|
||||
paragraph: strings.unsavedNoteDesc(),
|
||||
positiveText: "Yes",
|
||||
negativeText: "No"
|
||||
}))
|
||||
) {
|
||||
console.log("hide overlay");
|
||||
commands.setLoading(false);
|
||||
overlay(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!state.current.ready &&
|
||||
(await isEditorLoaded(
|
||||
@@ -703,6 +763,10 @@ export const useEditor = (
|
||||
await postMessage(NativeEvents.title, item.title, tabId);
|
||||
overlay(false);
|
||||
|
||||
DatabaseLogger.log(
|
||||
`"Loading content length: ${currentContents.current[item.id]?.data}`
|
||||
);
|
||||
|
||||
await postMessage(
|
||||
NativeEvents.html,
|
||||
{
|
||||
@@ -715,26 +779,28 @@ export const useEditor = (
|
||||
10000
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
if (event.searchResultIndex !== undefined) {
|
||||
commands.scrollToSearchResult(event.searchResultIndex);
|
||||
}
|
||||
if (blockIdRef.current) {
|
||||
commands.scrollIntoViewById(blockIdRef.current);
|
||||
blockIdRef.current = undefined;
|
||||
}
|
||||
}, 300);
|
||||
await sleep(300);
|
||||
if (event.searchResultIndex !== undefined) {
|
||||
commands.scrollToSearchResult(event.searchResultIndex);
|
||||
}
|
||||
if (blockIdRef.current) {
|
||||
commands.scrollIntoViewById(blockIdRef.current);
|
||||
blockIdRef.current = undefined;
|
||||
}
|
||||
|
||||
await commands.setTags(item);
|
||||
commands.setSettings();
|
||||
setTimeout(() => {
|
||||
if (currentLoadingNoteId.current === event.item?.id) {
|
||||
currentLoadingNoteId.current = undefined;
|
||||
}
|
||||
}, 300);
|
||||
await sleep(300);
|
||||
if (currentLoadingNoteId.current === event.item?.id) {
|
||||
currentLoadingNoteId.current = undefined;
|
||||
}
|
||||
}
|
||||
postMessage(NativeEvents.theme, theme);
|
||||
console.log("load finished", event.item?.id);
|
||||
DatabaseLogger.log(
|
||||
`Loaded note: ${event.item?.id || "new-note"}, block: ${
|
||||
event.blockId
|
||||
}`
|
||||
);
|
||||
});
|
||||
},
|
||||
[
|
||||
@@ -763,6 +829,12 @@ export const useEditor = (
|
||||
await commands.clearContent(tabId);
|
||||
useTabStore.getState().removeTab(tabId);
|
||||
}
|
||||
|
||||
DatabaseLogger.log(
|
||||
`Realtime sync item skipped: item deleted${isDeleted(
|
||||
data
|
||||
)} trash:${isTrashItem(data)}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -870,6 +942,10 @@ export const useEditor = (
|
||||
commands.setLoading(true, tabId);
|
||||
}
|
||||
} else {
|
||||
DatabaseLogger.log(
|
||||
`Realtime sync content update: ${note.id}, locked: true`
|
||||
);
|
||||
|
||||
await postMessage(
|
||||
NativeEvents.updatehtml,
|
||||
{
|
||||
@@ -891,6 +967,9 @@ export const useEditor = (
|
||||
}
|
||||
|
||||
lastContentChangeTime.current[note.id] = note.dateEdited;
|
||||
DatabaseLogger.log(
|
||||
`Realtime sync content update: ${note.id}, locked: false`
|
||||
);
|
||||
await postMessage(
|
||||
NativeEvents.updatehtml,
|
||||
{
|
||||
@@ -908,7 +987,10 @@ export const useEditor = (
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e as Error, "Error when applying sync changes");
|
||||
DatabaseLogger.error(
|
||||
e as Error,
|
||||
"Error when applying realtime sync changes in editor"
|
||||
);
|
||||
} finally {
|
||||
lock.current = false;
|
||||
}
|
||||
@@ -946,7 +1028,7 @@ export const useEditor = (
|
||||
pendingChanges?: boolean;
|
||||
}) => {
|
||||
DatabaseLogger.log(
|
||||
`saveContent... title: ${!!title}, content: ${!!content}, noteId: ${noteId}`
|
||||
`Save content: ${!!title}, content: ${!!content}, noteId: ${noteId}, pendingChanges: ${pendingChanges}, tabId: ${tabId}`
|
||||
);
|
||||
if (
|
||||
lock.current ||
|
||||
@@ -1016,7 +1098,7 @@ export const useEditor = (
|
||||
saveNote(params);
|
||||
}
|
||||
},
|
||||
ignoreEdit ? 0 : 150
|
||||
150
|
||||
);
|
||||
},
|
||||
[editorSessionHistory, withTimer, onChange, saveNote]
|
||||
|
||||
@@ -29,6 +29,8 @@ import { MMKV } from "../../../common/database/mmkv";
|
||||
import { eSendEvent } from "../../../services/event-manager";
|
||||
import { eOnLoadNote } from "../../../utils/events";
|
||||
import { editorController } from "./utils";
|
||||
import { confirmationDialog } from "../../../utils/functions";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
class TabHistory {
|
||||
history: string[];
|
||||
@@ -87,6 +89,7 @@ export type TabSessionItem = {
|
||||
locked?: boolean;
|
||||
readonly?: boolean;
|
||||
spellCheckDisabled: boolean;
|
||||
hasUnsavedChanges?: boolean;
|
||||
};
|
||||
|
||||
const TabSessionStorageKV = new MMKVLoader()
|
||||
@@ -188,7 +191,7 @@ export type TabStore = {
|
||||
focusEmptyTab: () => void;
|
||||
getCurrentNoteId: () => string | undefined;
|
||||
getTab: (tabId: string) => TabItem | undefined;
|
||||
clearAllTabs: () => void;
|
||||
clearAllTabs: () => Promise<void>;
|
||||
newTabSession: (
|
||||
id: string,
|
||||
options?: Omit<Partial<TabSessionItem>, "id">
|
||||
@@ -258,7 +261,7 @@ export const useTabStore = create<TabStore, any>(
|
||||
|
||||
const sessionId =
|
||||
oldSessionId &&
|
||||
tabSessionHistory.currentSessionId(tabId) === oldSessionId
|
||||
tabSessionHistory.currentSessionId(tabId) === oldSessionId
|
||||
? oldSessionId
|
||||
: tabSessionHistory.add(tabId, oldSessionId);
|
||||
|
||||
@@ -389,7 +392,7 @@ export const useTabStore = create<TabStore, any>(
|
||||
focusPreviewTab: (
|
||||
noteId: string,
|
||||
options: Omit<Partial<TabItem>, "id" | "noteId">
|
||||
) => { },
|
||||
) => {},
|
||||
|
||||
removeTab: (id: string) => {
|
||||
const index = get().tabs.findIndex((t) => t.id === id);
|
||||
@@ -486,23 +489,48 @@ export const useTabStore = create<TabStore, any>(
|
||||
getTab: (tabId) => {
|
||||
return get().tabs.find((t) => t.id === tabId);
|
||||
},
|
||||
clearAllTabs: () => {
|
||||
const tabs = get().tabs;
|
||||
tabs.forEach((tab) => {
|
||||
clearAllTabs: async () => {
|
||||
const tabs = get().tabs.slice();
|
||||
|
||||
for (let i = 0; i < tabs.length; i++) {
|
||||
const tab = tabs[i];
|
||||
if (
|
||||
tab.session?.hasUnsavedChanges &&
|
||||
!(await confirmationDialog({
|
||||
title: strings.unsavedChanges(),
|
||||
paragraph: strings.unsavedNoteDesc(),
|
||||
positiveText: "Yes",
|
||||
negativeText: "No",
|
||||
context: "local"
|
||||
}))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const tabSessions = tabSessionHistory.getTabHistory(tab.id);
|
||||
tabSessions.back.forEach((id) => TabSessionStorage.remove(id));
|
||||
tabSessions.forward.forEach((id) => TabSessionStorage.remove(id));
|
||||
tabSessionHistory.clearStackForTab(tab.id);
|
||||
});
|
||||
tabs.splice(i, 1);
|
||||
history.remove(tab.id);
|
||||
}
|
||||
|
||||
const id = getId();
|
||||
set({
|
||||
tabs: [{ id: id }],
|
||||
currentTab: id
|
||||
});
|
||||
history.history = [id];
|
||||
get().newTabSession(id);
|
||||
get().focusTab(id);
|
||||
if (tabs.length === 0) {
|
||||
const id = getId();
|
||||
set({
|
||||
tabs: [{ id: id }],
|
||||
currentTab: id
|
||||
});
|
||||
|
||||
history.history = [id];
|
||||
get().newTabSession(id);
|
||||
get().focusTab(id);
|
||||
} else {
|
||||
set({
|
||||
tabs: tabs,
|
||||
currentTab: tabs[0].id
|
||||
});
|
||||
get().focusTab(tabs[0].id);
|
||||
}
|
||||
syncTabs();
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -21,7 +21,7 @@ import { ItemType } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { Linking } from "react-native";
|
||||
import { db } from "../common/database";
|
||||
import { presentDialog } from "../components/dialog/functions";
|
||||
import { DialogInfo, presentDialog } from "../components/dialog/functions";
|
||||
import {
|
||||
useSideMenuNotebookSelectionStore,
|
||||
useSideMenuTagsSelectionStore
|
||||
@@ -219,3 +219,28 @@ export const deleteItems = async (
|
||||
export const openLinkInBrowser = async (link: string) => {
|
||||
Linking.openURL(link);
|
||||
};
|
||||
|
||||
export async function confirmationDialog(options: {
|
||||
title: string;
|
||||
paragraph: string;
|
||||
positiveText: string;
|
||||
negativeText?: string;
|
||||
positiveType?: DialogInfo["positiveType"];
|
||||
context?: string;
|
||||
}) {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
presentDialog({
|
||||
context: options.context || "global",
|
||||
title: options.title,
|
||||
paragraph: options.paragraph,
|
||||
positiveText: options.positiveText,
|
||||
negativeText: options.negativeText,
|
||||
positiveType: options.positiveType,
|
||||
onClose: () => resolve(false),
|
||||
positivePress: async () => {
|
||||
resolve(true);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ import useTablet from "../../hooks/use-tablet";
|
||||
import { isMac } from "../../utils/platform";
|
||||
import { CREATE_BUTTON_MAP } from "../../common";
|
||||
import { getDragData } from "../../utils/data-transfer";
|
||||
import { saveContent } from "./index";
|
||||
import { closeTabs, saveContent } from "./common";
|
||||
|
||||
type ToolButton = {
|
||||
title: string;
|
||||
@@ -408,47 +408,24 @@ const TabStrip = React.memo(function TabStrip() {
|
||||
useEditorManager.getState();
|
||||
const editor = getEditor(activeEditorId || "")?.editor;
|
||||
if (!editor) return;
|
||||
saveContent(session.id, false, editor.getContent());
|
||||
saveContent(session.id, {
|
||||
content: { type: "tiptap", data: editor.getContent() }
|
||||
});
|
||||
}}
|
||||
onFocus={() => {
|
||||
if (tab.id !== currentTab) {
|
||||
useEditorStore.getState().activateSession(tab.sessionId);
|
||||
}
|
||||
if (tab.id === currentTab) return;
|
||||
useEditorStore.getState().activateSession(tab.sessionId);
|
||||
}}
|
||||
onClose={() => useEditorStore.getState().closeTabs(tab.id)}
|
||||
onCloseAll={() =>
|
||||
useEditorStore
|
||||
.getState()
|
||||
.closeTabs(
|
||||
...tabs.filter((s) => !s.pinned).map((s) => s.id)
|
||||
)
|
||||
onClose={async () => await closeTabs([tab])}
|
||||
onCloseAll={async () => await closeTabs(tabs)}
|
||||
onCloseOthers={async () =>
|
||||
await closeTabs(tabs.filter((s) => s.id !== tab.id))
|
||||
}
|
||||
onCloseOthers={() =>
|
||||
useEditorStore
|
||||
.getState()
|
||||
.closeTabs(
|
||||
...tabs
|
||||
.filter((s) => s.id !== tab.id && !s.pinned)
|
||||
.map((s) => s.id)
|
||||
)
|
||||
onCloseToTheRight={async () =>
|
||||
await closeTabs(tabs.filter((s, index) => index > i))
|
||||
}
|
||||
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)
|
||||
)
|
||||
onCloseToTheLeft={async () =>
|
||||
await closeTabs(tabs.filter((s, index) => index < i))
|
||||
}
|
||||
onRevealInList={
|
||||
"note" in session
|
||||
|
||||
@@ -17,6 +17,13 @@ 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 { strings } from "@notesnook/intl";
|
||||
import { ConfirmDialog } from "../../dialogs/confirm";
|
||||
import { SaveState, useEditorStore } from "../../stores/editor-store";
|
||||
import { showToast } from "../../utils/toast";
|
||||
import { db } from "../../common/db";
|
||||
import { NoteContent } from "@notesnook/core";
|
||||
|
||||
export const EDITOR_ZOOM = {
|
||||
DEFAULT: 100,
|
||||
MAX: 500,
|
||||
@@ -29,3 +36,88 @@ export const EDITOR_LINE_HEIGHT = {
|
||||
MAX: 10,
|
||||
MIN: 1
|
||||
};
|
||||
|
||||
export async function closeTabs(
|
||||
tabs: { id: string; pinned?: boolean; sessionId: string }[]
|
||||
) {
|
||||
const closableTabIds = [];
|
||||
for (const tab of tabs) {
|
||||
if (tab.pinned || !(await warnIfSessionNotSaved(tab.sessionId))) continue;
|
||||
closableTabIds.push(tab.id);
|
||||
}
|
||||
useEditorStore.getState().closeTabs(...closableTabIds);
|
||||
}
|
||||
|
||||
export async function warnIfSessionNotSaved(sessionId: string) {
|
||||
const session = useEditorStore.getState().getSession(sessionId);
|
||||
if (
|
||||
!session ||
|
||||
session.type !== "default" ||
|
||||
session.needsHydration ||
|
||||
session.saveState === SaveState.Saved
|
||||
)
|
||||
return true;
|
||||
|
||||
const result = await ConfirmDialog.show({
|
||||
title: strings.discardChanges(),
|
||||
message: strings.discardChangesDesc(),
|
||||
positiveButtonText: strings.discardChanges(),
|
||||
negativeButtonText: strings.cancel()
|
||||
});
|
||||
if (!result) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function isConflictingEdit(sessionId: string) {
|
||||
const session = useEditorStore.getState().getSession(sessionId);
|
||||
if (!session || !("note" in session)) return false;
|
||||
|
||||
const dbNote = await db.notes.all
|
||||
.fields(["notes.id", "notes.dateEdited"])
|
||||
.find((e) => e("notes.id", "==", session.note.id));
|
||||
if (dbNote && dbNote.dateEdited !== session.note.dateEdited) {
|
||||
const result = await ConfirmDialog.show({
|
||||
title: strings.conflictDetected(),
|
||||
message: strings.conflictDetectedDesc(),
|
||||
positiveButtonText: strings.overwrite(),
|
||||
negativeButtonText: strings.cancel()
|
||||
});
|
||||
if (!result) {
|
||||
showToast("error", strings.saveConflictError());
|
||||
useEditorStore.getState().setSaveState(session.id, SaveState.NotSaved);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function saveContent(
|
||||
sessionId: string,
|
||||
partial: { content: NoteContent<false> } | { title: string }
|
||||
) {
|
||||
if (await isConflictingEdit(sessionId)) return;
|
||||
|
||||
const saveTimeout = setTimeout(() => {
|
||||
const { hide } = showToast(
|
||||
"warn",
|
||||
strings.savingNoteTakingTooLong(),
|
||||
[
|
||||
{
|
||||
text: strings.dismiss(),
|
||||
onClick: () => hide()
|
||||
}
|
||||
],
|
||||
0
|
||||
);
|
||||
}, 30 * 1000);
|
||||
|
||||
try {
|
||||
await useEditorStore.getState().saveSession(sessionId, partial);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) showToast("error", e.message);
|
||||
} finally {
|
||||
clearTimeout(saveTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,14 +40,13 @@ import {
|
||||
useNoteStatistics
|
||||
} from "./manager";
|
||||
import { getFormattedDate } from "@notesnook/common";
|
||||
import { MAX_AUTO_SAVEABLE_WORDS, NoteStatistics } from "./types";
|
||||
import { NoteStatistics } from "./types";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { EDITOR_ZOOM } from "./common";
|
||||
import { EDITOR_ZOOM, saveContent } from "./common";
|
||||
import { useWindowControls } from "../../hooks/use-window-controls";
|
||||
import { exitFullscreen } from "../../utils/fullscreen";
|
||||
import { useRef, useState } from "react";
|
||||
import { PopupPresenter } from "@notesnook/ui";
|
||||
import { saveContent } from "./index";
|
||||
|
||||
const SAVE_STATE_ICON_MAP = {
|
||||
"-1": NotSaved,
|
||||
@@ -174,15 +173,6 @@ function EditorFooter() {
|
||||
<Plus size={13} />
|
||||
</Button>
|
||||
</Flex>
|
||||
{statistics.words.total > MAX_AUTO_SAVEABLE_WORDS ? (
|
||||
<Text
|
||||
className="selectable"
|
||||
variant="subBody"
|
||||
sx={{ color: "paragraph" }}
|
||||
>
|
||||
{strings.autoSaveOff()}
|
||||
</Text>
|
||||
) : null}
|
||||
<Button
|
||||
className="selectable"
|
||||
data-test-id="editor-word-count"
|
||||
@@ -292,12 +282,12 @@ function EditorFooter() {
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
if (saveState === SaveState.NotSaved) {
|
||||
const { activeEditorId, getEditor } = useEditorManager.getState();
|
||||
const editor = getEditor(activeEditorId || "")?.editor;
|
||||
if (!editor) return;
|
||||
saveContent(session.id, false, editor.getContent());
|
||||
}
|
||||
const { activeEditorId, getEditor } = useEditorManager.getState();
|
||||
const editor = getEditor(activeEditorId || "")?.editor;
|
||||
if (!editor) return;
|
||||
saveContent(session.id, {
|
||||
content: { type: "tiptap", data: editor.getContent() }
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -76,51 +76,14 @@ import { EditorActionBar } from "./action-bar";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { NoteLinkingDialog } from "../../dialogs/note-linking-dialog";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { onPageVisibilityChanged } from "../../utils/page-visibility";
|
||||
import { Pane, SplitPane } from "../split-pane";
|
||||
import { TITLE_BAR_HEIGHT } from "../title-bar";
|
||||
import { isMobile } from "../../hooks/use-mobile";
|
||||
import { ConfirmDialog } from "../../dialogs/confirm";
|
||||
import { saveContent } from "./common";
|
||||
|
||||
const PDFPreview = React.lazy(() => import("../pdf-preview"));
|
||||
|
||||
const autoSaveToast = { show: true, hide: () => {} };
|
||||
|
||||
export async function saveContent(
|
||||
noteId: string,
|
||||
ignoreEdit: boolean,
|
||||
content: string
|
||||
) {
|
||||
logger.debug("saving content", {
|
||||
noteId,
|
||||
ignoreEdit,
|
||||
length: content.length
|
||||
});
|
||||
await Promise.race([
|
||||
useEditorStore.getState().saveSessionContent(noteId, ignoreEdit, {
|
||||
type: "tiptap",
|
||||
data: content
|
||||
}),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(
|
||||
() => reject(new Error(strings.savingNoteTakingTooLong())),
|
||||
30 * 1000
|
||||
)
|
||||
)
|
||||
]).catch((e) => {
|
||||
const { hide } = showToast(
|
||||
"error",
|
||||
(e as Error).message,
|
||||
[
|
||||
{
|
||||
text: strings.dismiss(),
|
||||
onClick: () => hide()
|
||||
}
|
||||
],
|
||||
0
|
||||
);
|
||||
});
|
||||
}
|
||||
const deferredSave = debounceWithId(saveContent, 100);
|
||||
|
||||
export default function TabsView() {
|
||||
@@ -376,7 +339,7 @@ function EditorView({
|
||||
}
|
||||
onContentChange={() => (lastChangedTime.current = Date.now())}
|
||||
onSelectionChange={() => root.current?.classList.remove("searching")}
|
||||
onSave={(content, ignoreEdit) => {
|
||||
onSave={(content) => {
|
||||
const currentSession = useEditorStore
|
||||
.getState()
|
||||
.getSession(session.id);
|
||||
@@ -411,7 +374,9 @@ function EditorView({
|
||||
id: session.id,
|
||||
length: data.length
|
||||
});
|
||||
deferredSave(session.id, session.id, ignoreEdit, data);
|
||||
deferredSave(session.id, session.id, {
|
||||
content: { type: "tiptap", data: data }
|
||||
});
|
||||
}}
|
||||
options={{
|
||||
readonly: session?.type === "readonly" || session?.type === "deleted",
|
||||
@@ -514,18 +479,10 @@ export function Editor(props: EditorProps) {
|
||||
focusMode: false,
|
||||
spellcheck: false
|
||||
};
|
||||
const saveSessionContentIfNotSaved = useEditorStore(
|
||||
(store) => store.saveSessionContentIfNotSaved
|
||||
);
|
||||
const setEditorSaveState = useEditorStore((store) => store.setSaveState);
|
||||
useScrollToBlock(session);
|
||||
useScrollToSearchResult(session);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoSaveToast.show) {
|
||||
autoSaveToast.hide();
|
||||
}
|
||||
|
||||
const event = AppEventManager.subscribe(
|
||||
AppEvents.UPDATE_ATTACHMENT_PROGRESS,
|
||||
({ hash, loaded, total }: AttachmentProgress) => {
|
||||
@@ -542,15 +499,6 @@ export function Editor(props: EditorProps) {
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsub = onPageVisibilityChanged((_, hidden) => {
|
||||
if (hidden) {
|
||||
saveSessionContentIfNotSaved(id);
|
||||
}
|
||||
});
|
||||
return () => unsub();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
const editor = useEditorManager.getState().getEditor(id)?.editor;
|
||||
@@ -670,25 +618,6 @@ export function Editor(props: EditorProps) {
|
||||
const link = await NoteLinkingDialog.show({ attributes });
|
||||
return link || undefined;
|
||||
}}
|
||||
onAutoSaveDisabled={() => {
|
||||
setEditorSaveState(id, SaveState.NotSaved);
|
||||
if (autoSaveToast.show === false) return;
|
||||
const { hide } = showToast(
|
||||
"error",
|
||||
"Auto-save is disabled for large notes. Press Ctrl + S to save.",
|
||||
[
|
||||
{
|
||||
text: "Dismiss",
|
||||
onClick: () => {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
],
|
||||
Infinity
|
||||
);
|
||||
autoSaveToast.show = false;
|
||||
autoSaveToast.hide = hide;
|
||||
}}
|
||||
>
|
||||
{headless ? null : (
|
||||
<>
|
||||
|
||||
@@ -48,7 +48,7 @@ import {
|
||||
useMemo,
|
||||
useRef
|
||||
} from "react";
|
||||
import { IEditor, MAX_AUTO_SAVEABLE_WORDS } from "./types";
|
||||
import { IEditor } from "./types";
|
||||
import { useEditorConfig, useToolbarConfig, useEditorManager } from "./manager";
|
||||
import { useStore as useSettingsStore } from "../../stores/setting-store";
|
||||
import { useStore as useUserStore } from "../../stores/user-store";
|
||||
@@ -56,7 +56,6 @@ import { debounce, useAreFeaturesAvailable } from "@notesnook/common";
|
||||
import { ScopedThemeProvider } from "../theme-provider";
|
||||
import { useStore as useThemeStore } from "../../stores/theme-store";
|
||||
import { writeToClipboard } from "../../utils/clipboard";
|
||||
import { useEditorStore } from "../../stores/editor-store";
|
||||
import { DayFormat, parseInternalLink } from "@notesnook/core";
|
||||
import { desktop } from "../../common/desktop-bridge";
|
||||
import Skeleton from "react-loading-skeleton";
|
||||
@@ -73,10 +72,7 @@ import { handleInternalLink } from "../../common";
|
||||
import { db } from "../../common/db";
|
||||
import { showToast } from "../../utils/toast";
|
||||
|
||||
export type OnChangeHandler = (
|
||||
content: () => string,
|
||||
ignoreEdit: boolean
|
||||
) => void;
|
||||
export type OnChangeHandler = (content: () => string) => void;
|
||||
type TipTapProps = {
|
||||
id: string;
|
||||
editorContainer: () => HTMLElement | undefined;
|
||||
@@ -98,7 +94,6 @@ type TipTapProps = {
|
||||
) => Promise<LinkAttributes | undefined>;
|
||||
onAttachFile?: (file: File) => void;
|
||||
onFocus?: () => void;
|
||||
onAutoSaveDisabled: () => void;
|
||||
content?: () => string | undefined;
|
||||
readonly?: boolean;
|
||||
spellcheck?: boolean;
|
||||
@@ -177,7 +172,6 @@ function TipTap(props: TipTapProps) {
|
||||
onInsertInternalLink,
|
||||
onContentChange,
|
||||
onFocus = () => {},
|
||||
onAutoSaveDisabled,
|
||||
content,
|
||||
editorContainer,
|
||||
readonly,
|
||||
@@ -193,7 +187,6 @@ function TipTap(props: TipTapProps) {
|
||||
fontLigatures
|
||||
} = props;
|
||||
|
||||
const autoSave = useRef(true);
|
||||
const { toolbarConfig } = useToolbarConfig();
|
||||
const features = useAreFeaturesAvailable([
|
||||
"callout",
|
||||
@@ -243,10 +236,8 @@ function TipTap(props: TipTapProps) {
|
||||
handleKeyDown(_, event) {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === "s") {
|
||||
event.preventDefault();
|
||||
onChange?.(
|
||||
() =>
|
||||
getHTMLFromFragment(editor.state.doc.content, editor.schema),
|
||||
false
|
||||
onChange?.(() =>
|
||||
getHTMLFromFragment(editor.state.doc.content, editor.schema)
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -331,11 +322,8 @@ function TipTap(props: TipTapProps) {
|
||||
const ignoreEdit = transaction.getMeta("ignoreEdit") as boolean;
|
||||
if (preventSave || !editor.isEditable || !onChange) return;
|
||||
|
||||
if (!autoSave.current) return;
|
||||
|
||||
onChange(
|
||||
() => getHTMLFromFragment(editor.state.doc.content, editor.schema),
|
||||
ignoreEdit
|
||||
onChange(() =>
|
||||
getHTMLFromFragment(editor.state.doc.content, editor.schema)
|
||||
);
|
||||
},
|
||||
onDestroy: () => {
|
||||
@@ -523,21 +511,6 @@ function TipTap(props: TipTapProps) {
|
||||
};
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = useEditorManager.subscribe(
|
||||
(s) => s.editors[id]?.statistics?.words.total,
|
||||
(totalWords) => {
|
||||
autoSave.current = !totalWords || totalWords < MAX_AUTO_SAVEABLE_WORDS;
|
||||
if (!autoSave.current) {
|
||||
onAutoSaveDisabled();
|
||||
}
|
||||
}
|
||||
);
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScopedThemeProvider
|
||||
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { useEffect, useLayoutEffect, useMemo, useRef } from "react";
|
||||
import { Textarea } from "@theme-ui/components";
|
||||
import { SaveState, useEditorStore } from "../../stores/editor-store";
|
||||
import { useEditorStore } from "../../stores/editor-store";
|
||||
import { debounceWithId } from "@notesnook/common";
|
||||
import { useEditorConfig, useEditorManager } from "./manager";
|
||||
import { getFontById } from "@notesnook/editor";
|
||||
@@ -28,6 +28,7 @@ import { useStore as useSettingsStore } from "../../stores/setting-store";
|
||||
import { AppEventManager, AppEvents } from "../../common/app-events";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { NEWLINE_STRIP_REGEX } from "@notesnook/core";
|
||||
import { saveContent } from "./common";
|
||||
|
||||
type TitleBoxProps = {
|
||||
id: string;
|
||||
@@ -172,11 +173,11 @@ export function resizeTextarea(input: HTMLTextAreaElement) {
|
||||
}
|
||||
|
||||
async function onTitleChange(
|
||||
noteId: string,
|
||||
sessionId: string,
|
||||
title: string,
|
||||
pendingChanges: React.MutableRefObject<boolean>
|
||||
) {
|
||||
await useEditorStore.getState().setTitle(noteId, title);
|
||||
await saveContent(sessionId, { title });
|
||||
pendingChanges.current = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { Attachment } from "@notesnook/editor";
|
||||
|
||||
export const MAX_AUTO_SAVEABLE_WORDS = IS_TESTING ? 100 : 100_000;
|
||||
|
||||
export type NoteStatistics = {
|
||||
words: {
|
||||
total: number;
|
||||
|
||||
@@ -52,8 +52,8 @@ import { isCipher } from "@notesnook/core";
|
||||
import { AppEventManager, AppEvents } from "../common/app-events";
|
||||
import Vault from "../common/vault";
|
||||
import { Mutex } from "async-mutex";
|
||||
import { useEditorManager } from "../components/editor/manager";
|
||||
import { Context } from "../components/list-container/types";
|
||||
import { warnIfSessionNotSaved } from "../components/editor/common";
|
||||
|
||||
export enum SaveState {
|
||||
NotSaved = -1,
|
||||
@@ -585,11 +585,6 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
const session = this.get().sessions.find((s) => s.id === id);
|
||||
if (!session) id = undefined;
|
||||
|
||||
const activeSession = this.getActiveSession();
|
||||
if (activeSession) {
|
||||
this.saveSessionContentIfNotSaved(activeSession.id);
|
||||
}
|
||||
|
||||
if (
|
||||
id &&
|
||||
!useSettingStore.getState().hideNoteTitle &&
|
||||
@@ -717,6 +712,9 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
|
||||
const tab = tabs.find((t) => t.id === tabId);
|
||||
const activeSession = tab && getSession(tab.sessionId);
|
||||
if (tab?.sessionId && !(await warnIfSessionNotSaved(tab?.sessionId)))
|
||||
return;
|
||||
|
||||
const noteAlreadyOpened =
|
||||
activeSession &&
|
||||
"note" in activeSession &&
|
||||
@@ -982,125 +980,99 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
|
||||
saveSession = async (
|
||||
id: string,
|
||||
partial: Partial<Omit<DefaultEditorSession, "note">> & {
|
||||
note?: Partial<Note>;
|
||||
ignoreEdit?: boolean;
|
||||
}
|
||||
partial: { content: NoteContent<false> } | { title: string }
|
||||
) => {
|
||||
const session = this.getSession(id, ["new", "default"]);
|
||||
if (!session) return;
|
||||
|
||||
// do not allow saving of readonly session
|
||||
if (partial.note?.readonly) return;
|
||||
|
||||
await saveMutex.runExclusive(async () => {
|
||||
this.setSaveState(id, 0);
|
||||
const session = this.getSession(id, ["new", "default"]);
|
||||
if (!session) return;
|
||||
|
||||
this.setSaveState(id, SaveState.Saving);
|
||||
const editSessionId = getEditSessionId(session);
|
||||
|
||||
try {
|
||||
// Get session again as it might have changed to default.
|
||||
const currentSession =
|
||||
session.type === "new"
|
||||
? this.getSession(id, ["new", "default"])
|
||||
: session;
|
||||
|
||||
if (!currentSession) return;
|
||||
const sessionId = getSessionId(currentSession);
|
||||
let noteId =
|
||||
"note" in currentSession ? currentSession.note.id : partial.note?.id;
|
||||
|
||||
if (isLockedSession(currentSession) && partial.content) {
|
||||
if (!noteId) return;
|
||||
logger.debug("Saving locked content", { noteId });
|
||||
|
||||
noteId = await db.vault.save({
|
||||
content: partial.content,
|
||||
sessionId,
|
||||
id: noteId
|
||||
if (session.type === "new") {
|
||||
const noteId = await db.notes.add({
|
||||
sessionId: editSessionId,
|
||||
...partial
|
||||
});
|
||||
} else {
|
||||
if (partial.content)
|
||||
logger.debug("Saving content", {
|
||||
noteId,
|
||||
length: partial.content.data.length
|
||||
});
|
||||
noteId = await db.notes.add({
|
||||
...partial.note,
|
||||
dateEdited:
|
||||
partial.ignoreEdit && currentSession.type === "default"
|
||||
? currentSession.note.dateEdited
|
||||
: undefined,
|
||||
contentId:
|
||||
currentSession.type === "default"
|
||||
? currentSession.note.contentId
|
||||
: undefined,
|
||||
content: partial.content,
|
||||
sessionId,
|
||||
id: noteId
|
||||
});
|
||||
}
|
||||
|
||||
const note = noteId && (await db.notes.note(noteId));
|
||||
if (!note) throw new Error("Note not saved.");
|
||||
const note = await db.notes.note(noteId);
|
||||
if (!note) throw new Error("Note not saved.");
|
||||
|
||||
if (currentSession.type === "new") {
|
||||
const context = useNoteStore.getState().context;
|
||||
await addNotebook(note, context);
|
||||
await addTag(note, context);
|
||||
await addColor(note, context);
|
||||
}
|
||||
|
||||
const attachmentsLength = await db.attachments
|
||||
.ofNote(note.id, "all")
|
||||
.count();
|
||||
const shouldRefreshNotes =
|
||||
currentSession.type === "new" ||
|
||||
note.title !== currentSession.note?.title ||
|
||||
note.headline !== currentSession.note?.headline ||
|
||||
attachmentsLength !== currentSession.attachmentsLength;
|
||||
if (shouldRefreshNotes) useNoteStore.getState().refresh();
|
||||
|
||||
if (currentSession.type === "new") {
|
||||
useNoteStore.getState().refresh();
|
||||
await this.openSession(note, { force: true });
|
||||
} else {
|
||||
// update any conflicted session that has the same content opened
|
||||
if (partial.content) {
|
||||
this.set((state) => {
|
||||
const session = state.sessions.find(
|
||||
(s): s is ConflictedEditorSession =>
|
||||
(s.type === "diff" || s.type === "conflicted") &&
|
||||
!!s.content?.conflicted &&
|
||||
s.content.conflicted.id === currentSession.note.contentId &&
|
||||
s.content.conflicted.dateEdited ===
|
||||
currentSession.note.dateEdited
|
||||
);
|
||||
if (!session || !session.content?.conflicted) return;
|
||||
session.content.conflicted.data = partial.content!.data;
|
||||
session.content.conflicted.dateEdited = note.dateEdited;
|
||||
if (isLockedSession(session) && "content" in partial) {
|
||||
await db.vault.save({
|
||||
content: partial.content,
|
||||
sessionId: editSessionId,
|
||||
id: session.note.id
|
||||
});
|
||||
} else {
|
||||
await db.notes.add({
|
||||
dateEdited: session.note.dateEdited,
|
||||
contentId: session.note.contentId,
|
||||
sessionId: editSessionId,
|
||||
id: session.note.id,
|
||||
...partial
|
||||
});
|
||||
}
|
||||
|
||||
const note = await db.notes.note(session.note.id);
|
||||
if (!note) throw new Error("Note not saved.");
|
||||
|
||||
// update any conflicted session that has the same content opened
|
||||
if ("content" in partial) {
|
||||
this.set((state) => {
|
||||
const conflictedSession = state.sessions.find(
|
||||
(s): s is ConflictedEditorSession =>
|
||||
(s.type === "diff" || s.type === "conflicted") &&
|
||||
!!s.content?.conflicted &&
|
||||
s.content.conflicted.id === session.note.contentId &&
|
||||
s.content.conflicted.dateEdited === session.note.dateEdited
|
||||
);
|
||||
if (!conflictedSession || !conflictedSession.content?.conflicted)
|
||||
return;
|
||||
conflictedSession.content.conflicted.data = partial.content.data;
|
||||
conflictedSession.content.conflicted.dateEdited = note.dateEdited;
|
||||
});
|
||||
}
|
||||
|
||||
if ("title" in partial) {
|
||||
const { sessions } = this.get();
|
||||
for (const session of sessions) {
|
||||
if ("note" in session && session.note.id === note.id) {
|
||||
this.updateSession(session.id, undefined, {
|
||||
note,
|
||||
title: note.title
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const attachmentsLength = await db.attachments
|
||||
.ofNote(note.id, "all")
|
||||
.count();
|
||||
this.updateSession(id, ["default"], {
|
||||
attachmentsLength: attachmentsLength,
|
||||
note,
|
||||
sessionId
|
||||
sessionId: editSessionId,
|
||||
saveState: SaveState.Saved
|
||||
});
|
||||
const shouldRefreshNotes =
|
||||
note.title !== session.note?.title ||
|
||||
note.headline !== session.note?.headline ||
|
||||
attachmentsLength !== session.attachmentsLength;
|
||||
if (shouldRefreshNotes) useNoteStore.getState().refresh();
|
||||
setDocumentTitle(
|
||||
useSettingStore.getState().hideNoteTitle ? undefined : note.title
|
||||
);
|
||||
}
|
||||
|
||||
if (partial.note?.title !== undefined) {
|
||||
const { sessions } = this.get();
|
||||
for (const session of sessions) {
|
||||
if ("note" in session && session.note.id === note.id) {
|
||||
this.updateSession(session.id, undefined, {
|
||||
note,
|
||||
title: note.title
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setDocumentTitle(
|
||||
useSettingStore.getState().hideNoteTitle ? undefined : note.title
|
||||
);
|
||||
this.setSaveState(id, SaveState.Saved);
|
||||
} catch (err) {
|
||||
showToast(
|
||||
"error",
|
||||
@@ -1118,25 +1090,6 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
});
|
||||
};
|
||||
|
||||
saveSessionContentIfNotSaved = (sessionId: string) => {
|
||||
const sessionSaveState = this.getSession(sessionId, ["default"])?.saveState;
|
||||
if (sessionSaveState === SaveState.NotSaved) {
|
||||
const editor = useEditorManager.getState().getEditor(sessionId);
|
||||
const content = editor?.editor?.getContent();
|
||||
this.saveSession(
|
||||
sessionId,
|
||||
content
|
||||
? {
|
||||
content: {
|
||||
data: content,
|
||||
type: "tiptap"
|
||||
}
|
||||
}
|
||||
: {}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
newSession = () => {
|
||||
const { activeTabId, activateSession, getActiveTab } = this.get();
|
||||
if (!activeTabId || getActiveTab()?.pinned) {
|
||||
@@ -1186,8 +1139,6 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.saveSessionContentIfNotSaved(tab.sessionId);
|
||||
|
||||
db.fs().cancel(tab.sessionId).catch(console.error);
|
||||
if (state.history.includes(tab.id))
|
||||
state.history.splice(state.history.indexOf(tab.id), 1);
|
||||
@@ -1209,30 +1160,10 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
if (tabs.length === 0) this.addTab();
|
||||
};
|
||||
|
||||
setTitle = (id: string, title: string) => {
|
||||
return this.saveSession(id, { note: { title } });
|
||||
};
|
||||
|
||||
toggle = (
|
||||
id: string,
|
||||
name: "favorite" | "pinned" | "readonly" | "localOnly" | "color",
|
||||
value: boolean | string
|
||||
) => {
|
||||
if (name === "color" && typeof value === "string")
|
||||
return this.updateSession(id, ["readonly", "default"], { color: value });
|
||||
return this.saveSession(id, { note: { [name]: value } });
|
||||
};
|
||||
|
||||
saveSessionContent = (
|
||||
id: string,
|
||||
ignoreEdit: boolean,
|
||||
content: NoteContent<false>
|
||||
) => {
|
||||
return this.saveSession(id, { content, ignoreEdit });
|
||||
};
|
||||
|
||||
setSaveState = (id: string, saveState: SaveState) => {
|
||||
this.updateSession(id, ["default", "new"], { saveState: saveState });
|
||||
this.updateSession(id, ["default", "new"], {
|
||||
saveState: saveState
|
||||
});
|
||||
};
|
||||
|
||||
toggleProperties = (toggleState?: boolean) => {
|
||||
@@ -1390,7 +1321,7 @@ export { useEditorStore, SESSION_STATES };
|
||||
|
||||
const MILLISECONDS_IN_A_MINUTE = 60 * 1000;
|
||||
const SESSION_DURATION = MILLISECONDS_IN_A_MINUTE * 5;
|
||||
function getSessionId(session: DefaultEditorSession | NewEditorSession) {
|
||||
function getEditSessionId(session: DefaultEditorSession | NewEditorSession) {
|
||||
const sessionId =
|
||||
"sessionId" in session ? parseInt(session.sessionId) : Date.now();
|
||||
if (sessionId + SESSION_DURATION < Date.now()) return `${Date.now()}`;
|
||||
|
||||
@@ -1761,6 +1761,10 @@ msgstr "Close to the left"
|
||||
msgid "Close to the right"
|
||||
msgstr "Close to the right"
|
||||
|
||||
#: src/strings.ts:2801
|
||||
msgid "Closing this note will discard all unsaved changes. Are you sure you want to proceed?"
|
||||
msgstr "Closing this note will discard all unsaved changes. Are you sure you want to proceed?"
|
||||
|
||||
#: src/strings.ts:2541
|
||||
msgid "cloud storage space for storing images and files."
|
||||
msgstr "cloud storage space for storing images and files."
|
||||
@@ -1886,6 +1890,10 @@ msgstr "Confirm pin"
|
||||
msgid "Confirmation email sent"
|
||||
msgstr "Confirmation email sent"
|
||||
|
||||
#: src/strings.ts:2794
|
||||
msgid "Conflict detected"
|
||||
msgstr "Conflict detected"
|
||||
|
||||
#: src/strings.ts:2092
|
||||
msgid "Congratulations!"
|
||||
msgstr "Congratulations!"
|
||||
@@ -1987,6 +1995,10 @@ msgstr "Could not convert note to {format}."
|
||||
msgid "Could not create backup"
|
||||
msgstr "Could not create backup"
|
||||
|
||||
#: src/strings.ts:2798
|
||||
msgid "Could not save due to conflict."
|
||||
msgstr "Could not save due to conflict."
|
||||
|
||||
#: src/strings.ts:559
|
||||
msgid "Could not unlock"
|
||||
msgstr "Could not unlock"
|
||||
@@ -2403,6 +2415,10 @@ msgstr "Disabling will delete all your unsynced inbox items. Additionally, disab
|
||||
msgid "Discard"
|
||||
msgstr "Discard"
|
||||
|
||||
#: src/strings.ts:2799
|
||||
msgid "Discard changes"
|
||||
msgstr "Discard changes"
|
||||
|
||||
#: src/strings.ts:1599
|
||||
msgid "Dismiss"
|
||||
msgstr "Dismiss"
|
||||
@@ -4818,6 +4834,10 @@ msgstr "Other"
|
||||
msgid "Outline list"
|
||||
msgstr "Outline list"
|
||||
|
||||
#: src/strings.ts:2797
|
||||
msgid "Overwrite"
|
||||
msgstr "Overwrite"
|
||||
|
||||
#: src/strings.ts:2361
|
||||
msgid "Paragraph"
|
||||
msgstr "Paragraph"
|
||||
|
||||
@@ -1750,6 +1750,10 @@ msgstr ""
|
||||
msgid "Close to the right"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2801
|
||||
msgid "Closing this note will discard all unsaved changes. Are you sure you want to proceed?"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2541
|
||||
msgid "cloud storage space for storing images and files."
|
||||
msgstr ""
|
||||
@@ -1875,6 +1879,10 @@ msgstr ""
|
||||
msgid "Confirmation email sent"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2794
|
||||
msgid "Conflict detected"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2092
|
||||
msgid "Congratulations!"
|
||||
msgstr ""
|
||||
@@ -1976,6 +1984,10 @@ msgstr ""
|
||||
msgid "Could not create backup"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2798
|
||||
msgid "Could not save due to conflict."
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:559
|
||||
msgid "Could not unlock"
|
||||
msgstr ""
|
||||
@@ -2392,6 +2404,10 @@ msgstr ""
|
||||
msgid "Discard"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2799
|
||||
msgid "Discard changes"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1599
|
||||
msgid "Dismiss"
|
||||
msgstr ""
|
||||
@@ -4792,6 +4808,10 @@ msgstr ""
|
||||
msgid "Outline list"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2797
|
||||
msgid "Overwrite"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2361
|
||||
msgid "Paragraph"
|
||||
msgstr ""
|
||||
@@ -6926,7 +6946,7 @@ msgstr ""
|
||||
#: src/strings.ts:1103
|
||||
#: src/strings.ts:1112
|
||||
msgid "This must only be used for troubleshooting. Using it regularly for sync is not recommended and will lead to unexpected data loss and other issues. If you are having persistent issues with sync, please report them to us at support@streetwriters.co."
|
||||
msgstr ""
|
||||
msgstr "<<<<<<< Updated upstream"
|
||||
|
||||
#: src/strings.ts:2651
|
||||
msgid "This note is empty"
|
||||
@@ -7209,7 +7229,7 @@ msgstr ""
|
||||
|
||||
#: src/strings.ts:2098
|
||||
msgid "Unregister"
|
||||
msgstr ""
|
||||
msgstr "<<<<<<< Updated upstream"
|
||||
|
||||
#: src/strings.ts:2647
|
||||
msgid "Unset expiry"
|
||||
|
||||
@@ -2790,5 +2790,13 @@ Continue without attachments?`,
|
||||
t`Permission required to save QR-Code to Gallery`,
|
||||
setupInboxKeys: () => t`Setup inbox keys`,
|
||||
enterPgpPublicKey: () => t`Enter your PGP public key`,
|
||||
enterPgpPrivateKey: () => t`Enter your PGP private key`
|
||||
enterPgpPrivateKey: () => t`Enter your PGP private key`,
|
||||
conflictDetected: () => t`Conflict detected`,
|
||||
conflictDetectedDesc: () =>
|
||||
`The note has been modified since you opened it. Do you want to overwrite the changes?`,
|
||||
overwrite: () => t`Overwrite`,
|
||||
saveConflictError: () => t`Could not save due to conflict.`,
|
||||
discardChanges: () => t`Discard changes`,
|
||||
discardChangesDesc: () =>
|
||||
t`Closing this note will discard all unsaved changes. Are you sure you want to proceed?`
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user