mobile: fix minor bugs in tabs

This commit is contained in:
Ammar Ahmed
2024-03-08 13:00:10 +05:00
committed by Abdullah Atta
parent 08c03bc3a4
commit 3efe977c49
17 changed files with 520 additions and 301 deletions

View File

@@ -26,7 +26,7 @@ import { eSendEvent, 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 { Pressable } from "../../ui/pressable";
import Heading from "../../ui/typography/heading"; import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph"; import Paragraph from "../../ui/typography/paragraph";
import { eUnlockNote } from "../../../utils/events"; import { eUnlockNote } from "../../../utils/events";
@@ -48,8 +48,8 @@ const TabItemComponent = (props: {
const [item] = useDBItem(props.tab.noteId, "note"); const [item] = useDBItem(props.tab.noteId, "note");
return ( return (
<PressableButton <Pressable
customStyle={{ style={{
alignItems: "center", alignItems: "center",
justifyContent: "space-between", justifyContent: "space-between",
flexDirection: "row", flexDirection: "row",
@@ -114,7 +114,7 @@ const TabItemComponent = (props: {
right={20} right={20}
bottom={0} bottom={0}
/> />
</PressableButton> </Pressable>
); );
}; };

View File

@@ -32,7 +32,7 @@ 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 Input from "../../ui/input"; import Input from "../../ui/input";
import { PressableButton } from "../../ui/pressable"; import { Pressable } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph"; import Paragraph from "../../ui/typography/paragraph";
import type { LinkAttributes } from "@notesnook/editor/dist/extensions/link"; import type { LinkAttributes } from "@notesnook/editor/dist/extensions/link";
import { editorController } from "../../../screens/editor/tiptap/utils"; import { editorController } from "../../../screens/editor/tiptap/utils";
@@ -48,13 +48,13 @@ const ListNoteItem = ({
}) => { }) => {
const [item] = useDBItem(id, "note", items); const [item] = useDBItem(id, "note", items);
return ( return (
<PressableButton <Pressable
onPress={() => { onPress={() => {
if (!item) return; if (!item) return;
onSelectNote(item as Note); onSelectNote(item as Note);
}} }}
type={"transparent"} type={"transparent"}
customStyle={{ style={{
paddingVertical: 12, paddingVertical: 12,
flexDirection: "row", flexDirection: "row",
width: "100%", width: "100%",
@@ -69,7 +69,7 @@ const ListNoteItem = ({
> >
<Paragraph numberOfLines={1}>{item?.title}</Paragraph> <Paragraph numberOfLines={1}>{item?.title}</Paragraph>
</View> </View>
</PressableButton> </Pressable>
); );
}; };
@@ -82,12 +82,12 @@ const ListBlockItem = ({
}) => { }) => {
const { colors } = useThemeColors(); const { colors } = useThemeColors();
return ( return (
<PressableButton <Pressable
onPress={() => { onPress={() => {
onSelectBlock(item); onSelectBlock(item);
}} }}
type={"transparent"} type={"transparent"}
customStyle={{ style={{
flexDirection: "row", flexDirection: "row",
width: "100%", width: "100%",
justifyContent: "flex-start", justifyContent: "flex-start",
@@ -127,7 +127,7 @@ const ListBlockItem = ({
: item.content} : item.content}
</Paragraph> </Paragraph>
</View> </View>
</PressableButton> </Pressable>
); );
}; };
@@ -198,7 +198,7 @@ export default function LinkNote(props: {
setSelectedNote(note); setSelectedNote(note);
inputRef.current?.clear(); inputRef.current?.clear();
setTimeout(async () => { setTimeout(async () => {
nodesRef.current = await db.notes.getBlocks(note.id); nodesRef.current = await db.notes.contentBlocks(note.id);
setNodes(nodesRef.current); setNodes(nodesRef.current);
}); });
// Fetch and set note's nodes. // Fetch and set note's nodes.
@@ -249,13 +249,13 @@ export default function LinkNote(props: {
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}> <Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
SELECTED NOTE SELECTED NOTE
</Paragraph> </Paragraph>
<PressableButton <Pressable
onPress={() => { onPress={() => {
setSelectedNote(undefined); setSelectedNote(undefined);
setSelectedNodeId(undefined); setSelectedNodeId(undefined);
setNodes([]); setNodes([]);
}} }}
customStyle={{ style={{
flexDirection: "row", flexDirection: "row",
width: "100%", width: "100%",
justifyContent: "flex-start", justifyContent: "flex-start",
@@ -264,7 +264,7 @@ export default function LinkNote(props: {
borderColor: colors.primary.accent, borderColor: colors.primary.accent,
paddingHorizontal: 12 paddingHorizontal: 12
}} }}
type="grayAccent" type="secondaryAccented"
> >
<View <View
style={{ style={{
@@ -280,7 +280,7 @@ export default function LinkNote(props: {
Tap to deselect Tap to deselect
</Paragraph> </Paragraph>
</View> </View>
</PressableButton> </Pressable>
{nodes?.length > 0 ? ( {nodes?.length > 0 ? (
<Paragraph <Paragraph

View File

@@ -39,7 +39,7 @@ import { SIZE } from "../../../utils/size";
import SheetProvider from "../../sheet-provider"; import SheetProvider from "../../sheet-provider";
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 { Pressable } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph"; import Paragraph from "../../ui/typography/paragraph";
export const useExpandedStore = create<{ export const useExpandedStore = create<{
@@ -68,12 +68,12 @@ const ListBlockItem = ({
}) => { }) => {
const { colors } = useThemeColors(); const { colors } = useThemeColors();
return ( return (
<PressableButton <Pressable
onPress={() => { onPress={() => {
onSelectBlock(); onSelectBlock();
}} }}
type={"transparent"} type={"transparent"}
customStyle={{ style={{
flexDirection: "row", flexDirection: "row",
width: "100%", width: "100%",
paddingLeft: 35, paddingLeft: 35,
@@ -114,7 +114,7 @@ const ListBlockItem = ({
: item.content} : item.content}
</Paragraph> </Paragraph>
</View> </View>
</PressableButton> </Pressable>
); );
}; };
@@ -130,12 +130,12 @@ const ListNoteInternalLink = ({
}) => { }) => {
const { colors } = useThemeColors(); const { colors } = useThemeColors();
return ( return (
<PressableButton <Pressable
onPress={() => { onPress={() => {
onSelect(); onSelect();
}} }}
type={"transparent"} type={"transparent"}
customStyle={{ style={{
flexDirection: "row", flexDirection: "row",
width: "100%", width: "100%",
paddingLeft: 35, paddingLeft: 35,
@@ -177,7 +177,7 @@ const ListNoteInternalLink = ({
</Paragraph> </Paragraph>
))} ))}
</View> </View>
</PressableButton> </Pressable>
); );
}; };
@@ -306,13 +306,13 @@ const ListNoteItem = ({
alignItems: "center" alignItems: "center"
}} }}
> >
<PressableButton <Pressable
type={"gray"} type={"plain"}
onPress={() => { onPress={() => {
if (!item) return; if (!item) return;
onSelect(item as Note); onSelect(item as Note);
}} }}
customStyle={{ style={{
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
justifyContent: "flex-start", justifyContent: "flex-start",
@@ -330,14 +330,14 @@ const ListNoteItem = ({
left={0} left={0}
bottom={0} bottom={0}
right={0} right={0}
customStyle={{ style={{
width: 35, width: 35,
height: 35 height: 35
}} }}
name={expanded ? "chevron-down" : "chevron-right"} name={expanded ? "chevron-down" : "chevron-right"}
/> />
<Paragraph numberOfLines={1}>{item?.title}</Paragraph> <Paragraph numberOfLines={1}>{item?.title}</Paragraph>
</PressableButton> </Pressable>
{expanded && !item?.locked ? ( {expanded && !item?.locked ? (
<View <View
@@ -441,7 +441,7 @@ export const ReferencesList = ({ item, close }: ReferencesListProps) => {
}} }}
> >
<Button <Button
type={"gray"} type={"plain"}
title="Linked notes" title="Linked notes"
style={{ style={{
borderRadius: 0, borderRadius: 0,
@@ -455,7 +455,7 @@ export const ReferencesList = ({ item, close }: ReferencesListProps) => {
}} }}
/> />
<Button <Button
type={"gray"} type={"plain"}
title="Referenced in" title="Referenced in"
style={{ style={{
width: "50%", width: "50%",

View File

@@ -1,7 +1,25 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme"; import { useThemeColors } from "@notesnook/theme";
import React from "react"; import React from "react";
import { View } from "react-native"; import { View } from "react-native";
import { PressableButton } from "../../ui/pressable"; import { Pressable } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph"; import Paragraph from "../../ui/typography/paragraph";
import { SIZE } from "../../../utils/size"; import { SIZE } from "../../../utils/size";
import { presentSheet } from "../../../services/event-manager"; import { presentSheet } from "../../../services/event-manager";
@@ -33,8 +51,8 @@ const TableOfContentsItem: React.FC<{
const { colors } = useThemeColors(); const { colors } = useThemeColors();
return ( return (
<PressableButton <Pressable
customStyle={{ style={{
alignItems: "center", alignItems: "center",
justifyContent: "space-between", justifyContent: "space-between",
flexDirection: "row", flexDirection: "row",
@@ -72,7 +90,7 @@ const TableOfContentsItem: React.FC<{
{item?.title || "New note"} {item?.title || "New note"}
</Paragraph> </Paragraph>
</View> </View>
</PressableButton> </Pressable>
); );
}; };

View File

@@ -471,13 +471,14 @@ export const useAppEvents = () => {
const noteId = useTabStore.getState().getTab(tab.id)?.noteId; const noteId = useTabStore.getState().getTab(tab.id)?.noteId;
if (!noteId) continue; if (!noteId) continue;
const note = await db.notes.note(noteId); const note = await db.notes.note(noteId);
if (note?.locked) { const locked = note && (await db.vaults.itemExists(note));
if (locked) {
useTabStore.getState().updateTab(tab.id, { useTabStore.getState().updateTab(tab.id, {
locked: true locked: true
}); });
if ( if (
tab.id === useTabStore.getState().currentTab && tab.id === useTabStore.getState().currentTab &&
note.locked && locked &&
!editorState().movedAway !editorState().movedAway
) { ) {
// Show unlock note screen. // Show unlock note screen.

View File

@@ -23,7 +23,7 @@ import {
deactivateKeepAwake deactivateKeepAwake
} from "@sayem314/react-native-keep-awake"; } from "@sayem314/react-native-keep-awake";
import React, { useCallback, useEffect, useRef, useState } from "react"; import React, { useCallback, useEffect, useRef, useState } from "react";
import { Dimensions, Platform, StatusBar, View } from "react-native"; import { Dimensions, Keyboard, Platform, StatusBar, View } from "react-native";
import changeNavigationBarColor from "react-native-navigation-bar-color"; import changeNavigationBarColor from "react-native-navigation-bar-color";
import { import {
addOrientationListener, addOrientationListener,
@@ -64,11 +64,12 @@ import { useSettingStore } from "../stores/use-setting-store";
import { import {
eClearEditor, eClearEditor,
eCloseFullscreenEditor, eCloseFullscreenEditor,
eOnChangeFluidTab,
eOnLoadNote, eOnLoadNote,
eOpenFullscreenEditor, eOpenFullscreenEditor,
eUnlockNote eUnlockNote
} from "../utils/events"; } from "../utils/events";
import { editorRef, tabBarRef } from "../utils/global-refs"; import { editorRef, inputRef, tabBarRef } from "../utils/global-refs";
import { sleep } from "../utils/time"; import { sleep } from "../utils/time";
import { NavigationStack } from "./navigation-stack"; import { NavigationStack } from "./navigation-stack";
@@ -512,7 +513,6 @@ const onChangeTab = async (obj) => {
editorState().movedAway = false; editorState().movedAway = false;
editorState().isFocused = true; editorState().isFocused = true;
activateKeepAwake(); activateKeepAwake();
if (!useTabStore.getState().getCurrentNoteId()) { if (!useTabStore.getState().getCurrentNoteId()) {
eSendEvent(eOnLoadNote, { eSendEvent(eOnLoadNote, {
newNote: true newNote: true
@@ -521,6 +521,7 @@ const onChangeTab = async (obj) => {
if ( if (
useTabStore.getState().getTab(useTabStore.getState().currentTab).locked useTabStore.getState().getTab(useTabStore.getState().currentTab).locked
) { ) {
console.log("Unlocking note....");
eSendEvent(eUnlockNote); eSendEvent(eUnlockNote);
} }
} }
@@ -530,13 +531,18 @@ const onChangeTab = async (obj) => {
editorState().movedAway = true; editorState().movedAway = true;
editorState().isFocused = false; editorState().isFocused = false;
eSendEvent(eClearEditor, "removeHandler"); eSendEvent(eClearEditor, "removeHandler");
let id = useTabStore.getState().getCurrentNoteId();
let note = await db.notes.note(id); // Lock all tabs with locked notes...
const locked = note && (await db.vaults.itemExists(note)); for (const tab of useTabStore.getState().tabs) {
if (locked) { const noteId = useTabStore.getState().getTab(tab.id)?.noteId;
useTabStore.getState().updateTab(useTabStore.getState().currentTab, { if (!noteId) continue;
locked: true const note = await db.notes.note(noteId);
}); const locked = note && (await db.vaults.itemExists(note));
if (locked) {
useTabStore.getState().updateTab(tab.id, {
locked: true
});
}
} }
} }
} }

View File

@@ -26,28 +26,15 @@ import React, {
useEffect, useEffect,
useImperativeHandle, useImperativeHandle,
useLayoutEffect, useLayoutEffect,
useRef, useRef
useState
} from "react"; } from "react";
import { import { Platform, TextInput, ViewStyle } from "react-native";
Platform,
ScrollView,
TextInput,
ViewStyle,
useWindowDimensions
} from "react-native";
import WebView from "react-native-webview"; import WebView from "react-native-webview";
import { ShouldStartLoadRequest } from "react-native-webview/lib/WebViewTypes"; import { ShouldStartLoadRequest } from "react-native-webview/lib/WebViewTypes";
import { notesnook } from "../../../e2e/test.ids"; import { notesnook } from "../../../e2e/test.ids";
import { db } from "../../common/database"; import { db } from "../../common/database";
import { Button } from "../../components/ui/button";
import { IconButton } from "../../components/ui/icon-button"; import { IconButton } from "../../components/ui/icon-button";
import Input from "../../components/ui/input";
import Seperator from "../../components/ui/seperator";
import Heading from "../../components/ui/typography/heading";
import Paragraph from "../../components/ui/typography/paragraph";
import { useDBItem } from "../../hooks/use-db-item"; import { useDBItem } from "../../hooks/use-db-item";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import useKeyboard from "../../hooks/use-keyboard"; import useKeyboard from "../../hooks/use-keyboard";
import BiometicService from "../../services/biometrics"; import BiometicService from "../../services/biometrics";
import { import {
@@ -55,15 +42,20 @@ import {
eSendEvent, eSendEvent,
eSubscribeEvent eSubscribeEvent
} from "../../services/event-manager"; } from "../../services/event-manager";
import { useSettingStore } from "../../stores/use-setting-store";
import { getElevationStyle } from "../../utils/elevation"; import { getElevationStyle } from "../../utils/elevation";
import { eOnLoadNote, eUnlockNote } from "../../utils/events"; import {
eOnLoadNote,
eUnlockNote,
eUnlockWithBiometrics,
eUnlockWithPassword
} from "../../utils/events";
import { openLinkInBrowser } from "../../utils/functions"; import { openLinkInBrowser } from "../../utils/functions";
import EditorOverlay from "./loading";
import { EDITOR_URI } from "./source"; import { EDITOR_URI } from "./source";
import { EditorProps, useEditorType } from "./tiptap/types"; import { EditorProps, useEditorType } from "./tiptap/types";
import { useEditor } from "./tiptap/use-editor"; import { useEditor } from "./tiptap/use-editor";
import { useEditorEvents } from "./tiptap/use-editor-events"; import { useEditorEvents } from "./tiptap/use-editor-events";
import { useTabStore } from "./tiptap/use-tab-store"; import { syncTabs, useTabStore } from "./tiptap/use-tab-store";
import { editorController, editorState } from "./tiptap/utils"; import { editorController, editorState } from "./tiptap/utils";
const style: ViewStyle = { const style: ViewStyle = {
@@ -181,7 +173,7 @@ const Editor = React.memo(
autoManageStatusBarEnabled={false} autoManageStatusBarEnabled={false}
onMessage={onMessage || undefined} onMessage={onMessage || undefined}
/> />
<EditorOverlay editorId={editorId || ""} editor={editor} /> {/* <EditorOverlay editorId={editorId || ""} editor={editor} /> */}
<ReadonlyButton editor={editor} /> <ReadonlyButton editor={editor} />
<LockOverlay /> <LockOverlay />
</> </>
@@ -193,214 +185,169 @@ const Editor = React.memo(
export default Editor; export default Editor;
let LOADED = false;
const LockOverlay = () => { const LockOverlay = () => {
const tab = useTabStore((state) => const tab = useTabStore((state) =>
state.tabs.find((t) => t.id === state.currentTab) state.tabs.find((t) => t.id === state.currentTab)
); );
const { height } = useWindowDimensions(); const isAppLoading = useSettingStore((state) => state.isAppLoading);
const [item] = useDBItem(tab?.noteId, "note"); const [item] = useDBItem(isAppLoading ? undefined : tab?.noteId, "note");
const isLocked = item?.locked && tab?.locked;
const { colors } = useThemeColors();
const insets = useGlobalSafeAreaInsets();
const password = useRef<string>();
const passInputRef = useRef<TextInput>(null);
const [biometryEnrolled, setBiometryEnrolled] = useState(false);
const [biometryAvailable, setBiometryAvailable] = useState(false);
const [enrollBiometrics, setEnrollBiometrics] = useState(false);
console.log("Tab locked", item?.locked, tab?.locked); useEffect(() => {
if (!isAppLoading && !LOADED) {
LOADED = true;
(async () => {
for (const tab of useTabStore.getState().tabs) {
const noteId = useTabStore.getState().getTab(tab.id)?.noteId;
if (!noteId) continue;
const note = await db.notes.note(noteId);
const locked = note && (await db.vaults.itemExists(note));
if (locked) {
useTabStore.getState().updateTab(tab.id, {
locked: true
});
}
}
})();
}
}, [isAppLoading]);
useEffect(() => { useEffect(() => {
(async () => { (async () => {
let biometry = await BiometicService.isBiometryAvailable(); const biometry = await BiometicService.isBiometryAvailable();
let fingerprint = await BiometicService.hasInternetCredentials(); const fingerprint = await BiometicService.hasInternetCredentials();
setBiometryAvailable(!!biometry); useTabStore.setState({
setBiometryEnrolled(!!fingerprint); biometryAvailable: !!biometry,
biometryEnrolled: !!fingerprint
});
syncTabs();
})(); })();
}, [isLocked]); }, [tab?.id]);
const unlockWithBiometrics = async () => { useEffect(() => {
try { const unlockWithBiometrics = async () => {
try {
if (!item || !tab) return;
console.log("Trying to unlock with biometrics...");
const credentials = await BiometicService.getCredentials(
"Unlock note",
"Unlock note to open it in editor. If biometrics are not working, you can enter device pin to unlock vault."
);
if (credentials && credentials?.password) {
const note = await db.vault.open(item.id, credentials?.password);
eSendEvent(eOnLoadNote, {
item: note
});
useTabStore.getState().updateTab(tab.id, {
locked: false
});
}
} catch (e) {
console.error(e);
}
};
const onSubmit = async ({
password,
biometrics: enrollBiometrics
}: {
password: string;
biometrics?: boolean;
}) => {
if (!item || !tab) return; if (!item || !tab) return;
console.log("Trying to unlock with biometrics..."); if (!password || password.trim().length === 0) {
let credentials = await BiometicService.getCredentials( ToastManager.show({
"Unlock note", heading: "Password not entered",
"Unlock note to open it in editor. If biometrics are not working, you can enter device pin to unlock vault." message: "Enter a password for the vault and try again.",
); type: "error"
});
return;
}
if (credentials && credentials?.password) { try {
let note = await db.vault.open(item.id, credentials?.password); const note = await db.vault.open(item.id, password);
if (enrollBiometrics) {
try {
await db.vault.unlock(password);
await BiometicService.storeCredentials(password);
eSendEvent("vaultUpdated");
ToastManager.show({
heading: "Biometric unlocking enabled!",
message: "Now you can unlock notes in vault with biometrics.",
type: "success",
context: "global"
});
const biometry = await BiometicService.isBiometryAvailable();
const fingerprint = await BiometicService.hasInternetCredentials();
useTabStore.setState({
biometryAvailable: !!biometry,
biometryEnrolled: !!fingerprint
});
syncTabs();
} catch (e) {
ToastManager.show({
heading: "Incorrect password",
message:
"Please enter the correct vault password to enable biometrics.",
type: "error",
context: "local"
});
}
}
eSendEvent(eOnLoadNote, { eSendEvent(eOnLoadNote, {
item: note item: note
}); });
useTabStore.getState().updateTab(tab.id, { useTabStore.getState().updateTab(tab.id, {
locked: false locked: false
}); });
} catch (e) {
console.log(e);
ToastManager.show({
heading: "Incorrect password",
type: "error",
context: "local"
});
} }
} catch (e) { };
console.error(e);
}
};
useEffect(() => {
const unlock = () => { const unlock = () => {
if ( if (
isLocked && (tab?.locked,
biometryAvailable && useTabStore.getState().biometryAvailable &&
biometryEnrolled && useTabStore.getState().biometryEnrolled &&
!editorState().movedAway !editorState().movedAway)
) { ) {
unlockWithBiometrics(); unlockWithBiometrics();
} else { } else {
console.log("Biometrics unavailable."); console.log("Biometrics unavailable.", editorState().movedAway);
if (isLocked && !editorState().movedAway) { if (!editorState().movedAway) {
setTimeout(() => { setTimeout(() => {
passInputRef.current?.focus(); if (tab && tab?.locked) {
}, 300); editorController.current?.commands.focus(tab?.id);
}
}, 100);
} }
} }
}; };
const sub = eSubscribeEvent(eUnlockNote, unlock); const subs = [
unlock(); eSubscribeEvent(eUnlockNote, unlock),
eSubscribeEvent(eUnlockWithBiometrics, () => {
unlock();
}),
eSubscribeEvent(eUnlockWithPassword, onSubmit)
];
if (tab?.locked) {
unlock();
}
return () => { return () => {
sub.unsubscribe(); subs.map((s) => s?.unsubscribe());
}; };
}, [isLocked, biometryAvailable, biometryEnrolled]); }, [item, tab]);
const onSubmit = async () => { return null;
if (!item || !tab) return;
if (!password.current || password.current.trim().length === 0) {
ToastManager.show({
heading: "Password not entered",
message: "Enter a password for the vault and try again.",
type: "error"
});
return;
}
try {
let note = await db.vault.open(item.id, password.current);
if (enrollBiometrics) {
try {
await db.vault.unlock(password.current);
await BiometicService.storeCredentials(password.current);
eSendEvent("vaultUpdated");
ToastManager.show({
heading: "Biometric unlocking enabled!",
message: "Now you can unlock notes in vault with biometrics.",
type: "success",
context: "global"
});
} catch (e) {
ToastManager.show({
heading: "Incorrect password",
message:
"Please enter the correct vault password to enable biometrics.",
type: "error",
context: "local"
});
}
}
eSendEvent(eOnLoadNote, {
item: note
});
useTabStore.getState().updateTab(tab.id, {
locked: false
});
} catch (e) {
console.log(e);
ToastManager.show({
heading: "Incorrect password",
type: "error",
context: "local"
});
}
};
return isLocked ? (
<ScrollView
style={{
width: "100%",
height: height,
backgroundColor: colors.primary.background,
position: "absolute",
top: 50 + insets.top,
zIndex: 999
}}
contentContainerStyle={{
alignItems: "center",
justifyContent: "center",
height: height
}}
keyboardDismissMode="interactive"
keyboardShouldPersistTaps="handled"
>
<Heading>{item.title}</Heading>
<Paragraph>This note is locked.</Paragraph>
<Seperator />
<Input
fwdRef={passInputRef}
autoCapitalize="none"
testID={notesnook.ids.dialogs.vault.pwd}
onChangeText={(value) => {
password.current = value;
}}
wrapperStyle={{
width: 300
}}
marginBottom={10}
onSubmit={() => {
onSubmit();
}}
autoComplete="password"
returnKeyLabel="Unlock"
returnKeyType={"done"}
secureTextEntry
placeholder="Password"
/>
<Button
title="Unlock note"
type="accent"
onPress={() => {
onSubmit();
}}
/>
{biometryAvailable && !biometryEnrolled ? (
<Button
title="Enable biometric unlocking"
type={enrollBiometrics ? "accent" : "gray"}
onPress={() => {
setEnrollBiometrics(!enrollBiometrics);
}}
style={{
marginTop: 10
}}
icon={
enrollBiometrics
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
iconSize={20}
/>
) : biometryEnrolled && biometryAvailable ? (
<IconButton
name="fingerprint"
type="gray"
customStyle={{
marginTop: 20
}}
size={40}
onPress={() => {
unlockWithBiometrics();
}}
/>
) : null}
</ScrollView>
) : null;
}; };
const ReadonlyButton = ({ editor }: { editor: useEditorType }) => { const ReadonlyButton = ({ editor }: { editor: useEditorType }) => {

View File

@@ -17,8 +17,10 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { Note } from "@notesnook/core/dist/types";
import type { Attachment } from "@notesnook/editor/dist/extensions/attachment/index"; import type { Attachment } from "@notesnook/editor/dist/extensions/attachment/index";
import type { ImageAttributes } from "@notesnook/editor/dist/extensions/image/index"; import type { ImageAttributes } from "@notesnook/editor/dist/extensions/image/index";
import type { LinkAttributes } from "@notesnook/editor/dist/extensions/link";
import { createRef, RefObject } from "react"; import { createRef, RefObject } from "react";
import { Platform } from "react-native"; import { Platform } from "react-native";
import { EdgeInsets } from "react-native-safe-area-context"; import { EdgeInsets } from "react-native-safe-area-context";
@@ -26,10 +28,8 @@ import WebView from "react-native-webview";
import { db } from "../../../common/database"; import { db } from "../../../common/database";
import { sleep } from "../../../utils/time"; import { sleep } from "../../../utils/time";
import { Settings } from "./types"; import { Settings } from "./types";
import { getResponse, randId, textInput } from "./utils";
import { Note } from "@notesnook/core/dist/types";
import { useTabStore } from "./use-tab-store"; import { useTabStore } from "./use-tab-store";
import type { LinkAttributes } from "@notesnook/editor/dist/extensions/link"; import { getResponse, randId, textInput } from "./utils";
type Action = { job: string; id: string }; type Action = { job: string; id: string };
@@ -82,7 +82,15 @@ class Commands {
setTimeout(async () => { setTimeout(async () => {
if (!this.ref) return; if (!this.ref) return;
textInput.current?.focus(); textInput.current?.focus();
await this.doAsync(`editors[${tabId}]?.commands.focus()`, "focus");
const locked = useTabStore.getState().getTab(tabId)?.locked;
await this.doAsync(
locked
? `editorControllers[${tabId}]?.focusPassInput();`
: `editors[${tabId}]?.commands.focus()`,
"focus"
);
this.ref?.current?.requestFocus(); this.ref?.current?.requestFocus();
}, 1); }, 1);
} else { } else {
@@ -98,6 +106,9 @@ class Commands {
const editorTitle = editorTitles[${tabId}]; const editorTitle = editorTitles[${tabId}];
typeof editor !== "undefined" && 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();
editorControllers[${tabId}]?.blurPassInput();
`, `,
"blur" "blur"
); );
@@ -119,7 +130,6 @@ typeof editorTitle !== "undefined" && editorTitle.current && editorTitle.current
if (editorController.content) editorController.content.current = null; if (editorController.content) editorController.content.current = null;
editorController.onUpdate(); editorController.onUpdate();
editorController.setTitle(null); editorController.setTitle(null);
editorController.countWords(0);
if (typeof statusBar !== "undefined") { if (typeof statusBar !== "undefined") {
statusBar.current.resetWords(); statusBar.current.resetWords();
statusBar.current.set({date:"",saved:""}); statusBar.current.set({date:"",saved:""});
@@ -237,7 +247,7 @@ editor && editor.commands.insertAttachment(${JSON.stringify(attachment)})`
}; };
setAttachmentProgress = async ( setAttachmentProgress = async (
attachmentProgress: AttachmentProgress, attachmentProgress: Partial<Attachment>,
tabId: number tabId: number
) => { ) => {
await this.doAsync( await this.doAsync(
@@ -323,6 +333,20 @@ const image = toBlobURL("${image.dataurl}", "${image.hash}");
`); `);
}; };
focusPassInput = async () => {
const tabId = useTabStore.getState().currentTab;
return this.doAsync(`
response = editorControllers[${tabId}]?.focusPassInput() || [];
`);
};
blurPassInput = async () => {
const tabId = useTabStore.getState().currentTab;
return this.doAsync(`
response = editorControllers[${tabId}]?.blurPassInput() || [];
`);
};
createInternalLink = async ( createInternalLink = async (
attributes: LinkAttributes, attributes: LinkAttributes,
resolverId: string resolverId: string

View File

@@ -43,5 +43,7 @@ export const EventTypes = {
tabFocused: "editor-events:tab-focused", tabFocused: "editor-events:tab-focused",
toc: "editor-events:toc", toc: "editor-events:toc",
createInternalLink: "editor-events:create-internal-link", createInternalLink: "editor-events:create-internal-link",
load: "editor-events:load" load: "editor-events:load",
unlock: "editor-events:unlock",
unlockWithBiometrics: "editor-events:unlock-biometrics"
}; };

View File

@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
/* eslint-disable no-case-declarations */ /* eslint-disable no-case-declarations */
/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-var-requires */
import { parseInternalLink } from "@notesnook/core";
import { ItemReference } from "@notesnook/core/dist/types"; import { ItemReference } from "@notesnook/core/dist/types";
import type { Attachment } from "@notesnook/editor/dist/extensions/attachment/index"; import type { Attachment } from "@notesnook/editor/dist/extensions/attachment/index";
import { getDefaultPresets } from "@notesnook/editor/dist/toolbar/tool-definitions"; import { getDefaultPresets } from "@notesnook/editor/dist/toolbar/tool-definitions";
@@ -36,9 +37,11 @@ import { WebViewMessageEvent } from "react-native-webview";
import { db } from "../../../common/database"; import { db } from "../../../common/database";
import downloadAttachment from "../../../common/filesystem/download-attachment"; import downloadAttachment from "../../../common/filesystem/download-attachment";
import EditorTabs from "../../../components/sheets/editor-tabs"; import EditorTabs from "../../../components/sheets/editor-tabs";
import LinkNote from "../../../components/sheets/link-note";
import ManageTagsSheet from "../../../components/sheets/manage-tags"; import ManageTagsSheet from "../../../components/sheets/manage-tags";
import { RelationsList } from "../../../components/sheets/relations-list"; import { RelationsList } from "../../../components/sheets/relations-list";
import ReminderSheet from "../../../components/sheets/reminder"; import ReminderSheet from "../../../components/sheets/reminder";
import TableOfContents from "../../../components/sheets/toc";
import { DDS } from "../../../services/device-detection"; import { DDS } from "../../../services/device-detection";
import { import {
ToastManager, ToastManager,
@@ -48,7 +51,6 @@ import {
} from "../../../services/event-manager"; } from "../../../services/event-manager";
import Navigation from "../../../services/navigation"; import Navigation from "../../../services/navigation";
import SettingsService from "../../../services/settings"; import SettingsService from "../../../services/settings";
import { useEditorStore } from "../../../stores/use-editor-store";
import { useRelationStore } from "../../../stores/use-relation-store"; import { useRelationStore } from "../../../stores/use-relation-store";
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";
@@ -61,7 +63,9 @@ import {
eOpenFullscreenEditor, eOpenFullscreenEditor,
eOpenLoginDialog, eOpenLoginDialog,
eOpenPremiumDialog, eOpenPremiumDialog,
eOpenPublishNoteDialog eOpenPublishNoteDialog,
eUnlockWithBiometrics,
eUnlockWithPassword
} from "../../../utils/events"; } from "../../../utils/events";
import { openLinkInBrowser } from "../../../utils/functions"; import { openLinkInBrowser } from "../../../utils/functions";
import { tabBarRef } from "../../../utils/global-refs"; import { tabBarRef } from "../../../utils/global-refs";
@@ -70,9 +74,6 @@ import { EventTypes } from "./editor-events";
import { EditorMessage, EditorProps, useEditorType } from "./types"; import { EditorMessage, EditorProps, useEditorType } from "./types";
import { useTabStore } from "./use-tab-store"; import { useTabStore } from "./use-tab-store";
import { EditorEvents, editorState } from "./utils"; import { EditorEvents, editorState } from "./utils";
import TableOfContents from "../../../components/sheets/toc";
import LinkNote from "../../../components/sheets/link-note";
import { parseInternalLink } from "@notesnook/core";
const publishNote = async () => { const publishNote = async () => {
const user = useUserStore.getState().user; const user = useUserStore.getState().user;
@@ -368,7 +369,7 @@ export const useEditorEvents = (
case EventTypes.content: case EventTypes.content:
editor.saveContent({ editor.saveContent({
type: editorMessage.type, type: editorMessage.type,
content: editorMessage.value as string, content: editorMessage.value.html as string,
noteId: noteId, noteId: noteId,
tabId: editorMessage.tabId, tabId: editorMessage.tabId,
ignoreEdit: (editorMessage.value as ContentMessage).ignoreEdit ignoreEdit: (editorMessage.value as ContentMessage).ignoreEdit
@@ -620,6 +621,16 @@ export const useEditorEvents = (
break; break;
} }
case EventTypes.unlock: {
eSendEvent(eUnlockWithPassword, editorMessage.value);
break;
}
case EventTypes.unlockWithBiometrics: {
eSendEvent(eUnlockWithBiometrics);
break;
}
default: default:
break; break;
} }

View File

@@ -24,6 +24,7 @@ import { EVENTS } from "@notesnook/core/dist/common";
import { import {
ContentItem, ContentItem,
ContentType, ContentType,
ItemReference,
Note, Note,
UnencryptedContentItem, UnencryptedContentItem,
isDeleted isDeleted
@@ -43,7 +44,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 { useSettingStore } from "../../../stores/use-setting-store";
import { useTagStore } from "../../../stores/use-tag-store"; import { useTagStore } from "../../../stores/use-tag-store";
import { import {
eClearEditor, eClearEditor,
@@ -168,7 +168,7 @@ export const useEditor = (
console.log(tabId); console.log(tabId);
}); });
return () => { return () => {
event.unsubscribe(); event?.unsubscribe();
}; };
}); });
@@ -202,7 +202,7 @@ export const useEditor = (
console.log("Resetting tab:", tabId); 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);
currentNotes.current[noteId] = null; currentNotes.current[noteId] = null;
currentContents.current[noteId] = null; currentContents.current[noteId] = null;
editorSessionHistory.clearSession(noteId); editorSessionHistory.clearSession(noteId);
@@ -403,13 +403,16 @@ export const useEditor = (
const tabId = useTabStore.getState().currentTab; const tabId = useTabStore.getState().currentTab;
currentNotes.current && (await reset(tabId)); currentNotes.current && (await reset(tabId));
setTimeout(() => { setTimeout(() => {
if (state.current?.ready) commands.focus(tabId); if (state.current?.ready && !state.current.movedAway)
commands.focus(tabId);
}); });
} else { } else {
if (!event.item) return; if (!event.item) return;
const item = event.item; const item = event.item;
const noteIsLocked = const noteIsLocked =
event.item.locked && !(event.item as NoteWithContent).content; (await db.vaults.itemExists(event.item as ItemReference)) &&
!(event.item as NoteWithContent).content;
// If note was already opened in a tab, focus that tab. // If note was already opened in a tab, focus that tab.
if (typeof event.tabId !== "number") { if (typeof event.tabId !== "number") {
@@ -428,8 +431,7 @@ export const useEditor = (
// 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, readonly: event.item.readonly || readonly,
locked: locked: noteIsLocked
event.item.locked && !(event.item as NoteWithContent).content
}); });
} }
} else { } else {
@@ -512,7 +514,15 @@ export const useEditor = (
}, 300); }, 300);
} }
}, },
[commands, editorSessionHistory, loadContent, postMessage, readonly, reset] [
commands,
editorSessionHistory,
loadContent,
overlay,
postMessage,
readonly,
reset
]
); );
const lockNoteWithVault = useCallback((note: Note) => { const lockNoteWithVault = useCallback((note: Note) => {
@@ -549,7 +559,7 @@ export const useEditor = (
if (data.type === "tiptap" && note) { if (data.type === "tiptap" && note) {
// Handle this case where note was locked on another device and synced. // Handle this case where note was locked on another device and synced.
const locked = await db.vaults.itemExists( const locked = await db.vaults.itemExists(
currentNotes.current[note.id] currentNotes.current[note.id] as ItemReference
); );
if (!locked && isContentEncrypted) { if (!locked && isContentEncrypted) {
lockNoteWithVault(note); lockNoteWithVault(note);
@@ -606,7 +616,6 @@ export const useEditor = (
title, title,
content, content,
type, type,
forSessionId,
ignoreEdit, ignoreEdit,
noteId, noteId,
tabId tabId
@@ -615,7 +624,6 @@ export const useEditor = (
title?: string; title?: string;
content?: string; content?: string;
type: string; type: string;
forSessionId: string;
ignoreEdit: boolean; ignoreEdit: boolean;
tabId: number; tabId: number;
}) => { }) => {
@@ -667,7 +675,6 @@ export const useEditor = (
const restoreEditorState = useCallback(async () => { const restoreEditorState = useCallback(async () => {
const appState = getAppState(); const appState = getAppState();
console.log(appState, "appState");
if (!appState) return; if (!appState) return;
state.current.isRestoringState = true; state.current.isRestoringState = true;
state.current.currentlyEditing = true; state.current.currentlyEditing = true;

View File

@@ -20,6 +20,7 @@ import create from "zustand";
import { persist, StateStorage } from "zustand/middleware"; import { persist, StateStorage } from "zustand/middleware";
import { editorController } from "./utils"; import { editorController } from "./utils";
import { MMKV } from "../../../common/database/mmkv"; import { MMKV } from "../../../common/database/mmkv";
import { db } from "../../../common/database";
class History { class History {
history: number[]; history: number[];
@@ -98,6 +99,8 @@ export type TabStore = {
getCurrentNoteId: () => string | undefined; getCurrentNoteId: () => string | undefined;
getTab: (tabId: number) => TabItem | undefined; getTab: (tabId: number) => TabItem | undefined;
tabHistory: number[]; tabHistory: number[];
biometryAvailable?: boolean;
biometryEnrolled?: boolean;
}; };
function getId(id: number, tabs: TabItem[]): number { function getId(id: number, tabs: TabItem[]): number {
@@ -112,7 +115,9 @@ export function syncTabs() {
editorController.current?.commands.doAsync(` editorController.current?.commands.doAsync(`
globalThis.tabStore?.setState({ globalThis.tabStore?.setState({
tabs: ${JSON.stringify(useTabStore.getState().tabs)}, tabs: ${JSON.stringify(useTabStore.getState().tabs)},
currentTab: ${useTabStore.getState().currentTab} currentTab: ${useTabStore.getState().currentTab},
biometryAvailable: ${useTabStore.getState().biometryAvailable},
biometryEnrolled: ${useTabStore.getState().biometryEnrolled}
}); });
`); `);
} }

View File

@@ -166,5 +166,8 @@ export const eOnRefreshSearch = "612";
export const eOpenAppLockPasswordDialog = "613"; export const eOpenAppLockPasswordDialog = "613";
export const eCloseAppLocKPasswordDailog = "614"; export const eCloseAppLocKPasswordDailog = "614";
export const eEditorTabFocused = "613"; export const eEditorTabFocused = "615";
export const eUnlockNote = "614"; export const eUnlockNote = "616";
export const eOnChangeFluidTab = "617";
export const eUnlockWithBiometrics = "618";
export const eUnlockWithPassword = "619";

View File

@@ -51,6 +51,7 @@ import Header from "./header";
import StatusBar from "./statusbar"; import StatusBar from "./statusbar";
import Tags from "./tags"; import Tags from "./tags";
import Title from "./title"; import Title from "./title";
import FingerprintIcon from "mdi-react/FingerprintIcon";
globalThis.toBlobURL = toBlobURL as typeof globalThis.toBlobURL; globalThis.toBlobURL = toBlobURL as typeof globalThis.toBlobURL;
@@ -71,6 +72,9 @@ const Tiptap = ({
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const noteStateUpdateTimer = useRef<NodeJS.Timeout>(); const noteStateUpdateTimer = useRef<NodeJS.Timeout>();
const tabRef = useRef<TabItem>(tab); const tabRef = useRef<TabItem>(tab);
const biometryAvailable = useTabStore((state) => state.biometryAvailable);
const biometryEnrolled = useTabStore((state) => state.biometryEnrolled);
const isFocusedRef = useRef<boolean>(false); const isFocusedRef = useRef<boolean>(false);
tabRef.current = tab; tabRef.current = tab;
@@ -161,6 +165,7 @@ const Tiptap = ({
timeFormat: settings.timeFormat as "12-hour" | "24-hour" | undefined, timeFormat: settings.timeFormat as "12-hour" | "24-hour" | undefined,
enableInputRules: settings.markdownShortcuts enableInputRules: settings.markdownShortcuts
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ }, [
getContentDiv, getContentDiv,
settings.readonly, settings.readonly,
@@ -169,6 +174,7 @@ const Tiptap = ({
settings.dateFormat, settings.dateFormat,
settings.timeFormat, settings.timeFormat,
settings.markdownShortcuts, settings.markdownShortcuts,
tick,
tab.id tab.id
]); ]);
@@ -372,7 +378,7 @@ const Tiptap = ({
position: "relative" position: "relative"
}} }}
> >
{settings.noHeader ? null : ( {settings.noHeader || tab.locked ? null : (
<> <>
<Tags settings={settings} /> <Tags settings={settings} />
<Title <Title
@@ -392,45 +398,219 @@ const Tiptap = ({
<div <div
style={{ style={{
width: "100%", width: "100%",
height: "95%", height: "90%",
position: "absolute", position: "absolute",
zIndex: 999, zIndex: 999,
backgroundColor: colors.primary.background, backgroundColor: colors.primary.background,
paddingRight: 12, paddingRight: 12,
paddingLeft: 12, paddingLeft: 12,
display: "flex", display: "flex",
flexDirection: "column" flexDirection: "column",
alignItems: "center",
justifyContent: "center",
boxSizing: "border-box",
rowGap: 10
}} }}
> >
<div {tab.locked ? (
style={{ <>
height: 16, <p
width: "94%", style={{
backgroundColor: colors.secondary.background, color: colors.primary.paragraph,
borderRadius: 5, fontSize: 20,
marginTop: 10 fontWeight: "600",
}} textAlign: "center",
/> padding: "0px 20px",
marginBottom: 0,
userSelect: "none"
}}
>
{controller.title} ajklsdksa jdlksaj dksljk jdalkjskldj
</p>
<p
style={{
color: colors.primary.paragraph,
marginTop: 0,
marginBottom: 0,
userSelect: "none"
}}
>
This note is locked.
</p>
<div <form
style={{ onSubmit={(e) => {
height: 16, e.preventDefault();
width: "94%", const data = new FormData(e.currentTarget);
backgroundColor: colors.secondary.background, const password = data.get("password");
borderRadius: 5, const biometrics = data.get("enrollBiometrics");
marginTop: 10 post("editor-events:unlock", {
}} password,
/> biometrics: biometrics === "on" ? true : false
});
}}
style={{
display: "flex",
flexDirection: "column",
rowGap: 10
}}
>
<input
placeholder="Enter password"
ref={controller.passwordInputRef}
name="password"
type="password"
required
style={{
boxSizing: "border-box",
width: 300,
height: 45,
borderRadius: 5,
border: `1px solid ${colors.primary.border}`,
paddingLeft: 12,
paddingRight: 12,
fontSize: "1em",
backgroundColor: "transparent",
caretColor: colors.primary.accent,
color: colors.primary.paragraph
}}
/>
<div <button
style={{ style={{
height: 16, backgroundColor: colors.primary.accent,
width: 200, borderRadius: 5,
backgroundColor: colors.secondary.background, boxSizing: "border-box",
borderRadius: 5, border: "none",
marginTop: 10 color: colors.static.white,
}} width: 300,
/> fontSize: "0.9em",
height: 45,
display: "flex",
alignItems: "center",
justifyContent: "center"
}}
onMouseDown={(e) => {
if (globalThis.keyboardShown) {
e.preventDefault();
}
}}
>
<p
style={{
userSelect: "none"
}}
>
Unlock note
</p>
</button>
{biometryAvailable && !biometryEnrolled ? (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 5
}}
>
<input
type="checkbox"
name="enrollBiometrics"
style={{
accentColor: colors.primary.accent
}}
onMouseDown={(e) => {
if (globalThis.keyboardShown) {
e.preventDefault();
}
}}
/>
<p
style={{
color: colors.primary.paragraph,
marginTop: 0,
marginBottom: 0,
userSelect: "none"
}}
>
Enable biometric unlocking
</p>
</div>
) : null}
</form>
{biometryEnrolled && biometryAvailable ? (
<button
style={{
backgroundColor: "transparent",
borderRadius: 5,
boxSizing: "border-box",
border: "none",
color: colors.primary.accent,
width: 300,
fontSize: "0.9em",
height: 45,
display: "flex",
alignItems: "center",
justifyContent: "center",
columnGap: 5,
userSelect: "none"
}}
onMouseDown={(e) => {
if (globalThis.keyboardShown) {
e.preventDefault();
}
}}
onClick={() => {
post("editor-events:unlock-biometrics");
}}
>
<FingerprintIcon />
<p
style={{
userSelect: "none"
}}
>
Unlock with biometrics
</p>
</button>
) : null}
</>
) : (
<>
<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> </div>
) : null} ) : null}

View File

@@ -109,8 +109,10 @@ export type EditorController = {
setLoading: (value: boolean) => void; setLoading: (value: boolean) => void;
getTableOfContents: () => any[]; getTableOfContents: () => any[];
scrollIntoView: (id: string) => void; scrollIntoView: (id: string) => void;
passwordInputRef: MutableRefObject<HTMLInputElement | null>;
focusPassInput: () => void;
blurPassInput: () => void;
}; };
export function useEditorController({ export function useEditorController({
update, update,
getTableOfContents getTableOfContents
@@ -118,6 +120,7 @@ export function useEditorController({
update: () => void; update: () => void;
getTableOfContents: () => any[]; getTableOfContents: () => any[];
}): EditorController { }): EditorController {
const passwordInputRef = useRef<HTMLInputElement | null>(null);
const tab = useTabContext(); const tab = useTabContext();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const setTheme = useThemeEngineStore((store) => store.setTheme); const setTheme = useThemeEngineStore((store) => store.setTheme);
@@ -240,8 +243,8 @@ export function useEditorController({
break; break;
} }
case "native:html": case "native:html":
// logger("info", "loading html", htmlContentRef.current);
htmlContentRef.current = value; htmlContentRef.current = value;
logger("info", "loading html");
if (!editor) break; if (!editor) break;
update(); update();
countWords(0); countWords(0);
@@ -379,6 +382,14 @@ export function useEditorController({
countWords(); countWords();
logger("info", `Tab ${tab.id} updated.`); logger("info", `Tab ${tab.id} updated.`);
}, 1); }, 1);
},
passwordInputRef,
focusPassInput: () => {
logger("info", "focus pass input...");
passwordInputRef.current?.focus();
},
blurPassInput: () => {
passwordInputRef.current?.blur();
} }
}; };
} }

View File

@@ -62,6 +62,8 @@ export type TabStore = {
getCurrentNoteId: () => string | undefined; getCurrentNoteId: () => string | undefined;
getTab: (tabId: number) => TabItem | undefined; getTab: (tabId: number) => TabItem | undefined;
setNoteState: (noteId: string, state: Partial<NoteState>) => void; setNoteState: (noteId: string, state: Partial<NoteState>) => void;
biometryAvailable?: boolean;
biometryEnrolled?: boolean;
}; };
function getId(id: number, tabs: TabItem[]): number { function getId(id: number, tabs: TabItem[]): number {

View File

@@ -186,7 +186,9 @@ export const EventTypes = {
tabFocused: "editor-events:tab-focused", tabFocused: "editor-events:tab-focused",
toc: "editor-events:toc", toc: "editor-events:toc",
createInternalLink: "editor-events:create-internal-link", createInternalLink: "editor-events:create-internal-link",
load: "editor-events:load" load: "editor-events:load",
unlock: "editor-events:unlock",
unlockWithBiometrics: "editor-events:unlock-biometrics"
} as const; } as const;
export function randId(prefix: string) { export function randId(prefix: string) {