mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-23 23:19:40 +01:00
mobile: some more fixes
This commit is contained in:
committed by
Abdullah Atta
parent
ad338aeefc
commit
366948cd28
@@ -43,18 +43,6 @@ I18nManager.allowRTL(false);
|
|||||||
I18nManager.forceRTL(false);
|
I18nManager.forceRTL(false);
|
||||||
I18nManager.swapLeftAndRightInRTL(false);
|
I18nManager.swapLeftAndRightInRTL(false);
|
||||||
|
|
||||||
// How app lock works
|
|
||||||
// 1. User goes to settings and setup app lock with a Pin/Password.
|
|
||||||
// 2. The Pin/Password is used to encrypt a random value or user's encryption key.
|
|
||||||
// 3. The encrypted value is stored in MMKV
|
|
||||||
// 4. When the app launches, the same value is decrypted with user provided key, if it works, we launch the app otherwise it remains locked.
|
|
||||||
// 5. If Biometrics are enabled, the app lock pin/password is stored in keychain. the value can be accessed if fingerprint auth works ONLY.
|
|
||||||
// 6. User can manually enter the pin if biometrics fails.
|
|
||||||
// 7. There is no way to enter the app if user forgets the PIN. The only way is to reset app data and start fresh again.
|
|
||||||
|
|
||||||
// How to handle app lock for existing users...
|
|
||||||
// 1.
|
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const init = useAppEvents();
|
const init = useAppEvents();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -20,14 +20,15 @@ import { useThemeColors } from "@notesnook/theme";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { useDBItem } from "../../../hooks/use-db-item";
|
import { useDBItem } from "../../../hooks/use-db-item";
|
||||||
|
import { useTabStore } from "../../../screens/editor/tiptap/use-tab-store";
|
||||||
|
import { editorController } from "../../../screens/editor/tiptap/utils";
|
||||||
import { presentSheet } from "../../../services/event-manager";
|
import { presentSheet } from "../../../services/event-manager";
|
||||||
import { SIZE } from "../../../utils/size";
|
import { SIZE } from "../../../utils/size";
|
||||||
import { Button } from "../../ui/button";
|
import { Button } from "../../ui/button";
|
||||||
import { IconButton } from "../../ui/icon-button";
|
import { IconButton } from "../../ui/icon-button";
|
||||||
import { PressableButton } from "../../ui/pressable";
|
import { PressableButton } from "../../ui/pressable";
|
||||||
import Paragraph from "../../ui/typography/paragraph";
|
|
||||||
import Heading from "../../ui/typography/heading";
|
import Heading from "../../ui/typography/heading";
|
||||||
import { useTabStore } from "../../../screens/editor/tiptap/use-tab-store";
|
import Paragraph from "../../ui/typography/paragraph";
|
||||||
|
|
||||||
type TabItem = {
|
type TabItem = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -55,6 +56,7 @@ const TabItemComponent = (props: {
|
|||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (!props.isFocused) {
|
if (!props.isFocused) {
|
||||||
useTabStore.getState().focusTab(props.tab.id);
|
useTabStore.getState().focusTab(props.tab.id);
|
||||||
|
props.close?.();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -82,7 +84,13 @@ const TabItemComponent = (props: {
|
|||||||
size={SIZE.lg}
|
size={SIZE.lg}
|
||||||
color={colors.primary.icon}
|
color={colors.primary.icon}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
|
const isLastTab = useTabStore.getState().tabs.length === 1;
|
||||||
useTabStore.getState().removeTab(props.tab.id);
|
useTabStore.getState().removeTab(props.tab.id);
|
||||||
|
// The last tab is not actually removed, it is just cleaned up.
|
||||||
|
if (isLastTab) {
|
||||||
|
editorController.current?.reset(props.tab.id, true, true);
|
||||||
|
props.close?.();
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
top={0}
|
top={0}
|
||||||
left={0}
|
left={0}
|
||||||
@@ -121,7 +129,10 @@ export default function EditorTabs({
|
|||||||
<Heading size={SIZE.lg}>Tabs</Heading>
|
<Heading size={SIZE.lg}>Tabs</Heading>
|
||||||
<Button
|
<Button
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
useTabStore.getState().newTab();
|
close?.();
|
||||||
|
setTimeout(() => {
|
||||||
|
useTabStore.getState().newTab();
|
||||||
|
}, 300);
|
||||||
}}
|
}}
|
||||||
title="New tab"
|
title="New tab"
|
||||||
icon="plus"
|
icon="plus"
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import React, {
|
|||||||
RefObject,
|
RefObject,
|
||||||
useEffect,
|
useEffect,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState
|
useState
|
||||||
} from "react";
|
} from "react";
|
||||||
@@ -36,7 +37,7 @@ import Animated, {
|
|||||||
WithSpringConfig,
|
WithSpringConfig,
|
||||||
withTiming
|
withTiming
|
||||||
} from "react-native-reanimated";
|
} from "react-native-reanimated";
|
||||||
import { editorState } from "../../screens/editor/tiptap/utils";
|
import { getAppState } from "../../screens/editor/tiptap/utils";
|
||||||
import { eSendEvent } from "../../services/event-manager";
|
import { eSendEvent } from "../../services/event-manager";
|
||||||
import { useSettingStore } from "../../stores/use-setting-store";
|
import { useSettingStore } from "../../stores/use-setting-store";
|
||||||
import { eClearEditor } from "../../utils/events";
|
import { eClearEditor } from "../../utils/events";
|
||||||
@@ -76,12 +77,19 @@ export const FluidTabs = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
|||||||
}: TabProps,
|
}: TabProps,
|
||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
|
const appState = useMemo(() => getAppState(), []);
|
||||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||||
const fullscreen = useSettingStore((state) => state.fullscreen);
|
const fullscreen = useSettingStore((state) => state.fullscreen);
|
||||||
const introCompleted = useSettingStore(
|
const introCompleted = useSettingStore(
|
||||||
(state) => state.settings.introCompleted
|
(state) => state.settings.introCompleted
|
||||||
);
|
);
|
||||||
const translateX = useSharedValue(widths ? widths.a : 0);
|
const translateX = useSharedValue(
|
||||||
|
widths
|
||||||
|
? appState && !appState?.movedAway
|
||||||
|
? widths.a + widths.b
|
||||||
|
: widths.a
|
||||||
|
: 0
|
||||||
|
);
|
||||||
const startX = useSharedValue(0);
|
const startX = useSharedValue(0);
|
||||||
const currentTab = useSharedValue(1);
|
const currentTab = useSharedValue(1);
|
||||||
const previousTab = useSharedValue(1);
|
const previousTab = useSharedValue(1);
|
||||||
@@ -112,9 +120,8 @@ export const FluidTabs = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
|||||||
translateX.value = 0;
|
translateX.value = 0;
|
||||||
} else {
|
} else {
|
||||||
if (prevWidths.current?.a !== widths.a) {
|
if (prevWidths.current?.a !== widths.a) {
|
||||||
translateX.value = editorState().movedAway
|
translateX.value =
|
||||||
? widths.a
|
!appState || appState?.movedAway ? widths.a : editorPosition;
|
||||||
: editorPosition;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isLoaded.current = true;
|
isLoaded.current = true;
|
||||||
@@ -142,7 +149,8 @@ export const FluidTabs = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
|||||||
isDrawerOpen,
|
isDrawerOpen,
|
||||||
homePosition,
|
homePosition,
|
||||||
onDrawerStateChange,
|
onDrawerStateChange,
|
||||||
editorPosition
|
editorPosition,
|
||||||
|
appState
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useImperativeHandle(
|
useImperativeHandle(
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ class Commands {
|
|||||||
`
|
`
|
||||||
const editor = editors[${tabId}];
|
const editor = editors[${tabId}];
|
||||||
const editorTitle = editorTitles[${tabId}];
|
const editorTitle = editorTitles[${tabId}];
|
||||||
editor && editor.commands.blur();
|
typeof editor !== "undefined" && editor.commands.blur();
|
||||||
typeof editorTitle !== "undefined" && editorTitle.current && editorTitle.current.blur();
|
typeof editorTitle !== "undefined" && editorTitle.current && editorTitle.current.blur();
|
||||||
`,
|
`,
|
||||||
"blur"
|
"blur"
|
||||||
@@ -110,7 +110,10 @@ const editorController = editorControllers[${tabId}];
|
|||||||
const editorTitle = editorTitles[${tabId}];
|
const editorTitle = editorTitles[${tabId}];
|
||||||
const statusBar = statusBars[${tabId}];
|
const statusBar = statusBars[${tabId}];
|
||||||
|
|
||||||
editor.commands.blur();
|
if (typeof editor !== "undefined") {
|
||||||
|
editor.commands.blur();
|
||||||
|
}
|
||||||
|
|
||||||
typeof editorTitle !== "undefined" && editorTitle.current && editorTitle.current?.blur();
|
typeof editorTitle !== "undefined" && editorTitle.current && editorTitle.current?.blur();
|
||||||
if (editorController.content) editorController.content.current = null;
|
if (editorController.content) editorController.content.current = null;
|
||||||
editorController.onUpdate();
|
editorController.onUpdate();
|
||||||
|
|||||||
@@ -531,7 +531,6 @@ export const useEditorEvents = (
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case EventTypes.tabFocused: {
|
case EventTypes.tabFocused: {
|
||||||
// Reload the note
|
|
||||||
console.log(
|
console.log(
|
||||||
"Focused tab",
|
"Focused tab",
|
||||||
editorMessage.tabId,
|
editorMessage.tabId,
|
||||||
@@ -542,15 +541,32 @@ export const useEditorEvents = (
|
|||||||
|
|
||||||
eSendEvent(eEditorTabFocused, editorMessage.tabId);
|
eSendEvent(eEditorTabFocused, editorMessage.tabId);
|
||||||
if (!editorMessage.value && editorMessage.noteId) {
|
if (!editorMessage.value && editorMessage.noteId) {
|
||||||
const note = await db.notes.note(editorMessage.noteId);
|
if (!useSettingStore.getState().isAppLoading) {
|
||||||
if (note) {
|
const note = await db.notes.note(editorMessage.noteId);
|
||||||
eSendEvent(eOnLoadNote, {
|
if (note) {
|
||||||
item: note,
|
eSendEvent(eOnLoadNote, {
|
||||||
forced: true,
|
item: note,
|
||||||
tabId: editorMessage.tabId
|
forced: true,
|
||||||
|
tabId: editorMessage.tabId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const unsub = useSettingStore.subscribe(async (state) => {
|
||||||
|
if (!state.isAppLoading) {
|
||||||
|
unsub();
|
||||||
|
const note = await db.notes.note(editorMessage.noteId);
|
||||||
|
if (note) {
|
||||||
|
eSendEvent(eOnLoadNote, {
|
||||||
|
item: note,
|
||||||
|
forced: true,
|
||||||
|
tabId: editorMessage.tabId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ import {
|
|||||||
import Navigation from "../../../services/navigation";
|
import Navigation from "../../../services/navigation";
|
||||||
import Notifications from "../../../services/notifications";
|
import Notifications from "../../../services/notifications";
|
||||||
import SettingsService from "../../../services/settings";
|
import SettingsService from "../../../services/settings";
|
||||||
import { TipManager } from "../../../services/tip-manager";
|
|
||||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||||
import { useTagStore } from "../../../stores/use-tag-store";
|
import { useTagStore } from "../../../stores/use-tag-store";
|
||||||
import {
|
import {
|
||||||
@@ -67,8 +66,7 @@ import {
|
|||||||
getAppState,
|
getAppState,
|
||||||
isContentInvalid,
|
isContentInvalid,
|
||||||
isEditorLoaded,
|
isEditorLoaded,
|
||||||
post,
|
post
|
||||||
waitForEvent
|
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
|
|
||||||
// Keep a fixed session id, dont' change it when a new note is opened, session id can stay the same always I think once the app is opened. DONE
|
// Keep a fixed session id, dont' change it when a new note is opened, session id can stay the same always I think once the app is opened. DONE
|
||||||
@@ -78,9 +76,20 @@ import {
|
|||||||
// the useEditor hook can recieve save messages for different notes at a time. DONE
|
// the useEditor hook can recieve save messages for different notes at a time. DONE
|
||||||
// When a note is created, the useEditor hook must immediately notify the editor with the note id and set the note id in the editor tabs store
|
// When a note is created, the useEditor hook must immediately notify the editor with the note id and set the note id in the editor tabs store
|
||||||
// so further changes will go into that note. DONE
|
// so further changes will go into that note. DONE
|
||||||
// Events sent to editor have the tab id value added to ensure the correct tab will recieve and return events only.
|
// Events sent to editor have the tab id value added to ensure the correct tab will recieve and return events only. DONE
|
||||||
// The useEditorEvents hook can manage events from different tabs at the same time as long as the attached session id matches. DONE
|
// The useEditorEvents hook can manage events from different tabs at the same time as long as the attached session id matches. DONE
|
||||||
// useEditor hook will keep historySessionId for different notes instead of a single note. DONE
|
// useEditor hook will keep historySessionId for different notes instead of a single note. DONE
|
||||||
|
//
|
||||||
|
// LIST OF CASES TO VERIFY WITH TABS OPENING & CLOSING
|
||||||
|
// 1. SWITCHING TAB CLOSES THE SHEET. DONE
|
||||||
|
// 2. Closing the tab does proper cleanup if it's the last tab and is not empty. DONE
|
||||||
|
// 3. Swiping left only focuses editor if current tab is empty. DONE
|
||||||
|
// 4. Pressing + button will open a new tab for new note if an empty tab does not exist.
|
||||||
|
// 5. Notes will always open in the preview tab.
|
||||||
|
// 6. If a note is edited, the tab will become persisted.
|
||||||
|
// 7. If note is already opened in a tab, we focus that tab.
|
||||||
|
// 8. If app is killed, restore the note in background.
|
||||||
|
// 9. During realtimes sync, tabs not focused will be updated so if focused, they have the latest and updated content loaded.
|
||||||
|
|
||||||
export const useEditor = (
|
export const useEditor = (
|
||||||
editorId = "",
|
editorId = "",
|
||||||
@@ -188,18 +197,17 @@ export const useEditor = (
|
|||||||
|
|
||||||
const reset = useCallback(
|
const reset = useCallback(
|
||||||
async (tabId: number, resetState = true, resetContent = true) => {
|
async (tabId: number, resetState = true, resetContent = true) => {
|
||||||
|
console.log("Resetting tab:", tabId);
|
||||||
const noteId = useTabStore.getState().getNoteIdForTab(tabId);
|
const noteId = useTabStore.getState().getNoteIdForTab(tabId);
|
||||||
if (noteId) {
|
if (noteId) {
|
||||||
currentNotes.current?.id && db.fs().cancel(noteId, "download");
|
currentNotes.current?.id && db.fs().cancel(noteId, "download");
|
||||||
|
|
||||||
currentNotes.current[noteId] = null;
|
currentNotes.current[noteId] = null;
|
||||||
currentContents.current[noteId] = null;
|
currentContents.current[noteId] = null;
|
||||||
editorSessionHistory.clearSession(noteId);
|
editorSessionHistory.clearSession(noteId);
|
||||||
lastContentChangeTime.current[noteId] = 0;
|
lastContentChangeTime.current[noteId] = 0;
|
||||||
|
clearTimeout(timers.current["loading-images" + noteId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearTimeout(timers.current["loading-images" + noteId]);
|
|
||||||
|
|
||||||
saveCount.current = 0;
|
saveCount.current = 0;
|
||||||
loadingState.current = undefined;
|
loadingState.current = undefined;
|
||||||
lock.current = false;
|
lock.current = false;
|
||||||
@@ -224,7 +232,7 @@ export const useEditor = (
|
|||||||
sessionHistoryId: currentSessionHistoryId,
|
sessionHistoryId: currentSessionHistoryId,
|
||||||
tabId
|
tabId
|
||||||
}: SavePayload) => {
|
}: SavePayload) => {
|
||||||
if (currentNotes.current[id as string]?.readonly) return;
|
if (currentNotes.current[id as string]?.readonly || readonly) return;
|
||||||
try {
|
try {
|
||||||
if (id && !(await db.notes?.note(id))) {
|
if (id && !(await db.notes?.note(id))) {
|
||||||
await reset(tabId);
|
await reset(tabId);
|
||||||
@@ -337,7 +345,7 @@ export const useEditor = (
|
|||||||
DatabaseLogger.error(e as Error);
|
DatabaseLogger.error(e as Error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[commands, editorSessionHistory, postMessage, reset]
|
[commands, editorSessionHistory, postMessage, readonly, reset]
|
||||||
);
|
);
|
||||||
|
|
||||||
const loadContent = useCallback(
|
const loadContent = useCallback(
|
||||||
@@ -406,7 +414,7 @@ export const useEditor = (
|
|||||||
const tabId = useTabStore.getState().getTabForNote(event.item.id);
|
const tabId = useTabStore.getState().getTabForNote(event.item.id);
|
||||||
if (typeof tabId === "number") {
|
if (typeof tabId === "number") {
|
||||||
useTabStore.getState().updateTab(tabId, {
|
useTabStore.getState().updateTab(tabId, {
|
||||||
readonly: event.item.readonly
|
readonly: event.item.readonly || readonly
|
||||||
});
|
});
|
||||||
useTabStore.getState().focusTab(tabId);
|
useTabStore.getState().focusTab(tabId);
|
||||||
}
|
}
|
||||||
@@ -415,7 +423,7 @@ export const useEditor = (
|
|||||||
console.log("Opening note in preview tab");
|
console.log("Opening note in preview tab");
|
||||||
// Otherwise we focus the preview tab or create one to open the note in.
|
// Otherwise we focus the preview tab or create one to open the note in.
|
||||||
useTabStore.getState().focusPreviewTab(event.item.id, {
|
useTabStore.getState().focusPreviewTab(event.item.id, {
|
||||||
readonly: event.item.readonly,
|
readonly: event.item.readonly || readonly,
|
||||||
locked: false
|
locked: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -472,15 +480,14 @@ export const useEditor = (
|
|||||||
|
|
||||||
await postMessage(EditorEvents.title, item.title, tabId);
|
await postMessage(EditorEvents.title, item.title, tabId);
|
||||||
loadingState.current = currentContents.current[item.id]?.data;
|
loadingState.current = currentContents.current[item.id]?.data;
|
||||||
if (currentContents.current[item.id]?.data) {
|
|
||||||
console.log("loading content for note...");
|
await postMessage(
|
||||||
await postMessage(
|
EditorEvents.html,
|
||||||
EditorEvents.html,
|
currentContents.current[item.id]?.data || "",
|
||||||
currentContents.current[item.id]?.data,
|
tabId,
|
||||||
tabId,
|
10000
|
||||||
10000
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
loadingState.current = undefined;
|
loadingState.current = undefined;
|
||||||
await commands.setTags(item);
|
await commands.setTags(item);
|
||||||
commands.setSettings();
|
commands.setSettings();
|
||||||
@@ -491,7 +498,7 @@ export const useEditor = (
|
|||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[commands, editorSessionHistory, loadContent, postMessage, reset]
|
[commands, editorSessionHistory, loadContent, postMessage, readonly, reset]
|
||||||
);
|
);
|
||||||
|
|
||||||
const lockNoteWithVault = useCallback((note: Note) => {
|
const lockNoteWithVault = useCallback((note: Note) => {
|
||||||
@@ -702,15 +709,13 @@ export const useEditor = (
|
|||||||
if (!state.current.ready && (await onReady())) {
|
if (!state.current.ready && (await onReady())) {
|
||||||
state.current.ready = true;
|
state.current.ready = true;
|
||||||
}
|
}
|
||||||
overlay(false);
|
setTimeout(() => overlay(false), 300);
|
||||||
|
|
||||||
const noteId = useTabStore.getState().getCurrentNoteId();
|
const noteId = useTabStore.getState().getCurrentNoteId();
|
||||||
async function restoreTabNote() {
|
async function restoreTabNote() {
|
||||||
if (!noteId) return;
|
if (!noteId) return;
|
||||||
const note = await db.notes.note(noteId);
|
const note = await db.notes.note(noteId);
|
||||||
if (note) {
|
if (!note) {
|
||||||
loadNote({ item: note, forced: true });
|
|
||||||
} else {
|
|
||||||
console.log("Editor loaded with blank note");
|
console.log("Editor loaded with blank note");
|
||||||
loadNote({ newNote: true });
|
loadNote({ newNote: true });
|
||||||
if (tabBarRef.current?.page === 1) {
|
if (tabBarRef.current?.page === 1) {
|
||||||
|
|||||||
@@ -147,16 +147,22 @@ export function isContentInvalid(content: string | undefined) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canRestoreAppState = (appState: AppState) => {
|
||||||
|
return (
|
||||||
|
appState.editing &&
|
||||||
|
!appState.note?.locked &&
|
||||||
|
appState.note?.id &&
|
||||||
|
Date.now() < appState.timestamp + 3600000
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
let appState: AppState | undefined;
|
||||||
export function getAppState() {
|
export function getAppState() {
|
||||||
|
if (appState && canRestoreAppState(appState)) return appState as AppState;
|
||||||
const json = MMKV.getString("appState");
|
const json = MMKV.getString("appState");
|
||||||
if (json) {
|
if (json) {
|
||||||
const appState = JSON.parse(json) as AppState;
|
appState = JSON.parse(json) as AppState;
|
||||||
if (
|
if (canRestoreAppState(appState)) {
|
||||||
appState.editing &&
|
|
||||||
!appState.note?.locked &&
|
|
||||||
appState.note?.id &&
|
|
||||||
Date.now() < appState.timestamp + 3600000
|
|
||||||
) {
|
|
||||||
return appState;
|
return appState;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
@@ -166,5 +172,6 @@ export function getAppState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function clearAppState() {
|
export function clearAppState() {
|
||||||
|
appState = undefined;
|
||||||
MMKV.removeItem("appState");
|
MMKV.removeItem("appState");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,12 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import { useEditorController } from "../hooks/useEditorController";
|
import { useEditorController } from "../hooks/useEditorController";
|
||||||
import { useSettings } from "../hooks/useSettings";
|
import { useSettings } from "../hooks/useSettings";
|
||||||
import { TabStore, useTabContext, useTabStore } from "../hooks/useTabStore";
|
import {
|
||||||
|
TabItem,
|
||||||
|
TabStore,
|
||||||
|
useTabContext,
|
||||||
|
useTabStore
|
||||||
|
} from "../hooks/useTabStore";
|
||||||
import { EmotionEditorToolbarTheme } from "../theme-factory";
|
import { EmotionEditorToolbarTheme } from "../theme-factory";
|
||||||
import { EventTypes, Settings } from "../utils";
|
import { EventTypes, Settings } from "../utils";
|
||||||
import Header from "./header";
|
import Header from "./header";
|
||||||
@@ -48,12 +53,18 @@ import Title from "./title";
|
|||||||
globalThis.toBlobURL = toBlobURL as typeof globalThis.toBlobURL;
|
globalThis.toBlobURL = toBlobURL as typeof globalThis.toBlobURL;
|
||||||
|
|
||||||
const Tiptap = ({ settings }: { settings: Settings }) => {
|
const Tiptap = ({ settings }: { settings: Settings }) => {
|
||||||
|
const { colors } = useThemeColors();
|
||||||
const tab = useTabContext();
|
const tab = useTabContext();
|
||||||
const isFocused = useTabStore((state) => state.currentTab === tab?.id);
|
const isFocused = useTabStore((state) => state.currentTab === tab?.id);
|
||||||
const [tick, setTick] = useState(0);
|
const [tick, setTick] = useState(0);
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [layout, setLayout] = useState(false);
|
const [layout, setLayout] = useState(false);
|
||||||
|
const noteStateUpdateTimer = useRef<NodeJS.Timeout>();
|
||||||
|
const tabRef = useRef<TabItem>(tab);
|
||||||
|
const isFocusedRef = useRef<boolean>(false);
|
||||||
|
tabRef.current = tab;
|
||||||
|
|
||||||
usePermissionHandler({
|
usePermissionHandler({
|
||||||
claims: {
|
claims: {
|
||||||
premium: settings.premium
|
premium: settings.premium
|
||||||
@@ -102,6 +113,21 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
|
|||||||
copyToClipboard: (text) => {
|
copyToClipboard: (text) => {
|
||||||
globalThis.editorControllers[tab.id]?.copyToClipboard(text);
|
globalThis.editorControllers[tab.id]?.copyToClipboard(text);
|
||||||
},
|
},
|
||||||
|
onSelectionUpdate: () => {
|
||||||
|
if (tab.noteId) {
|
||||||
|
const noteId = tab.noteId;
|
||||||
|
clearTimeout(noteStateUpdateTimer.current);
|
||||||
|
noteStateUpdateTimer.current = setTimeout(() => {
|
||||||
|
if (tab.noteId !== noteId) return;
|
||||||
|
const { to, from } =
|
||||||
|
editors[tabRef.current?.id]?.state.selection || {};
|
||||||
|
useTabStore.getState().setNoteState(noteId, {
|
||||||
|
to,
|
||||||
|
from
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
},
|
||||||
downloadOptions: {
|
downloadOptions: {
|
||||||
corsHost: settings.corsProxy
|
corsHost: settings.corsProxy
|
||||||
},
|
},
|
||||||
@@ -120,12 +146,33 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
|
|||||||
|
|
||||||
const update = useCallback(() => {
|
const update = useCallback(() => {
|
||||||
setTick((tick) => tick + 1);
|
setTick((tick) => tick + 1);
|
||||||
containerRef.current?.scrollTo?.({
|
setTimeout(() => {
|
||||||
left: 0,
|
const noteState = tabRef.current.noteId
|
||||||
top: 0
|
? useTabStore.getState().noteState[tabRef.current.noteId]
|
||||||
|
: undefined;
|
||||||
|
const top = noteState?.top;
|
||||||
|
|
||||||
|
if (noteState?.to || noteState?.from) {
|
||||||
|
editors[tabRef.current.id]?.chain().setTextSelection({
|
||||||
|
to: noteState.to,
|
||||||
|
from: noteState.from
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
containerRef.current?.scrollTo({
|
||||||
|
left: 0,
|
||||||
|
top: top || 0,
|
||||||
|
behavior: "auto"
|
||||||
|
});
|
||||||
|
}, 32);
|
||||||
|
|
||||||
|
globalThis.editorControllers[tabRef.current.id]?.setTitlePlaceholder(
|
||||||
|
"Note title"
|
||||||
|
);
|
||||||
|
setTimeout(() => {
|
||||||
|
editorControllers[tabRef.current.id]?.setLoading(false);
|
||||||
});
|
});
|
||||||
globalThis.editorControllers[tab.id]?.setTitlePlaceholder("Note title");
|
}, []);
|
||||||
}, [tab.id]);
|
|
||||||
|
|
||||||
const controller = useEditorController(update);
|
const controller = useEditorController(update);
|
||||||
const controllerRef = useRef(controller);
|
const controllerRef = useRef(controller);
|
||||||
@@ -136,33 +183,57 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
|
|||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
setLayout(true);
|
setLayout(true);
|
||||||
const updateScrollPosition = (state: TabStore) => {
|
const updateScrollPosition = (state: TabStore) => {
|
||||||
if (state.currentTab === tab.id) {
|
if (isFocusedRef.current) return;
|
||||||
const position = state.scrollPosition[tab?.id];
|
if (state.currentTab === tabRef.current.id) {
|
||||||
if (position) {
|
isFocusedRef.current = true;
|
||||||
|
const noteState = tabRef.current.noteId
|
||||||
|
? state.noteState[tabRef.current.noteId]
|
||||||
|
: undefined;
|
||||||
|
if (noteState) {
|
||||||
containerRef.current?.scrollTo({
|
containerRef.current?.scrollTo({
|
||||||
left: 0,
|
left: 0,
|
||||||
top: position,
|
top: noteState.top,
|
||||||
behavior: "auto"
|
behavior: "auto"
|
||||||
});
|
});
|
||||||
|
if (noteState.to || noteState.from) {
|
||||||
|
editors[tabRef.current.id]?.chain().setTextSelection({
|
||||||
|
to: noteState.to,
|
||||||
|
from: noteState.from
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!globalThis.editorControllers[tabRef.current.id]?.content.current &&
|
||||||
|
tabRef.current.noteId
|
||||||
|
) {
|
||||||
|
editorControllers[tabRef.current.id]?.setLoading(true);
|
||||||
|
}
|
||||||
|
|
||||||
post(
|
post(
|
||||||
EventTypes.tabFocused,
|
EventTypes.tabFocused,
|
||||||
!!globalThis.editorControllers[tab.id]?.content.current,
|
!!globalThis.editorControllers[tabRef.current.id]?.content.current,
|
||||||
tab.id,
|
tabRef.current.id,
|
||||||
state.getCurrentNoteId()
|
state.getCurrentNoteId()
|
||||||
);
|
);
|
||||||
|
editorControllers[tabRef.current.id]?.updateTab();
|
||||||
|
} else {
|
||||||
|
isFocusedRef.current = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
updateScrollPosition(useTabStore.getState());
|
updateScrollPosition(useTabStore.getState());
|
||||||
const unsub = useTabStore.subscribe((state, prevState) => {
|
const unsub = useTabStore.subscribe((state, prevState) => {
|
||||||
|
if (state.currentTab !== tabRef.current.id) {
|
||||||
|
isFocusedRef.current = false;
|
||||||
|
}
|
||||||
if (state.currentTab === prevState.currentTab) return;
|
if (state.currentTab === prevState.currentTab) return;
|
||||||
updateScrollPosition(state);
|
updateScrollPosition(state);
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
unsub();
|
unsub();
|
||||||
};
|
};
|
||||||
}, [tab.id]);
|
}, []);
|
||||||
|
|
||||||
const onClickEmptyArea: React.MouseEventHandler<HTMLDivElement> = useCallback(
|
const onClickEmptyArea: React.MouseEventHandler<HTMLDivElement> = useCallback(
|
||||||
(event) => {
|
(event) => {
|
||||||
@@ -244,6 +315,7 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
|
|||||||
settings={settings}
|
settings={settings}
|
||||||
noHeader={settings.noHeader || false}
|
noHeader={settings.noHeader || false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
onScroll={controller.scroll}
|
onScroll={controller.scroll}
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
@@ -269,6 +341,52 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{controller.loading ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
position: "absolute",
|
||||||
|
zIndex: 999,
|
||||||
|
backgroundColor: "white",
|
||||||
|
paddingRight: 12,
|
||||||
|
paddingLeft: 12,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: 16,
|
||||||
|
width: "94%",
|
||||||
|
backgroundColor: colors.secondary.background,
|
||||||
|
borderRadius: 5,
|
||||||
|
marginTop: 10
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: 16,
|
||||||
|
width: "94%",
|
||||||
|
backgroundColor: colors.secondary.background,
|
||||||
|
borderRadius: 5,
|
||||||
|
marginTop: 10
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: 16,
|
||||||
|
width: 200,
|
||||||
|
backgroundColor: colors.secondary.background,
|
||||||
|
borderRadius: 5,
|
||||||
|
marginTop: 10
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<ContentDiv
|
<ContentDiv
|
||||||
padding={settings.doubleSpacedLines ? 0 : 6}
|
padding={settings.doubleSpacedLines ? 0 : 6}
|
||||||
fontSize={settings.fontSize}
|
fontSize={settings.fontSize}
|
||||||
|
|||||||
@@ -104,21 +104,30 @@ export type EditorController = {
|
|||||||
countWords: (ms: number) => void;
|
countWords: (ms: number) => void;
|
||||||
copyToClipboard: (text: string) => void;
|
copyToClipboard: (text: string) => void;
|
||||||
getAttachmentData: (attachment: Attachment) => Promise<string>;
|
getAttachmentData: (attachment: Attachment) => Promise<string>;
|
||||||
|
updateTab: () => void;
|
||||||
|
loading: boolean;
|
||||||
|
setLoading: (value: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useEditorController(update: () => void): EditorController {
|
export function useEditorController(update: () => void): EditorController {
|
||||||
const tab = useTabContext();
|
const tab = useTabContext();
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
const setTheme = useThemeEngineStore((store) => store.setTheme);
|
const setTheme = useThemeEngineStore((store) => store.setTheme);
|
||||||
const { colors } = useThemeColors("editor");
|
const { colors } = useThemeColors("editor");
|
||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState("");
|
||||||
const [titlePlaceholder, setTitlePlaceholder] = useState("Note title");
|
const [titlePlaceholder, setTitlePlaceholder] = useState("Note title");
|
||||||
const htmlContentRef = useRef<string | null>(null);
|
const htmlContentRef = useRef<string | null>(null);
|
||||||
|
const updateTabOnFocus = useRef(false);
|
||||||
const timers = useRef<Timers>({
|
const timers = useRef<Timers>({
|
||||||
selectionChange: null,
|
selectionChange: null,
|
||||||
change: null,
|
change: null,
|
||||||
wordCounter: null
|
wordCounter: null
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!tab.noteId && loading) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
const selectionChange = useCallback((_editor: Editor) => {}, []);
|
const selectionChange = useCallback((_editor: Editor) => {}, []);
|
||||||
|
|
||||||
const titleChange = useCallback(
|
const titleChange = useCallback(
|
||||||
@@ -176,9 +185,11 @@ export function useEditorController(update: () => void): EditorController {
|
|||||||
const scroll = useCallback(
|
const scroll = useCallback(
|
||||||
(_event: React.UIEvent<HTMLDivElement, UIEvent>) => {
|
(_event: React.UIEvent<HTMLDivElement, UIEvent>) => {
|
||||||
if (!tab) return;
|
if (!tab) return;
|
||||||
useTabStore
|
if (tab.noteId) {
|
||||||
.getState()
|
useTabStore.getState().setNoteState(tab.noteId, {
|
||||||
.setScrollPosition(tab.id, _event.currentTarget.scrollTop);
|
top: _event.currentTarget.scrollTop
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[tab]
|
[tab]
|
||||||
);
|
);
|
||||||
@@ -212,25 +223,30 @@ export function useEditorController(update: () => void): EditorController {
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case "native:updatehtml": {
|
case "native:updatehtml": {
|
||||||
htmlContentRef.current = value;
|
htmlContentRef.current = value;
|
||||||
if (!editor) break;
|
if (tab.id !== useTabStore.getState().currentTab) {
|
||||||
const { from, to } = editor.state.selection;
|
updateTabOnFocus.current = true;
|
||||||
|
} else {
|
||||||
|
if (!editor) break;
|
||||||
|
const { from, to } = editor.state.selection;
|
||||||
|
editor?.commands.setContent(htmlContentRef.current, false, {
|
||||||
|
preserveWhitespace: true
|
||||||
|
});
|
||||||
|
|
||||||
editor?.commands.setContent(htmlContentRef.current, false, {
|
editor.commands.setTextSelection({
|
||||||
preserveWhitespace: true
|
from,
|
||||||
});
|
to
|
||||||
|
});
|
||||||
|
countWords(0);
|
||||||
|
}
|
||||||
|
|
||||||
editor.commands.setTextSelection({
|
|
||||||
from,
|
|
||||||
to
|
|
||||||
});
|
|
||||||
countWords();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "native:html":
|
case "native:html":
|
||||||
// logger("info", "loading html", htmlContentRef.current);
|
// logger("info", "loading html", htmlContentRef.current);
|
||||||
htmlContentRef.current = value;
|
htmlContentRef.current = value;
|
||||||
|
if (!editor) break;
|
||||||
update();
|
update();
|
||||||
countWords();
|
countWords(0);
|
||||||
break;
|
break;
|
||||||
case "native:theme":
|
case "native:theme":
|
||||||
setTheme(message.value);
|
setTheme(message.value);
|
||||||
@@ -329,6 +345,8 @@ export function useEditorController(update: () => void): EditorController {
|
|||||||
selectionChange,
|
selectionChange,
|
||||||
titleChange,
|
titleChange,
|
||||||
scroll,
|
scroll,
|
||||||
|
loading,
|
||||||
|
setLoading,
|
||||||
title,
|
title,
|
||||||
setTitle,
|
setTitle,
|
||||||
titlePlaceholder,
|
titlePlaceholder,
|
||||||
@@ -341,6 +359,26 @@ export function useEditorController(update: () => void): EditorController {
|
|||||||
onUpdate: onUpdate,
|
onUpdate: onUpdate,
|
||||||
countWords,
|
countWords,
|
||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
getAttachmentData
|
getAttachmentData,
|
||||||
|
updateTab: () => {
|
||||||
|
// When the tab is focused, we apply any updates to content that were recieved when
|
||||||
|
// the tab was not focused.
|
||||||
|
updateTabOnFocus.current = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!updateTabOnFocus.current) return;
|
||||||
|
const editor = editors[tab.id];
|
||||||
|
if (!editor) return;
|
||||||
|
const { from, to } = editor.state.selection;
|
||||||
|
editor?.commands.setContent(htmlContentRef.current, false, {
|
||||||
|
preserveWhitespace: true
|
||||||
|
});
|
||||||
|
editor.commands.setTextSelection({
|
||||||
|
from,
|
||||||
|
to
|
||||||
|
});
|
||||||
|
countWords();
|
||||||
|
logger("info", `Tab ${tab.id} updated.`);
|
||||||
|
}, 1);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,10 +33,17 @@ export type TabItem = {
|
|||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type NoteState = {
|
||||||
|
top: number;
|
||||||
|
to: number;
|
||||||
|
from: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type TabStore = {
|
export type TabStore = {
|
||||||
tabs: TabItem[];
|
tabs: TabItem[];
|
||||||
currentTab: number;
|
currentTab: number;
|
||||||
scrollPosition: Record<number, number>;
|
scrollPosition: Record<number, number>;
|
||||||
|
noteState: Record<string, NoteState>;
|
||||||
updateTab: (id: number, options: Omit<Partial<TabItem>, "id">) => void;
|
updateTab: (id: number, options: Omit<Partial<TabItem>, "id">) => void;
|
||||||
removeTab: (index: number) => void;
|
removeTab: (index: number) => void;
|
||||||
moveTab: (index: number, toIndex: number) => void;
|
moveTab: (index: number, toIndex: number) => void;
|
||||||
@@ -53,6 +60,7 @@ export type TabStore = {
|
|||||||
) => void;
|
) => void;
|
||||||
getCurrentNoteId: () => string | undefined;
|
getCurrentNoteId: () => string | undefined;
|
||||||
getTab: (tabId: number) => TabItem | undefined;
|
getTab: (tabId: number) => TabItem | undefined;
|
||||||
|
setNoteState: (noteId: string, state: Partial<NoteState>) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getId(id: number, tabs: TabItem[]): number {
|
function getId(id: number, tabs: TabItem[]): number {
|
||||||
@@ -66,6 +74,7 @@ function getId(id: number, tabs: TabItem[]): number {
|
|||||||
export const useTabStore = create(
|
export const useTabStore = create(
|
||||||
persist<TabStore>(
|
persist<TabStore>(
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
|
noteState: {},
|
||||||
tabs: [
|
tabs: [
|
||||||
{
|
{
|
||||||
id: 0,
|
id: 0,
|
||||||
@@ -74,6 +83,18 @@ export const useTabStore = create(
|
|||||||
],
|
],
|
||||||
currentTab: 0,
|
currentTab: 0,
|
||||||
scrollPosition: {},
|
scrollPosition: {},
|
||||||
|
setNoteState: (noteId: string, state: Partial<NoteState>) => {
|
||||||
|
const noteState = {
|
||||||
|
...get().noteState
|
||||||
|
};
|
||||||
|
noteState[noteId] = {
|
||||||
|
...get().noteState[noteId],
|
||||||
|
...state
|
||||||
|
};
|
||||||
|
set({
|
||||||
|
noteState
|
||||||
|
});
|
||||||
|
},
|
||||||
updateTab: (id: number, options: Omit<Partial<TabItem>, "id">) => {
|
updateTab: (id: number, options: Omit<Partial<TabItem>, "id">) => {
|
||||||
const index = get().tabs.findIndex((t) => t.id === id);
|
const index = get().tabs.findIndex((t) => t.id === id);
|
||||||
if (index == -1) return;
|
if (index == -1) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user