mobile: some more fixes

This commit is contained in:
Ammar Ahmed
2023-12-25 15:02:34 +05:00
committed by Abdullah Atta
parent ad338aeefc
commit 366948cd28
10 changed files with 305 additions and 90 deletions

View File

@@ -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(() => {

View File

@@ -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"

View File

@@ -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(

View File

@@ -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();

View File

@@ -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;
} }

View File

@@ -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) {

View File

@@ -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");
} }

View File

@@ -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}

View File

@@ -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);
}
}; };
} }

View File

@@ -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;