Compare commits

..

1 Commits

Author SHA1 Message Date
Ammar Ahmed
4fec64d2eb mobile: fix footer size for notebook screen 2024-04-30 21:46:54 +05:00
46 changed files with 194 additions and 524 deletions

View File

@@ -32,7 +32,6 @@ import { AssetManager } from "../utils/asset-manager";
import { isFlatpak } from "../utils";
import { setupDesktopIntegration } from "../utils/desktop-integration";
import { rm } from "fs/promises";
import { disableCustomDns, enableCustomDns } from "../utils/custom-dns";
const t = initTRPC.create();
@@ -56,15 +55,6 @@ export const osIntegrationRouter = t.router({
config.zoomFactor = factor;
}),
customDns: t.procedure.query(() => config.customDns),
setCustomDns: t.procedure
.input(z.boolean())
.mutation(({ input: customDns }) => {
if (customDns) enableCustomDns();
else disableCustomDns();
config.customDns = customDns;
}),
proxyRules: t.procedure.query(() => config.proxyRules),
setProxyRules: t.procedure
.input(z.string().optional())

View File

@@ -35,7 +35,6 @@ import path from "path";
import { bringToFront } from "./utils/bring-to-front";
import { bridge } from "./api/bridge";
import { setupDesktopIntegration } from "./utils/desktop-integration";
import { disableCustomDns, enableCustomDns } from "./utils/custom-dns";
// only run a single instance
if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
@@ -152,8 +151,14 @@ async function createWindow() {
app.once("ready", async () => {
console.info("App ready. Opening window.");
if (config.customDns) enableCustomDns();
else disableCustomDns();
app.configureHostResolver({
secureDnsServers: [
"https://mozilla.cloudflare-dns.com/dns-query",
"https://dns.quad9.net/dns-query"
],
enableBuiltInResolver: true,
secureDnsMode: "automatic"
});
if (!isDevelopment()) registerProtocol();
await createWindow();

View File

@@ -43,7 +43,6 @@ export const config = {
theme: nativeTheme.themeSource,
automaticUpdates: true,
proxyRules: "",
customDns: true,
backgroundColor: nativeTheme.themeSource === "dark" ? "#0f0f0f" : "#ffffff",
windowControlsIconColor:

View File

@@ -1,37 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { app } from "electron";
export function enableCustomDns() {
app.configureHostResolver({
secureDnsServers: [
"https://mozilla.cloudflare-dns.com/dns-query",
"https://dns.quad9.net/dns-query"
],
enableBuiltInResolver: true
});
}
export function disableCustomDns() {
app.configureHostResolver({
secureDnsServers: [],
enableBuiltInResolver: true
});
}

View File

@@ -19,6 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { View } from "react-native";
import React from "react";
export const Footer = () => {
return <View style={{ height: 150 }} />;
export const Footer = ({ height = 150 }) => {
return <View style={{ height: height }} />;
};

View File

@@ -192,7 +192,9 @@ export default function List(props: ListProps) {
/>
) : null
}
ListFooterComponent={<Footer />}
ListFooterComponent={
<Footer height={props.renderedInRoute === "Notebook" ? 300 : 150} />
}
ListHeaderComponent={
<>
{props.CustomLisHeader ? (

View File

@@ -219,7 +219,7 @@ export default function LinkNote(props: {
<View
style={{
paddingHorizontal: 12,
minHeight: "100%",
minHeight: 400,
maxHeight: "100%"
}}
>
@@ -311,7 +311,6 @@ export default function LinkNote(props: {
style={{
marginTop: 10
}}
keyboardShouldPersistTaps="handled"
windowSize={3}
keyExtractor={(item) => item.id}
data={nodes}
@@ -325,7 +324,6 @@ export default function LinkNote(props: {
onSelectNote={onSelectNote}
/>
)}
keyboardShouldPersistTaps="handled"
style={{
marginTop: 10
}}

View File

@@ -99,7 +99,6 @@ export default function Migrate() {
const { error } = await BackupService.run(false, "local");
if (error) {
ToastManager.error(error, "Backup failed");
reportError(error);
setLoading(false);
return;
}

View File

@@ -21,11 +21,15 @@ import { useThemeColors } from "@notesnook/theme";
import React, { useCallback } from "react";
import { View } from "react-native";
import { DraxProvider, DraxScrollView } from "react-native-drax";
import { notesnook } from "../../../e2e/test.ids";
import { db } from "../../common/database";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { DDS } from "../../services/device-detection";
import { eSendEvent } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { useMenuStore } from "../../stores/use-menu-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { useThemeStore } from "../../stores/use-theme-store";
import { useUserStore } from "../../stores/use-user-store";
import { SUBSCRIPTION_STATUS } from "../../utils/constants";
import { eOpenPremiumDialog } from "../../utils/events";
@@ -55,6 +59,26 @@ export const SideMenu = React.memo(
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
const BottomItemsList = [
{
name: isDark ? "Day" : "Night",
icon: "theme-light-dark",
func: () => {
useThemeStore.getState().setColorScheme();
},
switch: true,
on: !!isDark,
close: false
},
{
name: "Settings",
icon: "cog-outline",
close: true,
func: () => {
Navigation.navigate("Settings");
}
}
];
const pro = {
name: "Notesnook Pro",
@@ -97,7 +121,7 @@ export const SideMenu = React.memo(
<PinnedSection />
</>
),
[order, hiddensItems]
[]
);
return !isAppLoading && introCompleted ? (

View File

@@ -59,7 +59,7 @@ export interface TabsRef {
lock: () => boolean;
openDrawer: (animated?: boolean) => void;
closeDrawer: (animated?: boolean) => void;
page: () => number;
page: number;
setScrollEnabled: () => true;
isDrawerOpen: () => boolean;
node: RefObject<Animated.View>;
@@ -239,7 +239,7 @@ export const FluidTabs = forwardRef<TabsRef, TabProps>(function FluidTabs(
onDrawerStateChange(false);
isDrawerOpen.value = false;
},
page: () => currentTab.value,
page: currentTab.value,
setScrollEnabled: () => true,
node: node
}),

View File

@@ -65,7 +65,6 @@ import {
eClearEditor,
eCloseFullscreenEditor,
eOnEnterEditor,
eOnExitEditor,
eOnLoadNote,
eOpenFullscreenEditor,
eUnlockNote
@@ -512,7 +511,6 @@ const onChangeTab = async (event) => {
editorState().movedAway = false;
editorState().isFocused = true;
activateKeepAwake();
eSendEvent(eOnEnterEditor);
if (!useTabStore.getState().getCurrentNoteId()) {
eSendEvent(eOnLoadNote, {
@@ -524,13 +522,14 @@ const onChangeTab = async (event) => {
) {
eSendEvent(eUnlockNote);
}
eSendEvent(eOnEnterEditor);
}
} else {
if (event.from === 2) {
deactivateKeepAwake();
editorState().movedAway = true;
editorState().isFocused = false;
eSendEvent(eOnExitEditor);
eSendEvent(eClearEditor, "removeHandler");
// Lock all tabs with locked notes...
for (const tab of useTabStore.getState().tabs) {

View File

@@ -51,11 +51,7 @@ import { EditorProps, useEditorType } from "./tiptap/types";
import { useEditor } from "./tiptap/use-editor";
import { useEditorEvents } from "./tiptap/use-editor-events";
import { syncTabs, useTabStore } from "./tiptap/use-tab-store";
import {
editorController,
editorState,
openInternalLink
} from "./tiptap/utils";
import { editorController, editorState } from "./tiptap/utils";
const style: ViewStyle = {
height: "100%",
@@ -65,10 +61,7 @@ const style: ViewStyle = {
backgroundColor: "transparent"
};
const onShouldStartLoadWithRequest = (request: ShouldStartLoadRequest) => {
if (request.url.includes("nn://")) {
openInternalLink(request.url);
return false;
} else if (request.url.includes("https")) {
if (request.url.includes("https")) {
if (Platform.OS === "ios" && !request.isTopFrame) return true;
openLinkInBrowser(request.url);
return false;

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 @typescript-eslint/no-var-requires */
import { parseInternalLink } from "@notesnook/core";
import { ItemReference } from "@notesnook/core/dist/types";
import type { Attachment } from "@notesnook/editor/dist/extensions/attachment/index";
import { getDefaultPresets } from "@notesnook/editor/dist/toolbar/tool-definitions";
@@ -26,6 +27,7 @@ import Clipboard from "@react-native-clipboard/clipboard";
import React, { useCallback, useEffect, useRef } from "react";
import {
BackHandler,
InteractionManager,
Keyboard,
KeyboardEventListener,
NativeEventSubscription,
@@ -35,7 +37,6 @@ import { WebViewMessageEvent } from "react-native-webview";
import { DatabaseLogger, db } from "../../../common/database";
import downloadAttachment from "../../../common/filesystem/download-attachment";
import EditorTabs from "../../../components/sheets/editor-tabs";
import { Issue } from "../../../components/sheets/github/issue";
import LinkNote from "../../../components/sheets/link-note";
import ManageTagsSheet from "../../../components/sheets/manage-tags";
import { RelationsList } from "../../../components/sheets/relations-list";
@@ -60,7 +61,6 @@ import {
eCloseFullscreenEditor,
eEditorTabFocused,
eOnEnterEditor,
eOnExitEditor,
eOnLoadNote,
eOpenFullscreenEditor,
eOpenLoginDialog,
@@ -75,8 +75,8 @@ import { useDragState } from "../../settings/editor/state";
import { EventTypes } from "./editor-events";
import { EditorMessage, EditorProps, useEditorType } from "./types";
import { useTabStore } from "./use-tab-store";
import { EditorEvents, editorState, openInternalLink } from "./utils";
import { EditorEvents, editorState } from "./utils";
import { Issue } from "../../../components/sheets/github/issue";
const publishNote = async () => {
const user = useUserStore.getState().user;
@@ -270,20 +270,21 @@ export const useEditorEvents = (
}, [editor, deviceMode, fullscreen]);
const onHardwareBackPress = useCallback(() => {
console.log(tabBarRef.current?.page());
if (tabBarRef.current?.page() === 2) {
if (tabBarRef.current?.page === 2) {
onBackPress();
return true;
}
}, [onBackPress]);
const onEnterEditor = useCallback(async () => {
if (!DDS.isTab) {
handleBack.current = BackHandler.addEventListener(
"hardwareBackPress",
onHardwareBackPress
);
}
InteractionManager.runAfterInteractions(() => {
if (!DDS.isTab) {
handleBack.current = BackHandler.addEventListener(
"hardwareBackPress",
onHardwareBackPress
);
}
});
}, [onHardwareBackPress]);
const onClearEditorSessionRequest = useCallback(
@@ -330,14 +331,7 @@ export const useEditorEvents = (
}, [fullscreen, onHardwareBackPress]);
useEffect(() => {
const onExitEditor = () => {
if (handleBack.current) {
handleBack.current.remove();
}
};
eSubscribeEvent(eOnEnterEditor, onEnterEditor);
eSubscribeEvent(eOnExitEditor, onExitEditor);
eSubscribeEvent(
eClearEditor + editor.editorId,
onClearEditorSessionRequest
@@ -345,7 +339,6 @@ export const useEditorEvents = (
return () => {
eUnSubscribeEvent(eClearEditor, onClearEditorSessionRequest);
eUnSubscribeEvent(eOnEnterEditor, onEnterEditor);
eUnSubscribeEvent(eOnExitEditor, onExitEditor);
};
}, [editor.editorId, onClearEditorSessionRequest, onEnterEditor]);
@@ -526,7 +519,27 @@ export const useEditorEvents = (
break;
case EventTypes.link:
if (editorMessage.value.startsWith("nn://")) {
openInternalLink(editorMessage.value);
const data = parseInternalLink(editorMessage.value);
if (!data?.id) break;
if (
data.id ===
useTabStore
.getState()
.getNoteIdForTab(useTabStore.getState().currentTab)
) {
if (data.params?.blockId) {
setTimeout(() => {
if (!data.params?.blockId) return;
editor.commands.scrollIntoViewById(data.params.blockId);
}, 150);
}
return;
}
eSendEvent(eOnLoadNote, {
item: await db.notes.note(data?.id),
blockId: data.params?.blockId
});
console.log(
"Opening note from internal link:",
editorMessage.value
@@ -649,8 +662,10 @@ export const useEditorEvents = (
.updateTab(useTabStore.getState().currentTab, {
readonly: false
});
setTimeout(() => {
Navigation.queueRoutesForUpdate();
Navigation.queueRoutesForUpdate();
ToastManager.show({
heading: "Readonly mode disabled.",
type: "success"
});
}
break;

View File

@@ -837,7 +837,7 @@ export const useEditor = (
if (!noteId) {
overlay(false);
loadNote({ newNote: true });
if (tabBarRef.current?.page() === 1) {
if (tabBarRef.current?.page === 1) {
state.current.currentlyEditing = false;
}
}

View File

@@ -22,15 +22,10 @@ import { TextInput } from "react-native";
import WebView from "react-native-webview";
import { MMKV } from "../../../common/database/mmkv";
import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent
} from "../../../services/event-manager";
import { AppState, EditorState, useEditorType } from "./types";
import { useTabStore } from "./use-tab-store";
import { parseInternalLink } from "@notesnook/core";
import { eOnLoadNote } from "../../../utils/events";
import { db } from "../../../common/database";
export const textInput = createRef<TextInput>();
export const editorController =
createRef<useEditorType>() as MutableRefObject<useEditorType>;
@@ -181,27 +176,3 @@ export function clearAppState() {
appState = undefined;
MMKV.removeItem("appState");
}
export async function openInternalLink(url: string) {
const data = parseInternalLink(url);
if (!data?.id) return false;
if (
data.id ===
useTabStore.getState().getNoteIdForTab(useTabStore.getState().currentTab)
) {
if (data.params?.blockId) {
setTimeout(() => {
if (!data.params?.blockId) return;
editorController.current.commands.scrollIntoViewById(
data.params.blockId
);
}, 150);
}
return;
}
eSendEvent(eOnLoadNote, {
item: await db.notes.note(data?.id),
blockId: data.params?.blockId
});
}

View File

@@ -31,7 +31,6 @@ import { eCloseSheet } from "../utils/events";
import { sleep } from "../utils/time";
import { ToastManager, eSendEvent, presentSheet } from "./event-manager";
import SettingsService from "./settings";
import { useUserStore } from "../stores/use-user-store";
const MS_DAY = 86400000;
const MS_WEEK = MS_DAY * 7;
@@ -192,10 +191,9 @@ async function run(progress = false, context) {
await RNFetchBlob.fs.mkdir(zipSourceFolder);
try {
const user = await db.user.getUser();
for await (const file of db.backup.export(
"mobile",
SettingsService.get().encryptedBackup && user
SettingsService.get().encryptedBackup
)) {
console.log("Writing backup chunk of size...", file?.data?.length);
await RNFetchBlob.fs.writeFile(

View File

@@ -72,7 +72,7 @@ export const useMenuStore = create<MenuStore>((set, get) => ({
section as SideBarSection
);
hiddenItems[section as SideBarHideableSection] =
db.settings.getSideBarHiddenItems(section as SideBarHideableSection);
db.settings.getSideBarHiddenItems("colors");
}
if (
@@ -80,7 +80,6 @@ export const useMenuStore = create<MenuStore>((set, get) => ({
JSON.stringify(get().hiddenItems || {}) !==
JSON.stringify(hiddenItems || {})
) {
console.log(order, hiddenItems);
set({
order: order,
hiddenItems: hiddenItems

View File

@@ -173,4 +173,3 @@ export const eUnlockWithBiometrics = "618";
export const eUnlockWithPassword = "619";
export const eUpdateNoteInEditor = "620";
export const eOnEnterEditor = "621";
export const eOnExitEditor = "622";

View File

@@ -142,13 +142,8 @@ function DesktopAppContents({
}, [show]);
useEffect(() => {
if (isFocusMode) {
const middlePaneSize = middlePane.current?.getSize() || 20;
navPane.current?.collapse();
// the middle pane has to be resized because collapsing the nav
// pane increases the middle pane's size every time.
middlePane.current?.resize(middlePaneSize);
} else navPane.current?.expand();
if (isFocusMode) navPane.current?.collapse();
else navPane.current?.expand();
}, [isFocusMode]);
return (
@@ -164,10 +159,9 @@ function DesktopAppContents({
ref={navPane}
className="nav-pane"
defaultSize={10}
minSize={3.5}
onResize={(size) => setIsNarrow(size <= 5)}
minSize={3}
onResize={(size) => setIsNarrow(size <= 3)}
collapsible
collapsedSize={3.5}
>
<NavigationMenu
toggleNavigationContainer={(state) => {
@@ -198,7 +192,7 @@ function DesktopAppContents({
</ScopedThemeProvider>
</Panel>
<PanelResizeHandle className="panel-resize-handle" />
<Panel className="editor-pane" defaultSize={70}>
<Panel className="editor-pane">
<Flex
sx={{
display: "flex",

View File

@@ -77,18 +77,18 @@ export async function introduceFeatures() {
export const DEFAULT_CONTEXT = { colors: [], tags: [], notebook: {} };
export async function createBackup(rescueMode = false) {
export async function createBackup() {
const { isLoggedIn } = useUserStore.getState();
const { encryptBackups, toggleEncryptBackups } = useSettingStore.getState();
if (!isLoggedIn && encryptBackups) toggleEncryptBackups();
const verified = rescueMode || encryptBackups || (await verifyAccount());
const verified = encryptBackups || (await verifyAccount());
if (!verified) {
showToast("error", "Could not create a backup: user verification failed.");
return false;
return;
}
const encryptedBackups = !rescueMode && isLoggedIn && encryptBackups;
const encryptedBackups = isLoggedIn && encryptBackups;
const filename = sanitizeFilename(
`${formatDate(Date.now(), {
@@ -139,9 +139,7 @@ export async function createBackup(rescueMode = false) {
console.error(error);
} else {
showToast("success", `Backup saved at ${filePath}.`);
return true;
}
return false;
}
export async function selectBackupFile() {

View File

@@ -87,6 +87,7 @@ function saveContent(noteId: string, ignoreEdit: boolean, content: string) {
const deferredSave = debounceWithId(saveContent, 100);
export default function TabsView() {
const sessions = useEditorStore((store) => store.sessions);
const documentPreview = useEditorStore((store) => store.documentPreview);
const activeSessionId = useEditorStore((store) => store.activeSessionId);
const arePropertiesVisible = useEditorStore(
@@ -95,7 +96,6 @@ export default function TabsView() {
const isTOCVisible = useEditorStore((store) => store.isTOCVisible);
const [dropRef, overlayRef] = useDragOverlay();
const sessions = useEditorStore.getState().sessions;
return (
<>
{IS_DESKTOP_APP ? (

View File

@@ -46,7 +46,6 @@ import { Section } from "../properties";
import { scrollIntoViewById } from "@notesnook/editor";
import { Button, Flex, Text } from "@theme-ui/components";
import { useEditorManager } from "./manager";
import { TITLE_BAR_HEIGHT } from "../title-bar";
type TableOfContentsProps = {
sessionId: string;
@@ -101,7 +100,7 @@ function TableOfContents(props: TableOfContentsProps) {
display: "flex",
position: "absolute",
right: 0,
top: TITLE_BAR_HEIGHT,
top: 0,
zIndex: 999,
height: "100%",
width: "300px",

View File

@@ -29,8 +29,6 @@ import {
ZoomIn,
ZoomOut
} from "../icons";
import { getPlatform } from "../../utils/platform";
import { TITLE_BAR_HEIGHT } from "../title-bar";
const DEFAULT_ZOOM_STEP = 0.3;
const DEFAULT_LARGE_ZOOM = 4;
@@ -339,12 +337,7 @@ export class Lightbox extends React.Component<LightboxProps> {
borderRadius: "0px 0px 0px 5px",
overflow: "hidden",
alignItems: "center",
justifyContent: "flex-end",
height: IS_DESKTOP_APP ? TITLE_BAR_HEIGHT : "auto",
pr:
IS_DESKTOP_APP && getPlatform() !== "darwin"
? "calc(100vw - env(titlebar-area-width))"
: 0
justifyContent: "flex-end"
}}
>
{tools.map((tool) => (
@@ -356,7 +349,6 @@ export class Lightbox extends React.Component<LightboxProps> {
bg="transparent"
title={tool.title}
sx={{
height: "100%",
borderRadius: 0,
display: [
tool.hideOnMobile ? "none" : "flex",

View File

@@ -106,7 +106,7 @@ function NavigationItem(
<Button
data-test-id={`navigation-item`}
sx={{
px: isTablet ? 1 : 2,
px: 2,
flex: 1,
alignItems: "center",
justifyContent: isTablet ? "center" : "flex-start",
@@ -124,10 +124,7 @@ function NavigationItem(
}}
>
{image ? (
<Image
src={image}
sx={{ borderRadius: 50, size: 20, minWidth: 20, flexShrink: 0 }}
/>
<Image src={image} sx={{ borderRadius: 50, size: 20 }} />
) : Icon ? (
<Icon
size={isTablet ? 16 : 15}

View File

@@ -282,8 +282,7 @@ export default React.memo(Note, function (prevProps, nextProps) {
prevProps.notebooks?.dateEdited === nextProps.notebooks?.dateEdited &&
prevProps.tags?.dateEdited === nextProps.tags?.dateEdited &&
prevProps.reminder?.dateModified === nextProps.reminder?.dateModified &&
prevProps.attachments?.failed === nextProps.attachments?.failed &&
prevProps.attachments?.total === nextProps.attachments?.total &&
prevProps.attachments === nextProps.attachments &&
prevProps.locked === nextProps.locked
);
});

View File

@@ -61,7 +61,6 @@ import {
} from "@notesnook/core";
import { VirtualizedTable } from "../virtualized-table";
import { TextSlice } from "@notesnook/core/dist/utils/content-block";
import { TITLE_BAR_HEIGHT } from "../title-bar";
const tools = [
{ key: "pin", property: "pinned", icon: Pin, label: "Pin" },
@@ -130,7 +129,7 @@ function EditorProperties(props: EditorPropertiesProps) {
sx={{
display: "flex",
position: "absolute",
top: TITLE_BAR_HEIGHT,
top: 0,
right: 0,
zIndex: 999,
height: "100%",

View File

@@ -56,7 +56,6 @@ function Toggle(props: ToggleProps) {
<Switch
sx={{ m: 0, bg: isOn ? "accent" : "icon-secondary" }}
checked={isOn}
onClick={(e) => e.stopPropagation()}
/>
</Flex>
);

View File

@@ -29,7 +29,6 @@ import {
} from "../icons";
import { BaseThemeProvider } from "../theme-provider";
export const TITLE_BAR_HEIGHT = IS_DESKTOP_APP ? 37.8 : 0;
export function TitleBar() {
const { isMaximized, isFullscreen, hasNativeWindowControls } =
useWindowControls();
@@ -66,7 +65,7 @@ export function TitleBar() {
scope="titleBar"
sx={{
background: "background",
height: TITLE_BAR_HEIGHT,
height: 37.8,
display: "flex",
borderBottom: "1px solid var(--border)",
...(!isFullscreen && hasNativeWindowControls

View File

@@ -185,16 +185,9 @@ export default function NoteLinkingDialog(props: NoteLinkingDialogProps) {
autoFocus
placeholder="Search for a note to link to..."
sx={{ mx: 0 }}
onChange={async (e) => {
const query = e.target.value.trim();
setNotes(
query
? await db.lookup.notes(e.target.value).sorted()
: await db.notes.all.sorted(
db.settings.getGroupOptions("home")
)
);
}}
onChange={async (e) =>
setNotes(await db.lookup.notes(e.target.value).sorted())
}
/>
{notes && (
<ScrollContainer>

View File

@@ -44,8 +44,7 @@ export const AppearanceSettings: SettingsGroup[] = [
type: "input",
inputType: "number",
min: 0.5,
max: 3.0,
step: 0.1,
max: 2.0,
defaultValue: () => useSettingStore.getState().zoomFactor,
onChange: (value) => useSettingStore.getState().setZoomFactor(value)
}

View File

@@ -46,7 +46,7 @@ export const AuthenticationSettings: SettingsGroup[] = [
title: "Change password",
variant: "secondary",
action: async () => {
if (!(await createBackup())) return;
await createBackup();
const result = await showPasswordDialog({
title: "Change account password",
message: `All your data will be re-encrypted and synced with the new password.

View File

@@ -508,7 +508,6 @@ function SettingItem(props: { item: Setting }) {
type={"number"}
min={component.min}
max={component.max}
step={component.step}
defaultValue={component.defaultValue()}
sx={{ width: 80, mr: 1 }}
onChange={debounce((e) => {

View File

@@ -111,26 +111,6 @@ What data is collected & when?`,
section: "privacy",
header: "Advanced",
settings: [
{
key: "custom-dns",
title: "Use custom DNS",
description: `Notesnook uses the following DNS providers:
1. Cloudflare DNS
2. Quad9
This can sometimes bypass local ISP blockages on Notesnook traffic. Disable this if you want the app to use system's DNS settings.`,
onStateChange: (listener) =>
useSettingStore.subscribe((s) => s.customDns, listener),
isHidden: () => !IS_DESKTOP_APP,
components: [
{
type: "toggle",
isToggled: () => useSettingStore.getState().customDns,
toggle: () => useSettingStore.getState().toggleCustomDns()
}
]
},
{
key: "custom-cors",
title: "Custom CORS proxy",

View File

@@ -134,7 +134,6 @@ export type NumberInputSettingComponent = BaseSettingComponent<"input"> & {
inputType: "number";
min: number;
max: number;
step?: number;
defaultValue: () => number;
onChange: (value: number) => void;
};

View File

@@ -40,7 +40,7 @@ async function renderApp() {
const { useKeyStore } = await import("./interfaces/key-store");
await useKeyStore.getState().init();
// if (serviceWorkerWhitelist.includes(path)) await initializeServiceWorker();
if (serviceWorkerWhitelist.includes(path)) await initializeServiceWorker();
const { default: Component } = await component();
const { default: AppLock } = await import("./views/app-lock");

View File

@@ -438,11 +438,7 @@ class EditorStore extends BaseStore<EditorStore> {
if (index === -1) return;
const session = state.sessions[index] as SessionTypeMap[T[number]];
if (typeof partial === "function") partial(session);
else {
for (const key in partial) {
session[key] = partial[key] as any;
}
}
else state.sessions[index] = { ...session, ...partial };
});
};

View File

@@ -42,7 +42,6 @@ class SettingStore extends BaseStore<SettingStore> {
zoomFactor = 1.0;
privacyMode = false;
customDns = true;
hideNoteTitle = Config.get("hideNoteTitle", false);
telemetry = isTelemetryEnabled();
dateFormat = "DD-MM-YYYY";
@@ -68,7 +67,6 @@ class SettingStore extends BaseStore<SettingStore> {
desktopIntegrationSettings:
await desktop?.integration.desktopIntegration.query(),
privacyMode: await desktop?.integration.privacyMode.query(),
customDns: await desktop?.integration.customDns.query(),
zoomFactor: await desktop?.integration.zoomFactor.query(),
autoUpdates: await desktop?.updater.autoUpdates.query(),
proxyRules: await desktop?.integration.proxyRules.query()
@@ -179,12 +177,6 @@ class SettingStore extends BaseStore<SettingStore> {
await desktop?.integration.setPrivacyMode.mutate({ enabled: !privacyMode });
};
toggleCustomDns = async () => {
const customDns = this.get().customDns;
this.set({ customDns: !customDns });
await desktop?.integration.setCustomDns.mutate(!customDns);
};
toggleHideTitle = async () => {
const { hideNoteTitle } = this.get();
this.set({ hideNoteTitle: !hideNoteTitle });

View File

@@ -460,7 +460,7 @@ function BackupData(props: BaseRecoveryComponentProps<"backup">) {
"Please wait while we create a backup file for you to download."
}}
onSubmit={async () => {
await createBackup(true);
await createBackup();
navigate("new");
}}
>

View File

@@ -1,176 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { expect, test } from "vitest";
import { TEST_NOTE, databaseTest, noteTest } from "./utils";
test("updating deleted content should not throw", () =>
databaseTest().then(async (db) => {
const id = await db.notes.add({
title: "New note"
});
const contentId = await db.content.add({
data: "helloworld",
noteId: id
});
await db.content.remove(contentId!);
await expect(
db.content.collection.update(
[contentId!],
{ synced: true },
{ sendEvent: false }
)
).resolves.toBeFalsy();
}));
test("updating content should not break full text search", () =>
databaseTest().then(async (db) => {
const id = await db.notes.add({
title: "New note"
});
const contentId = await db.content.add({
data: "hello world",
noteId: id
});
await db.content.add({
id: contentId,
data: "i am amazing",
noteId: id
});
expect(await db.lookup.notes("amazing").ids()).toContain(id);
expect(await db.lookup.notes("hello world").ids()).not.toContain(id);
}));
test("updating note title should not break full text search", () =>
databaseTest().then(async (db) => {
const id = await db.notes.add({
title: "New note"
});
await db.notes.add({
id,
title: "What an amazing note!"
});
expect(await db.lookup.notes("amazing").ids()).toContain(id);
expect(await db.lookup.notes("new note").ids()).not.toContain(id);
}));
test("updating deleted note should not throw", () =>
databaseTest().then(async (db) => {
const id = await db.notes.add({
title: "New note"
});
await db.notes.remove(id);
await expect(
db.notes.collection.update([id], { synced: true }, { sendEvent: false })
).resolves.toBeFalsy();
}));
test("overwriting unlocked content with locked content should update search index", () =>
noteTest({
content: {
data: "hello world",
type: "tiptap"
}
}).then(async ({ db, id }) => {
await db.content.collection.upsert({
id: "something",
locked: false,
data: "What is this?",
noteId: id,
dateCreated: Date.now(),
dateEdited: Date.now(),
synced: false,
dateModified: Date.now()
});
await db.content.collection.put([
{
id: "something",
locked: true,
data: {
alg: "as",
cipher: "s",
format: "base64",
iv: "",
length: 20,
salt: ""
},
noteId: id,
dateCreated: Date.now(),
dateEdited: Date.now(),
synced: false,
dateModified: Date.now()
}
]);
expect(await db.lookup.notes("what is this").ids()).not.toContain(id);
}));
test("overwriting content with deleted content should update search index", () =>
noteTest({
content: {
data: "hello world",
type: "tiptap"
}
}).then(async ({ db, id }) => {
await db.content.collection.upsert({
id: "something",
locked: false,
data: "What is this?",
noteId: id,
dateCreated: Date.now(),
dateEdited: Date.now(),
synced: false,
dateModified: Date.now()
});
await db.content.collection.put([
{
id: "something",
deleted: true,
synced: true,
dateModified: Date.now()
}
]);
expect(await db.lookup.notes("what is this").ids()).not.toContain(id);
}));
test("overwriting note with deleted note should update search index", () =>
noteTest({
title: "I am title"
}).then(async ({ db, id }) => {
await db.notes.collection.put([
{
id,
deleted: true,
synced: true,
dateModified: Date.now()
}
]);
expect(await db.lookup.notes("title").ids()).not.toContain(id);
}));

View File

@@ -24,7 +24,6 @@ import { DatabaseSchema, RawDatabaseSchema } from "../database";
import { AnyColumnWithTable, Kysely, sql } from "kysely";
import { FilteredSelector } from "../database/sql-collection";
import { VirtualizedGrouping } from "../utils/virtualized-grouping";
import { logger } from "../logger";
type SearchResults<T> = {
sorted: (limit?: number) => Promise<VirtualizedGrouping<T>>;
@@ -45,6 +44,8 @@ export default class Lookup {
if (query.length < 3) return [];
const db = this.db.sql() as unknown as Kysely<RawDatabaseSchema>;
query = query.replace(/"/, '""');
const excludedIds = this.db.trash.cache.notes;
const results = await db
.selectFrom((eb) =>
@@ -56,7 +57,7 @@ export default class Lookup {
.$if(excludedIds.length > 0, (eb) =>
eb.where("id", "not in", excludedIds)
)
.where("title", "match", query)
.where("title", "match", `"${query}"`)
.select(["id", sql<number>`rank * 10`.as("rank")])
.unionAll((eb) =>
eb
@@ -67,7 +68,7 @@ export default class Lookup {
.$if(excludedIds.length > 0, (eb) =>
eb.where("id", "not in", excludedIds)
)
.where("data", "match", query)
.where("data", "match", `"${query}"`)
.select(["noteId as id", "rank"])
.$castTo<{ id: string; rank: number }>()
)
@@ -84,11 +85,7 @@ export default class Lookup {
"in",
(notes || this.db.notes.all).filter.select("id")
)
.execute()
.catch((e) => {
logger.error(e, `Error while searching`, { query });
return [];
});
.execute();
return results.map((r) => r.id);
}, notes || this.db.notes.all);
}

View File

@@ -44,6 +44,7 @@ import {
Kysely,
SelectQueryBuilder,
SqlBool,
Transaction,
sql
} from "kysely";
import { VirtualizedGrouping } from "../utils/virtualized-grouping";
@@ -337,10 +338,8 @@ export class SQLCollection<
}
export class FilteredSelector<T extends Item> {
private _fields: (
| AnyColumn<DatabaseSchema, keyof DatabaseSchema>
| AnyColumnWithTable<DatabaseSchema, keyof DatabaseSchema>
)[] = [];
private _fields: AnyColumnWithTable<DatabaseSchema, keyof DatabaseSchema>[] =
[];
filter: SelectQueryBuilder<DatabaseSchema, keyof DatabaseSchema, unknown>;
private _limit = 0;
constructor(
@@ -525,11 +524,6 @@ export class FilteredSelector<T extends Item> {
async *[Symbol.asyncIterator]() {
let lastRow: any | null = null;
const fields = this._fields.slice();
if (!fields.find((f) => f.includes(".dateCreated")))
fields.push("dateCreated");
if (!fields.find((f) => f.includes(".id"))) fields.push("id");
while (true) {
const rows = await this.filter
.orderBy("dateCreated asc")
@@ -542,8 +536,8 @@ export class FilteredSelector<T extends Item> {
)
)
.limit(this.batchSize)
.$if(fields.length === 0, (eb) => eb.selectAll())
.$if(fields.length > 0, (eb) => eb.select(fields))
.$if(this._fields.length === 0, (eb) => eb.selectAll())
.$if(this._fields.length > 0, (eb) => eb.select(this._fields))
.execute();
if (rows.length === 0) break;
for (const row of rows) {

View File

@@ -31,8 +31,8 @@ export async function createTriggers(db: Kysely<RawDatabaseSchema>) {
.addEvent("insert")
.when((eb) =>
eb.and([
eb("new.deleted", "is not", true),
eb("new.locked", "is not", true),
eb.or([eb("new.deleted", "is", null), eb("new.deleted", "==", false)]),
eb.or([eb("new.locked", "is", null), eb("new.locked", "==", false)]),
eb("new.data", "is not", null)
])
)
@@ -71,13 +71,6 @@ export async function createTriggers(db: Kysely<RawDatabaseSchema>) {
.onTable("content", "main")
.after()
.addEvent("update")
.when((eb) =>
eb.and([
eb("old.deleted", "is not", true),
eb("old.noteId", "is not", null),
eb("old.data", "is not", null)
])
)
.addQuery((c) =>
c.insertInto("content_fts").values({
content_fts: sql.lit("delete"),
@@ -142,13 +135,7 @@ export async function createTriggers(db: Kysely<RawDatabaseSchema>) {
.ifNotExists()
.onTable("notes", "main")
.after()
.addEvent("update")
.when((eb) =>
eb.and([
eb("old.deleted", "is not", true),
eb("old.title", "is not", null)
])
)
.addEvent("update", ["title"])
.addQuery((c) =>
c.insertInto("notes_fts").values({
notes_fts: sql.lit("delete"),

View File

@@ -331,10 +331,7 @@ function Header({
<ControlledMenu
align="end"
anchorPoint={{
x: window.innerWidth - 10,
y: 70
}}
anchorRef={btnRef}
state={isOpen ? "open" : "closed"}
menuClassName={menuClassName}
onClose={() => {

View File

@@ -19,7 +19,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Extension } from "@tiptap/core";
import { Decoration, DecorationSet } from "prosemirror-view";
import { EditorState, Plugin, PluginKey, Transaction } from "prosemirror-state";
import {
EditorState,
Plugin,
PluginKey,
TextSelection,
Transaction
} from "prosemirror-state";
import { SearchSettings } from "../../toolbar/stores/search-store";
type DispatchFn = (tr: Transaction) => void;
@@ -56,10 +62,7 @@ export type SearchStorage = {
interface TextNodesWithPosition {
text: string;
startPos: number;
endPos: number;
start: number;
end: number;
pos: number;
}
const updateView = (state: EditorState, dispatch: DispatchFn) => {
@@ -68,19 +71,15 @@ const updateView = (state: EditorState, dispatch: DispatchFn) => {
dispatch(state.tr);
};
const regex = (s: string, settings: SearchSettings): RegExp | undefined => {
const regex = (s: string, settings: SearchSettings): RegExp => {
const { enableRegex, matchCase, matchWholeWord } = settings;
const boundary = matchWholeWord ? "\\b" : "";
try {
return RegExp(
boundary +
(enableRegex ? s : s.replace(/[/\\^$*+?.()|[\]]/g, "\\$&")) +
boundary,
matchCase ? "gum" : "guim"
);
} catch (e) {
console.error(e);
}
return RegExp(
boundary +
(enableRegex ? s : s.replace(/[/\\^$*+?.()|[\]]/g, "\\$&")) +
boundary,
matchCase ? "gu" : "gui"
);
};
function searchDocument(
@@ -99,61 +98,41 @@ function searchDocument(
const doc = tr.doc;
const results: Result[] = [];
let index = -1;
const textNodesWithPosition: TextNodesWithPosition[] = [];
let index = 0;
let textNodesWithPosition: TextNodesWithPosition[] = [];
let cursor = 0;
doc?.descendants((node, pos) => {
if (node.isText) {
if (textNodesWithPosition[index]) {
textNodesWithPosition[index].text += node.text;
textNodesWithPosition[index].end = cursor + (node.text?.length || 0);
textNodesWithPosition[index] = {
text: textNodesWithPosition[index].text + node.text,
pos: textNodesWithPosition[index].pos
};
} else {
textNodesWithPosition[index] = {
text: node.text || "",
startPos: pos,
endPos: pos + node.nodeSize,
start: cursor,
end: cursor + (node.text?.length || 0)
pos
};
}
cursor += node.text?.length || 0;
} else if (node.isBlock) {
const lastNode = textNodesWithPosition[index];
if (lastNode) {
lastNode.text += "\n";
lastNode.end++;
lastNode.endPos = pos;
cursor++;
}
index++;
} else {
index += 1;
}
});
textNodesWithPosition = textNodesWithPosition.filter(Boolean);
const text = textNodesWithPosition.map((c) => c.text).join("");
for (const match of text.matchAll(searchTerm)) {
const start = match.index;
const end = match.index + match[0].length;
for (const { text, pos } of textNodesWithPosition) {
const matches = text.matchAll(searchTerm);
// Gets all matching nodes that have either the start or end of the
// search term in them. This adds support for multi line regex searches.
const nodes = textNodesWithPosition.filter((node) => {
const nodeStart = node.start;
const nodeEnd = node.end;
return (
(start >= nodeStart && start < nodeEnd) ||
(end >= nodeStart && end < nodeEnd)
);
});
for (const m of matches) {
if (m[0] === "") break;
if (!nodes.length) continue;
const endNode = nodes[nodes.length - 1];
const startNode = nodes[0];
results.push({
// reposition our RegExp match index relative to the actual node.
from: start + (startNode.startPos - startNode.start),
to: end + (endNode.endPos - endNode.end)
});
if (m.index !== undefined) {
results.push({
from: pos + m.index,
to: pos + m.index + m[0].length
});
}
}
}
const { from: selectedFrom, to: selectedTo } = tr.selection;
@@ -320,8 +299,24 @@ export const SearchReplace = Extension.create<SearchOptions, SearchStorage>({
const { from, to } = results[index];
tr.insertText(term, from, to);
if (index + 1 < results.length) {
const { from, to } = results[index + 1];
const nextResult = (results[index + 1] = {
from: tr.mapping.map(from),
to: tr.mapping.map(to)
});
commands.focus();
tr.setSelection(
new TextSelection(
tr.doc.resolve(nextResult.from),
tr.doc.resolve(nextResult.to)
)
);
}
dispatch(tr);
commands.moveToNextResult();
results.splice(index, 1);
return true;
},
replaceAll:

View File

@@ -50,7 +50,7 @@ export function SearchReplacePopup(props: SearchReplacePopupProps) {
matchWholeWord
});
},
[editor.commands, matchCase, enableRegex, matchWholeWord]
[matchCase, enableRegex, matchWholeWord]
);
useEffect(() => {
@@ -94,7 +94,7 @@ export function SearchReplacePopup(props: SearchReplacePopupProps) {
ref={searchInputRef}
autoFocus
placeholder="Find"
sx={{ p: 0, fontFamily: "monospace" }}
sx={{ p: 0 }}
value={searchTerm}
onChange={(e) => {
search(e.target.value);
@@ -102,8 +102,7 @@ export function SearchReplacePopup(props: SearchReplacePopupProps) {
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
if (e.shiftKey) editor.commands.moveToPreviousResult();
else editor.commands.moveToNextResult();
editor.commands.moveToNextResult();
}
}}
/>
@@ -137,10 +136,9 @@ export function SearchReplacePopup(props: SearchReplacePopupProps) {
title="Match case"
id="matchCase"
icon="caseSensitive"
onClick={() => {
useEditorSearchStore.setState({ matchCase: !matchCase });
search(useEditorSearchStore.getState().searchTerm);
}}
onClick={() =>
useEditorSearchStore.setState({ matchCase: !matchCase })
}
iconSize={"medium"}
/>
<ToolButton
@@ -151,12 +149,11 @@ export function SearchReplacePopup(props: SearchReplacePopupProps) {
title="Match whole word"
id="matchWholeWord"
icon="wholeWord"
onClick={() => {
onClick={() =>
useEditorSearchStore.setState({
matchWholeWord: !matchWholeWord
});
search(useEditorSearchStore.getState().searchTerm);
}}
})
}
iconSize={"medium"}
/>
<ToolButton
@@ -167,12 +164,11 @@ export function SearchReplacePopup(props: SearchReplacePopupProps) {
title="Enable regex"
id="enableRegex"
icon="regex"
onClick={() => {
onClick={() =>
useEditorSearchStore.setState({
enableRegex: !enableRegex
});
search(useEditorSearchStore.getState().searchTerm);
}}
})
}
iconSize={"medium"}
/>
</>
@@ -194,7 +190,7 @@ export function SearchReplacePopup(props: SearchReplacePopupProps) {
</Flex>
{isReplacing && (
<Input
sx={{ mt: 1, p: "7px", fontFamily: "monospace" }}
sx={{ mt: 1, p: "7px" }}
placeholder="Replace"
value={replaceTerm}
onChange={(e) =>
@@ -255,11 +251,7 @@ export function SearchReplacePopup(props: SearchReplacePopupProps) {
title="Replace"
id="replace"
icon="replaceOne"
onClick={() =>
editor.commands.replace(
useEditorSearchStore.getState().replaceTerm
)
}
onClick={() => editor.commands.replace(replaceTerm)}
sx={{ mr: 0 }}
iconSize={18}
/>
@@ -268,11 +260,7 @@ export function SearchReplacePopup(props: SearchReplacePopupProps) {
title="Replace all"
id="replaceAll"
icon="replaceAll"
onClick={() =>
editor.commands.replaceAll(
useEditorSearchStore.getState().replaceTerm
)
}
onClick={() => editor.commands.replaceAll(replaceTerm)}
sx={{ mr: 0 }}
iconSize={18}
/>

View File

@@ -6,7 +6,7 @@
font-family: inherit;
}
.ProseMirror p.is-editor-empty::before {
.ProseMirror p.is-empty:first-child::before {
color: var(--placeholder);
content: attr(data-placeholder);
float: left;