Compare commits

...

40 Commits

Author SHA1 Message Date
Ammar Ahmed
614f228e6e Merge branch 'master' into beta 2026-08-05 08:42:26 +05:00
Ammar Ahmed
b8a02640d6 Merge pull request #10193 from streetwriters/mobile/release-3.4.8
mobile: release v3.4.8
2026-08-04 19:24:16 +05:00
Ammar Ahmed
31dbbe5e9e mobile: fix rn 82 build on xcode 26.5 2026-08-04 12:11:54 +05:00
Ammar Ahmed
427dcbbb20 mobile: release v3.4.8 2026-08-03 19:30:12 +05:00
Ammar Ahmed
d3d21aa835 Merge pull request #10161 from streetwriters/fix/android-widgets
Fix android widget bugs.
2026-08-03 19:29:33 +05:00
Ammar Ahmed
d60c8be423 Merge pull request #10019 from kashaf-ansari-dev/feat/9694-new-reminder-shortcut
mobile: add app shortcut for creating reminders
2026-08-03 12:28:09 +05:00
Ammar Ahmed
516a598b2b Merge pull request #10166 from kashaf-ansari-dev/fix-10135-image-note-save
mobile: fix images sometimes not saving after insert
2026-08-03 09:58:06 +05:00
Abdullah Atta
f858e3471f desktop: set snap base to core24 (#10180) 2026-07-31 12:41:56 +05:00
kashaf-ansari-dev
57113e0896 mobile: remove AddReminder from stack on navigation away to prevent stale form state
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-31 09:49:36 +05:00
kashaf-ansari-dev
28f2ffdbf7 mobile: remove unused ignoreEdit handling
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-30 23:29:32 +05:00
kashaf-ansari-dev
02c60d14c7 editor: skip ignoreEdit updates before scheduling save
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-30 23:29:00 +05:00
kashaf-ansari-dev
41cdc882c9 editor: fix ignoreEdit flag overwritten during debounced save
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-30 23:29:00 +05:00
Suyadi
a1879dae17 mobile: fix error on android's search query (#10106)
* mobile: pass value for items when searching on main screen to match other screens' pattern
Signed-off-by: Suyadi <afsuyadi@gmail.com>

* core: guard notesWithHighlighting against an undefined notes selector.
Signed-off-by: Suyadi <afsuyadi@gmail.com>

* core: add regression test for notesWithHighlighting crash.
Signed-off-by: Suyadi <afsuyadi@gmail.com>

* core: revert changes after feedback
Signed-off-by: Suyadi <afsuyadi@gmail.com>

* mobile: ensure type: "note" will make items become mandatory.
Signed-off-by: Suyadi <afsuyadi@gmail.com>

* core: remove tests after feedback
Signed-off-by: Suyadi <afsuyadi@gmail.com>
2026-07-30 23:01:21 +05:00
Ammar Ahmed
4765657423 mobile: fix editor cross contamination (#10157)
1. Use noteId defined in editorMessage when saving content, never use tabId since it can point to a different note since a tab can load a different note while a note save message is coming across the bridge.
2. If saving fails and editor saves save payloads in localStorage, add the noteId to them so we know which note the content belongs to.
3. Pending content saves must keep noteId and edit time so when we save them later, they save into the correct note and if the content is newer, we skip saving.
4. Fix the debounce key so cross contamination can never occur in new notes between two tabs.
2026-07-30 22:55:06 +05:00
Ammar Ahmed
e5b97abe0d Merge branch 'master' into beta 2026-07-30 12:35:39 +05:00
Abdullah Atta
f7c755f2d6 editor: sort code block languages by recently used (#10144) 2026-07-30 08:57:38 +05:00
Kashaf Ansari
10a08d5719 mobile: fix add notes to notebook selection behavior (#10104)
* mobile: fix add notes to notebook selection behavior

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

* mobile: improve move notes keyboard interactions

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

* mobile: fix bottom inset when keyboard is open

---------

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
Co-authored-by: Ammar Ahmed <ammarahmed6506@gmail.com>
2026-07-30 08:56:18 +05:00
Kashaf Ansari
6e10ce8d8e mobile: add sync status button to side menu (#10112)
* mobile: add sync status button to side menu

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

* mobile: minor fixes

* mobile: hide sync status during logout

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

* mobile: skipped sync during logout

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>

---------

Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
Co-authored-by: Ammar Ahmed <ammarahmed6506@gmail.com>
2026-07-30 08:54:05 +05:00
Ammar Ahmed
ae1811cc4a Merge branch 'master' into beta 2026-07-29 12:50:50 +05:00
Abdullah Atta
b5140d9947 Merge pull request #10159 from streetwriters/release/3.4.5
web: bump version to 3.4.5
2026-07-28 21:09:08 +05:00
Abdullah Atta
2288b946ad desktop: launch app with --hidden flag for tests 2026-07-28 19:53:37 +05:00
Abdullah Atta
5fd43ae08e desktop: fix auto start with minimized window on macos 2026-07-28 19:53:37 +05:00
Abdullah Atta
729c410602 global: update intl & lockfiles 2026-07-28 19:53:37 +05:00
Abdullah Atta
bb30336f5c desktop: add tests for #9942 2026-07-28 19:52:48 +05:00
01zulfi
0dc89ba945 web: bump version to 3.4.5
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-07-28 19:52:47 +05:00
Abdullah Atta
bb51244fa3 editor: update prosemirror packages (#10158)
* editor: update prosemirror packages

* editor: fix `__serializeForClipboard` usage
2026-07-28 19:46:51 +05:00
Abdullah Atta
b5ac41c29b web: improve "skip & go directly to app" wording (#10167) 2026-07-28 19:46:23 +05:00
01zulfi
aafb6049b2 core: delete all vaults when user triggers their vault deletion (#9939)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-07-28 19:45:30 +05:00
Abdullah Atta
32070327f9 core: add logs to diagnose why decryption fails after changing password (#10139) 2026-07-27 10:20:25 +05:00
Ammar Ahmed
e4fa0e9f0b mobile: fix ios app does not load if awaited RNBootSplash.hide 2026-07-25 08:54:44 +05:00
kashaf-ansari-dev
97749729ad mobile: improve startup loading shortcuts
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-24 18:57:54 +05:00
Ammar Ahmed
ca19fd9230 mobile: fix new note loading from shortcut on cold launch 2026-07-24 18:57:54 +05:00
kashaf-ansari-dev
c435ab46d1 mobile: simplified new editor load logic
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-24 18:57:54 +05:00
kashaf-ansari-dev
62ecc5f7fb mobile: resolve app shortcut navigation and add reminder feature gate
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-24 18:57:54 +05:00
kashaf-ansari-dev
3e2847f844 mobile: add fluid panels initialRoute to simplified navigation
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-24 18:57:54 +05:00
kashaf-ansari-dev
c3c9b1fbf4 mobile: fix app shortcut navigation, flash, and back-nav issues
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-24 18:57:53 +05:00
kashaf-ansari-dev
a40814a3ab mobile: revise app shortcut handling implementation
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-24 18:57:53 +05:00
kashaf-ansari-dev
180c202c4e mobile: handle app shortcuts correctly during cold and warm app launches
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-24 18:57:53 +05:00
kashaf-ansari-dev
64943647f8 mobile: add app shortcut for creating reminders
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-07-24 18:57:53 +05:00
Ammar Ahmed
774d612bf2 misc: cleanup 2026-07-07 09:50:36 +05:00
63 changed files with 1262 additions and 1779 deletions

View File

@@ -42,6 +42,7 @@ export interface AppContext {
export interface TestOptions {
version: string;
args?: string[];
config?: Record<string, unknown>;
}
@@ -72,7 +73,8 @@ export async function buildAndLaunchApp(
const { app } = await launchApp(
executablePath,
userDataDir,
options?.version
options?.version,
options?.args
);
const ctx: AppContext = {
app,
@@ -82,7 +84,8 @@ export async function buildAndLaunchApp(
const { app } = await launchApp(
executablePath,
userDataDir,
options?.version
options?.version,
options?.args
);
ctx.app = app;
ctx.userDataDir = userDataDir;
@@ -94,11 +97,12 @@ export async function buildAndLaunchApp(
async function launchApp(
executablePath: string,
userDataDir: string,
version?: string
version?: string,
args: string[] = []
) {
const app = await electron.launch({
executablePath,
args: IS_DEBUG ? [] : ["--hidden"],
args: IS_DEBUG ? [...args] : ["--hidden", ...args],
baseURL: "https://app.notesnook.com",
acceptDownloads: true,
env: {
@@ -154,6 +158,7 @@ export async function buildApp(version?: string) {
stdio: IS_DEBUG ? "inherit" : "ignore",
env: {
...process.env,
CSC_IDENTITY_AUTO_DISCOVERY: "false",
NOTESNOOK_STAGING: "true",
NN_PRODUCT_NAME: productName,
NN_APP_ID: `com.notesnook.test.${productName}`,

View File

@@ -17,7 +17,65 @@ 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 { test } from "@nn/test";
import { test, expect } from "@nn/test";
import type { ElectronApplication } from "@playwright/test";
async function getMainWindowState(app: ElectronApplication) {
return await app.evaluate((window) => {
const { BrowserWindow } = window;
const mainWindow = BrowserWindow.getAllWindows()[0];
if (!mainWindow) throw new Error("Main window not found");
return {
isMinimized: mainWindow.isMinimized(),
isVisible: mainWindow.isVisible()
};
});
}
test("make sure app loads", async ({ page }) => {
await page.waitForSelector(".ProseMirror");
});
test("hidden launch minimizes when tray is disabled", async ({
launchElectronApp,
options
}) => {
const app = await launchElectronApp({
version: options.version,
args: ["--hidden"],
config: {
desktopSettings: {
minimizeToSystemTray: false,
closeToSystemTray: false
}
}
});
const page = await app.firstWindow();
await page.waitForSelector(".ProseMirror");
const state = await getMainWindowState(app);
expect(state.isMinimized).toBe(true);
});
test("hidden launch does not minimize when close-to-tray is enabled", async ({
launchElectronApp,
options
}) => {
const app = await launchElectronApp({
version: options.version,
args: ["--hidden"],
config: {
desktopSettings: {
minimizeToSystemTray: false,
closeToSystemTray: true
}
}
});
const page = await app.firstWindow();
await page.waitForSelector(".ProseMirror");
const state = await getMainWindowState(app);
expect(state.isMinimized).toBe(false);
});

View File

@@ -203,11 +203,12 @@ module.exports = {
toolsets: {
appimage: "1.0.2"
},
snap: {
autoStart: false,
confinement: "strict",
allowNativeWayland: true,
base: "core22"
snapcraft: {
base: "core24",
core24: {
confinement: "strict",
autoStart: false
}
},
extraResources: ["app-update.yml", "./assets/**"],
extraMetadata: {

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "3.4.4",
"version": "3.4.5",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/cjs/index.js",

View File

@@ -92,6 +92,17 @@ async function createWindow() {
const cliOptions = await parseArguments(process.argv);
setTheme(getTheme());
// this workaround is necessary because macos doesn't support
// the --hidden flag when launching the app on startup
if (
process.platform === "darwin" &&
app.getLoginItemSettings().wasOpenedAtLogin &&
config.desktopSettings.autoStart &&
config.desktopSettings.startMinimized
) {
cliOptions.hidden = true;
}
const mainWindowState = new WindowState({});
const mainWindow = new BrowserWindow({
show: !cliOptions.hidden,
@@ -141,8 +152,13 @@ async function createWindow() {
mainWindow.setMenuBarVisibility(false);
mainWindowState.manage(mainWindow);
if (cliOptions.hidden && !(config.desktopSettings.minimizeToSystemTray
|| config.desktopSettings.closeToSystemTray))
if (
cliOptions.hidden &&
!(
config.desktopSettings.minimizeToSystemTray ||
config.desktopSettings.closeToSystemTray
)
)
mainWindow.minimize();
await mainWindow.webContents.loadURL(`${createURL(cliOptions, "/")}`);

View File

@@ -140,7 +140,7 @@ android {
if (project.hasProperty("prBuildNumber")) {
versionCode Integer.parseInt(prBuildNumber())
} else {
versionCode 3113
versionCode 3114
}
versionName getNpmVersion()
testBuildType System.getProperty('testBuildType', 'debug')

View File

@@ -1,4 +1,6 @@
- Add option to clear note version history
- Added sync status icon in sidebar
- Added new reminder shortcut in app icon context menu
- Improved editor saving reliability
- Minor bug fixes and improvements
Thank you for using Notesnook!

View File

@@ -23,7 +23,7 @@ import {
THEME_COMPATIBILITY_VERSION,
useThemeEngineStore
} from "@notesnook/theme";
import React, { PropsWithChildren, useEffect } from "react";
import React, { PropsWithChildren, useEffect, useState } from "react";
import { Appearance, I18nManager, Linking, StatusBar } from "react-native";
import "react-native-gesture-handler";
import { GestureHandlerRootView } from "react-native-gesture-handler";
@@ -44,26 +44,37 @@ import { useUserStore } from "./stores/use-user-store";
import RNBootSplash from "react-native-bootsplash";
import AppLocked from "./components/app-lock";
import { useSettingStore } from "./stores/use-setting-store";
import {
initShortcutListener,
launchNewNoteTab,
registerAppShortcuts
} from "./hooks/use-shortcut-manager";
import Shortcuts from "react-native-actions-shortcuts";
I18nManager.allowRTL(false);
I18nManager.forceRTL(false);
I18nManager.swapLeftAndRightInRTL(false);
const { appLockEnabled, appLockMode } = SettingsService.get();
if (appLockEnabled || appLockMode !== "none") {
useUserStore.getState().lockApp(true);
}
RNBootSplash.hide({
fade: true
});
Linking.getInitialURL().then((url) => {
useSettingStore.setState({
initialUrl: url
});
});
const App = (props: { configureMode: "note-preview" }) => {
useAppEvents();
//@ts-ignore
globalThis["IS_MAIN_APP_RUNNING"] = true;
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
useEffect(() => {
if (introCompleted) {
registerAppShortcuts();
}
}, [introCompleted]);
useEffect(() => {
RNBootSplash.hide({ fade: true });
SettingsService.onFirstLaunch();
changeSystemBarColors();
SettingsService.setPrivacyScreen(
@@ -176,4 +187,41 @@ export const withTheme = (
};
};
export default withTheme(withErrorBoundry(App, "App"));
export const withStartupBoundry = (
Element: (props: PropsWithChildren) => JSX.Element
) => {
return function AppWithStartupBoundary(props: PropsWithChildren) {
const [ready, setReady] = useState(false);
useEffect(() => {
async function init() {
try {
const [url, shortcut] = await Promise.all([
Linking.getInitialURL(),
Shortcuts.getInitialShortcut()
]);
console.log(url, shortcut);
if (shortcut?.type === "notesnook.action.newnote") {
launchNewNoteTab();
}
useSettingStore.setState({
initialUrl: url,
pendingShortcut: shortcut ?? null
});
initShortcutListener();
} finally {
setReady(true);
}
}
init();
}, []);
if (!ready) return null;
return <Element {...props} />;
};
};
export default withStartupBoundry(withTheme(withErrorBoundry(App, "App")));

View File

@@ -36,6 +36,7 @@ import { getElevationStyle } from "../../utils/elevation";
import { AppFontSize, normalize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { hexToRGBA, RGB_Linear_Shade } from "../../utils/colors";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
interface FloatingButtonProps {
onPress: () => void;
@@ -47,6 +48,7 @@ interface FloatingButtonProps {
position?: "left" | "right";
size?: "small" | "large";
style?: ViewStyle;
hideOnKeyboard?: boolean;
}
const FloatingButton = ({
@@ -57,26 +59,28 @@ const FloatingButton = ({
testID,
position = "right",
size = "large",
style
style,
hideOnKeyboard = true
}: FloatingButtonProps) => {
const { colors } = useThemeColors();
const deviceMode = useSettingStore((state) => state.deviceMode);
const selectionMode = useSelectionStore((state) => state.selectionMode);
const translate = useSharedValue(0);
const keyboardHeight = useSharedValue(0);
const route = useRoute();
const insets = useGlobalSafeAreaInsets();
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateX: translate.value
},
{
translateY: translate.value
}
]
};
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{
translateX: hideOnKeyboard ? translate.value : 0
},
{
translateY: hideOnKeyboard ? translate.value : 0
}
],
bottom: hideOnKeyboard ? 15 : keyboardHeight.value + 15
}));
const animate = useCallback(
(toValue: number) => {
@@ -97,12 +101,22 @@ const FloatingButton = ({
editorState().keyboardState = false;
if (deviceMode !== "mobile") return;
animate(0);
keyboardHeight.value = withTiming(0);
};
const onKeyboardShow = () => {
const onKeyboardShow = (e: any) => {
editorState().keyboardState = true;
if (deviceMode !== "mobile") return;
animate(150);
if (hideOnKeyboard) {
animate(150);
} else {
keyboardHeight.value = withTiming(
e.endCoordinates.height - insets.bottom,
{
duration: 250
}
);
}
};
const sub = [
@@ -110,9 +124,9 @@ const FloatingButton = ({
Keyboard.addListener("keyboardDidHide", onKeyboardHide)
];
return () => {
sub.forEach((sub) => sub?.remove?.());
sub.forEach((sub) => sub.remove());
};
}, [deviceMode, animate]);
}, [deviceMode, animate, hideOnKeyboard, keyboardHeight, insets.bottom]);
return deviceMode !== "mobile" && !alwaysVisible ? null : (
<Animated.View
@@ -161,10 +175,10 @@ const FloatingButton = ({
icon
? icon
: route.name === "Notebooks"
? "notebook-plus"
: route.name === "Trash"
? "delete"
: "plus"
? "notebook-plus"
: route.name === "Trash"
? "delete"
: "plus"
}
color={color || colors.primary.accent}
size={size === "small" ? AppFontSize.xl : AppFontSize.xxxl}

View File

@@ -143,9 +143,6 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
}, [hide, show]);
const onNegativePress = async () => {
if (dialogInfo?.onClose) {
await dialogInfo.onClose();
}
hide();
};

View File

@@ -51,9 +51,10 @@ interface TabProps extends ViewProps {
onScroll: (offset: number) => void;
enabled: boolean;
onDrawerStateChange: (state: boolean) => void;
initialPage?: FluidTabPage;
}
type FluidTabPage = "home" | "editor";
export type FluidTabPage = "home" | "editor";
export interface TabsRef {
goToPage: (page: FluidTabPage, animated?: boolean) => void;
@@ -77,15 +78,21 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
onChangeTab,
onScroll,
enabled,
onDrawerStateChange
onDrawerStateChange,
initialPage
}: TabProps,
ref
) {
const deviceMode = useSettingStore((state) => state.deviceMode);
const fullscreen = useSettingStore((state) => state.fullscreen);
const translateX = useSharedValue(widths ? widths.sidebar : 0);
const editorStartPosition = widths.sidebar + widths.list;
const translateX = useSharedValue(
initialPage === "editor" ? editorStartPosition : widths ? widths.sidebar : 0
);
const startX = useSharedValue(0);
const currentTab = useSharedValue(1);
const currentTab = useSharedValue(
initialPage === "editor" && deviceMode !== "tablet" ? 2 : 1
);
const previousTab = useSharedValue(1);
const isDrawerOpen = useSharedValue(false);
const gestureStartValue = useSharedValue({

View File

@@ -30,6 +30,7 @@ import { Pressable } from "../ui/pressable";
import { SvgView } from "../ui/svg";
import Heading from "../ui/typography/heading";
import { useSideBarDraggingStore } from "./dragging-store";
import SyncStatusButton from "./sync-status-button";
const SettingsIcon = () => {
const { colors } = useThemeColors();
@@ -123,7 +124,7 @@ export const SideMenuHeader = (props: { rightButtons?: IconButtonProps[] }) => {
size={AppFontSize.lg}
/>
))}
<SyncStatusButton />
<SettingsIcon />
</View>
</View>

View File

@@ -0,0 +1,161 @@
/*
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 { useTimeAgo } from "@notesnook/common";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import { useNetInfo } from "@react-native-community/netinfo";
import React from "react";
import { View } from "react-native";
import Animated, {
Easing,
cancelAnimation,
useAnimatedStyle,
useSharedValue,
withRepeat,
withTiming
} from "react-native-reanimated";
import Sync from "../../services/sync";
import { SyncStatus, useUserStore } from "../../stores/use-user-store";
import { AppFontSize } from "../../utils/size";
import NativeTooltip from "../../utils/tooltip";
import AppIcon from "../ui/AppIcon";
import { IconButton } from "../ui/icon-button";
const SyncStatusButton = () => {
const { colors } = useThemeColors();
const [user, syncing, lastSyncStatus, lastSynced, isLoggingOut] =
useUserStore((state) => [
state.user,
state.syncing,
state.lastSyncStatus,
state.lastSynced,
state.isLoggingOut
]);
const { isInternetReachable } = useNetInfo();
const isOffline = !isInternetReachable;
const hasSyncedBefore = lastSynced && lastSynced !== "Never";
const isFailed = lastSyncStatus === SyncStatus.Failed;
const isSynced = lastSyncStatus === SyncStatus.Passed;
const lastSyncedTimeAgo = useTimeAgo(lastSynced, {
interval: 5000,
live: true
});
const getIconColor = (): string => {
if (syncing) return colors.primary.accent;
if (isSynced) return colors.primary.accent;
return colors.secondary.icon;
};
const rotation = useSharedValue(0);
React.useEffect(() => {
if (syncing && !isOffline) {
rotation.value = 0;
rotation.value = withRepeat(
withTiming(360, { duration: 1000, easing: Easing.linear }),
-1,
false
);
} else {
cancelAnimation(rotation);
rotation.value = withTiming(360, {
duration: 1000 * (1 - (rotation.value % 360) / 360),
easing: Easing.linear
});
}
}, [syncing, rotation, isOffline]);
const rotationStyle = useAnimatedStyle(() => ({
transform: [{ rotate: `${rotation.value}deg` }]
}));
const tooltipText = React.useMemo(() => {
const offlineSuffix = isOffline ? ` (${strings.offline()})` : "";
if (syncing) return strings.syncing();
if (!hasSyncedBefore) return `${strings.never()}${offlineSuffix}`;
if (isFailed) return `${strings.syncFailed()}${offlineSuffix}`;
return `${strings.synced()}${lastSyncedTimeAgo}${offlineSuffix ? `${offlineSuffix}` : ""}`;
}, [isOffline, syncing, hasSyncedBefore, isFailed, lastSyncedTimeAgo]);
const onPress = () => {
if (syncing) return;
Sync.run();
};
if (!user || isLoggingOut) return null;
return (
<View
style={{
justifyContent: "center",
alignItems: "center",
position: "relative"
}}
>
{syncing ? (
<View
style={{
width: 28,
height: 28,
borderRadius: 6,
borderWidth: 1,
borderColor: colors.primary.border,
justifyContent: "center",
alignItems: "center"
}}
>
<Animated.View style={rotationStyle}>
<AppIcon name="sync" size={AppFontSize.lg} color={getIconColor()} />
</Animated.View>
</View>
) : (
<IconButton
name="sync"
onPress={onPress}
tooltipText={tooltipText}
tooltipPosition={NativeTooltip.POSITIONS.BOTTOM}
size={AppFontSize.lg}
color={getIconColor()}
style={{
width: 28,
height: 28,
borderRadius: 6,
borderWidth: 1,
borderColor: colors.primary.border
}}
/>
)}
{!syncing && (isFailed || isOffline) ? (
<AppIcon
name="information"
color={isOffline ? colors.static.yellow : colors.error.icon}
size={AppFontSize.xxs}
style={{ position: "absolute", bottom: -3, right: -3 }}
/>
) : null}
</View>
);
};
export default SyncStatusButton;

View File

@@ -346,6 +346,7 @@ const onLogout = async (reason: string) => {
SettingsService.resetSettings();
useUserStore.getState().setUser(null);
useUserStore.getState().setSyncing(false);
useUserStore.getState().setIsLoggingOut(false);
eSendEvent(eAfterSync);
};

View File

@@ -17,54 +17,67 @@ 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 Shortcuts, { ShortcutItem } from "react-native-actions-shortcuts";
import { useEffect } from "react";
import { NativeEventEmitter, NativeModule } from "react-native";
import { useRef } from "react";
import { Platform } from "react-native";
import { NativeEventEmitter, NativeModule, Platform } from "react-native";
import deviceInfoModule from "react-native-device-info";
import { strings } from "@notesnook/intl";
import { useSettingStore } from "../stores/use-setting-store";
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
const ShortcutsEmitter = new NativeEventEmitter(
Shortcuts as unknown as NativeModule
);
function isSupported() {
export function isShortcutsSupported() {
return Platform.OS !== "android" || deviceInfoModule.getApiLevelSync() > 25;
}
const defaultShortcuts: ShortcutItem[] = [
{
type: "notesnook.action.newnote",
title: strings.createNewNote(),
shortTitle: strings.newNote(),
iconName: Platform.OS === "android" ? "ic_newnote" : "plus"
},
{
type: "notesnook.action.newreminder",
title: strings.setReminder(),
shortTitle: strings.newReminder(),
iconName: Platform.OS === "android" ? "ic_newnote" : "plus"
}
];
export const useShortcutManager = ({
onShortcutPressed,
shortcuts = defaultShortcuts
}: {
onShortcutPressed: (shortcut: ShortcutItem | null) => void;
shortcuts?: ShortcutItem[];
}) => {
const initialShortcutRecieved = useRef(false);
useEffect(() => {
if (!isSupported()) return;
Shortcuts.setShortcuts(shortcuts);
}, [shortcuts]);
export function registerAppShortcuts(
shortcuts: ShortcutItem[] = defaultShortcuts
) {
if (!isShortcutsSupported()) return;
Shortcuts.setShortcuts(shortcuts);
}
useEffect(() => {
if (!isSupported()) return;
Shortcuts.getInitialShortcut().then((shortcut) => {
if (initialShortcutRecieved.current || !shortcut) return;
onShortcutPressed(shortcut);
initialShortcutRecieved.current = true;
});
const subscription = ShortcutsEmitter.addListener(
"onShortcutItemPressed",
onShortcutPressed
);
return () => {
subscription?.remove();
};
}, [onShortcutPressed]);
};
let listenerInitialized = false;
export function initShortcutListener() {
if (!isShortcutsSupported() || listenerInitialized) return;
listenerInitialized = true;
ShortcutsEmitter.addListener(
"onShortcutItemPressed",
(shortcut: ShortcutItem) => {
console.time("shortcut");
useSettingStore.setState({ pendingShortcut: shortcut });
}
);
}
export function launchNewNoteTab() {
let tabId;
const currentTab = useTabStore
.getState()
.getTab(useTabStore.getState().currentTab as string);
if (useTabStore.getState().tabs.length === 0 || currentTab?.pinned) {
tabId = useTabStore.getState().newTab();
} else {
tabId = useTabStore.getState().currentTab;
if (useTabStore.getState().getTab(tabId)?.session?.noteId) {
useTabStore.getState().newTabSession(tabId, {});
}
}
}

View File

@@ -41,10 +41,9 @@ import Animated, {
} from "react-native-reanimated";
import { notesnook } from "../../e2e/test.ids";
import { db } from "../common/database";
import { FluidPanels } from "../components/fluid-panels";
import { FluidPanels, FluidTabPage } from "../components/fluid-panels";
import { useSideBarDraggingStore } from "../components/side-menu/dragging-store";
import useGlobalSafeAreaInsets from "../hooks/use-global-safe-area-insets";
import { useShortcutManager } from "../hooks/use-shortcut-manager";
import { hideAllTooltips } from "../hooks/use-tooltip";
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
import { editorController, editorState } from "../screens/editor/tiptap/utils";
@@ -59,7 +58,6 @@ import {
eCloseFullscreenEditor,
eOnEnterEditor,
eOnExitEditor,
eOnLoadNote,
eOpenFullscreenEditor,
eUnlockNote
} from "../utils/events";
@@ -67,6 +65,7 @@ import { valueLimiter } from "../utils/functions";
import { fluidTabsRef } from "../utils/global-refs";
import { AppNavigationStack } from "./navigation-stack";
import type { PaneWidths } from "../screens/editor/wrapper";
import { NavigationProps } from "../services/navigation";
const MOBILE_SIDEBAR_SIZE = 0.85;
@@ -74,7 +73,7 @@ let SideMenu: any = null;
let EditorWrapper: any = null;
export const FluidPanelsView = React.memo(
() => {
({ route }: NavigationProps<"FluidPanelsView">) => {
const { colors } = useThemeColors();
const deviceMode = useSettingStore((state) => state.deviceMode);
const setFullscreen = useSettingStore((state) => state.setFullscreen);
@@ -102,6 +101,15 @@ export const FluidPanelsView = React.memo(
setOrientation(o);
}
});
React.useEffect(() => {
const shortcut = useSettingStore.getState().pendingShortcut;
if (shortcut?.type === "notesnook.action.newnote") {
useSettingStore.setState({
pendingShortcut: null
});
}
}, []);
useEffect(() => {
if (!appLoading) {
@@ -111,29 +119,6 @@ export const FluidPanelsView = React.memo(
}
}, [appLoading]);
useShortcutManager({
onShortcutPressed: async (item) => {
if (!item) return;
if (item?.type === "notesnook.action.newnote") {
if (!fluidTabsRef.current) {
setTimeout(() => {
eSendEvent(eOnLoadNote, { newNote: true });
editorState().movedAway = false;
fluidTabsRef.current?.goToPage("editor", false);
}, 1000);
return;
}
eSendEvent(eOnLoadNote, { newNote: true });
editorState().movedAway = false;
setTimeout(
() => fluidTabsRef.current?.goToPage("editor", false),
300
);
}
}
});
const showFullScreenEditor = useCallback(() => {
setFullscreen(true);
if (deviceMode === "smallTablet") {
@@ -356,6 +341,7 @@ export const FluidPanelsView = React.memo(
dimensions={dimensions}
widths={PANE_WIDTHS[deviceMode as keyof typeof PANE_WIDTHS]}
enabled={deviceMode !== "tablet" && !fullscreen}
initialPage={route.params?.initialPage}
onScroll={onScroll}
onChangeTab={onChangeTab}
onDrawerStateChange={(state) => {

View File

@@ -27,10 +27,17 @@ import useNavigationStore, {
} from "../stores/use-navigation-store";
import { useSelectionStore } from "../stores/use-selection-store";
import { useSettingStore } from "../stores/use-setting-store";
import { rootNavigatorRef } from "../utils/global-refs";
import { fluidTabsRef, rootNavigatorRef } from "../utils/global-refs";
import Navigation from "../services/navigation";
import { isFeatureAvailable } from "@notesnook/common";
import { isFeatureAvailable, useIsFeatureAvailable } from "@notesnook/common";
import { isInternalLink, parseInternalLink } from "@notesnook/core";
import { eSendEvent } from "../services/event-manager";
import { editorState } from "../screens/editor/tiptap/utils";
import { eOnLoadNote } from "../utils/events";
import { strings } from "@notesnook/intl";
import PaywallSheet from "../components/sheets/paywall";
import { presentDialog } from "../components/dialog/functions";
import { launchNewNoteTab } from "../hooks/use-shortcut-manager";
const RootStack = createNativeStackNavigator();
const AppStack = createNativeStackNavigator();
@@ -300,8 +307,15 @@ export const RootNavigation = () => {
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
const initialShortcut = React.useRef(
useSettingStore.getState().pendingShortcut
).current;
const reminderFeature = useIsFeatureAvailable("activeReminders");
const clearSelection = useSelectionStore((state) => state.clearSelection);
const resetTimer = React.useRef<NodeJS.Timeout>(undefined);
const onStateChange = React.useCallback(
(state: any) => {
if (useSelectionStore.getState().selectionMode) {
@@ -316,13 +330,66 @@ export const RootNavigation = () => {
[clearSelection]
);
React.useEffect(() => {
const unsubscribe = useSettingStore.subscribe((state, prevState) => {
const pendingShortcut = state.pendingShortcut;
if (pendingShortcut === prevState.pendingShortcut || !pendingShortcut) {
return;
}
if (pendingShortcut.type === "notesnook.action.newreminder") {
if (reminderFeature === undefined) return;
if (!reminderFeature.isAllowed) {
presentDialog({
title: strings.upgrade(),
paragraph: reminderFeature.error,
positiveText: strings.upgrade(),
negativeText: strings.cancel(),
positivePress: async () => {
PaywallSheet.present(reminderFeature);
}
});
useSettingStore.setState({
pendingShortcut: null
});
return;
}
rootNavigatorRef.current?.navigate("AddReminder" as any);
} else if (pendingShortcut.type === "notesnook.action.newnote") {
if (fluidTabsRef.current) {
rootNavigatorRef.current?.navigate("FluidPanelsView" as any);
eSendEvent(eOnLoadNote, { newNote: true });
editorState().movedAway = false;
fluidTabsRef.current.goToPage("editor", true);
} else {
launchNewNoteTab();
rootNavigatorRef.current?.navigate("FluidPanelsView" as any, {
initialPage: "editor"
});
}
}
});
return unsubscribe;
}, [reminderFeature]);
const initialRouteName = !introCompleted
? "Welcome"
: initialShortcut?.type === "notesnook.action.newreminder"
? "AddReminder"
: "FluidPanelsView";
return (
<NavigationContainer onStateChange={onStateChange} ref={rootNavigatorRef}>
<RootStack.Navigator
screenOptions={{
headerShown: false
}}
initialRouteName={introCompleted ? "FluidPanelsView" : "Welcome"}
initialRouteName={initialRouteName}
>
<RootStack.Screen
name="Welcome"
@@ -347,6 +414,12 @@ export const RootNavigation = () => {
require("../navigation/fluid-panels-view").default;
return FluidPanelsView;
}}
initialParams={{
initialPage:
initialShortcut?.type === "notesnook.action.newnote"
? "editor"
: undefined
}}
/>
<RootStack.Screen

View File

@@ -20,8 +20,9 @@ import { Note, Reminder } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import dayjs from "dayjs";
import React, { useRef, useState } from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import {
BackHandler,
KeyboardAvoidingView,
Platform,
ScrollView,
@@ -63,6 +64,7 @@ import FormInput, {
validators
} from "../../components/ui/input/form-input";
import AppIcon from "../../components/ui/AppIcon";
import { presentDialog } from "../../components/dialog/functions";
const ReminderModes =
Platform.OS === "ios"
@@ -94,7 +96,7 @@ const ReminderNotificationModes = {
};
export default function AddReminder(props: NavigationProps<"AddReminder">) {
const { reminder, reference } = props.route.params;
const { reminder, reference } = props.route.params ?? {};
useNavigationFocus(props.navigation, {
focusOnInit: true,
onFocus: () => {
@@ -106,6 +108,23 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
return false;
}
});
const handleBackNavigation = useCallback(() => {
const routes = props.navigation.getState()?.routes;
if (routes && routes.length <= 1) {
props.navigation.navigate("FluidPanelsView" as any);
return true;
}
Navigation.goBack();
return true;
}, [props.navigation]);
useEffect(() => {
const sub = BackHandler.addEventListener("hardwareBackPress", () => {
return handleBackNavigation();
});
return () => sub.remove();
}, [handleBackNavigation]);
const { colors, isDark } = useThemeColors();
const weekFormat = useSettingStore((state) => state.weekFormat);
const [reminderMode, setReminderMode] = useState<Reminder["mode"]>(
@@ -127,6 +146,7 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
const [repeatFrequency, setRepeatFrequency] = useState(1);
const referencedItem = reference ? (reference as Note) : null;
const recurringReminderFeature = useIsFeatureAvailable("recurringReminders");
const activeReminderFeature = useIsFeatureAvailable("activeReminders");
const formRef = useRef(
createFormRef({
title: reminder?.title || referencedItem?.title || "",
@@ -153,6 +173,31 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
);
const [dateError, setDateError] = useState<string>();
const [selectDayError, setSelectDayError] = useState<string>();
React.useEffect(() => {
const shortcut = useSettingStore.getState().pendingShortcut;
if (shortcut?.type === "notesnook.action.newreminder") {
useSettingStore.setState({
pendingShortcut: null
});
}
}, []);
useEffect(() => {
if (activeReminderFeature === undefined) return;
if (!activeReminderFeature.isAllowed) {
presentDialog({
title: strings.upgrade(),
paragraph: activeReminderFeature.error,
positiveText: strings.upgrade(),
negativeText: strings.cancel(),
positivePress: async () => {
PaywallSheet.present(activeReminderFeature);
},
onClose: () => {
props.navigation.navigate("FluidPanelsView" as any);
}
});
}
}, [activeReminderFeature]);
const showDatePicker = () => {
setDatePickerVisibility(true);
@@ -243,7 +288,7 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
Notifications.scheduleNotification(_reminder as Reminder);
Navigation.queueRoutesForUpdate();
useRelationStore.getState().update();
Navigation.goBack();
handleBackNavigation();
} catch (e) {
ToastManager.error(e as Error, undefined);
}
@@ -267,12 +312,12 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
<Header
title={reminder ? strings.editReminder() : strings.newReminder()}
canGoBack
onLeftMenuButtonPress={handleBackNavigation}
rightButton={{
name: "check",
onPress: saveReminder
}}
/>
<Dialog context="local" />
<ScrollView
style={{
marginBottom: DDS.isTab ? 25 : undefined,

View File

@@ -90,9 +90,10 @@ export type SavePayload = {
data?: string;
type?: "tiptap";
sessionHistoryId?: number;
ignoreEdit: boolean;
tabId: string;
pendingChanges?: boolean;
sourceNoteId?: string;
pendingChangesAt?: number;
};
export type AppState = {

View File

@@ -149,7 +149,7 @@ const showActionsheet = async () => {
}
};
type ContentMessage = { html: string; ignoreEdit: boolean };
type ContentMessage = { html: string };
export const useEditorEvents = (
editor: useEditorType,
@@ -411,16 +411,19 @@ export const useEditorEvents = (
.getState()
.getNoteIdForTab(editorMessage.tabId);
const saveNoteId = editorMessage.noteId || noteId;
switch (editorMessage.type) {
case EditorEvents.content:
DatabaseLogger.log("EditorEvents.content");
editor.saveContent({
type: editorMessage.type,
content: editorMessage.value.html as string,
noteId: noteId,
noteId: saveNoteId,
sourceNoteId: editorMessage.noteId,
tabId: editorMessage.tabId,
ignoreEdit: (editorMessage.value as ContentMessage).ignoreEdit,
pendingChanges: editorMessage.value?.pendingChanges
pendingChanges: editorMessage.value?.pendingChanges,
pendingChangesAt: editorMessage.value?.pendingChangesAt
});
break;
case EditorEvents.title:
@@ -428,10 +431,11 @@ export const useEditorEvents = (
editor.saveContent({
type: editorMessage.type,
title: editorMessage.value?.title as string,
noteId: noteId,
noteId: saveNoteId,
sourceNoteId: editorMessage.noteId,
tabId: editorMessage.tabId,
ignoreEdit: false,
pendingChanges: editorMessage.value?.pendingChanges
pendingChanges: editorMessage.value?.pendingChanges,
pendingChangesAt: editorMessage.value?.pendingChangesAt
});
break;
case EditorEvents.logger:

View File

@@ -260,29 +260,65 @@ export const useEditor = (
id,
data,
type,
ignoreEdit,
sessionHistoryId: currentSessionHistoryId,
tabId,
pendingChanges
pendingChanges,
sourceNoteId,
pendingChangesAt
}: SavePayload) => {
if (currentNotes.current[id as string]?.readonly || readonly) return;
if (sourceNoteId && id && sourceNoteId !== id) {
DatabaseLogger.error(
new Error(
`Refused to save content of note ${sourceNoteId} into note ${id}`
)
);
return;
}
try {
if (id && !(await db.notes?.note(id))) {
await reset(tabId);
useTabStore.getState().updateTab(tabId, {
session: {
noteId: undefined,
noteLocked: undefined,
locked: undefined,
readonly: undefined,
scrollTop: undefined,
selection: undefined,
spellCheckDisabled: false
}
});
if (useTabStore.getState().getNoteIdForTab(tabId) === id) {
await reset(tabId);
useTabStore.getState().updateTab(tabId, {
session: {
noteId: undefined,
noteLocked: undefined,
locked: undefined,
readonly: undefined,
scrollTop: undefined,
selection: undefined,
spellCheckDisabled: false
}
});
}
return;
}
let note = id ? await db.notes?.note(id) : undefined;
// A restored pending change can be older than what is already in the
// db (it was saved on another device, or the save actually went
// through and only the acknowledgement was lost). Applying it would
// roll the note back, so verify it is still the newest edit. Content
// and title are compared separately so that a newer title doesn't
// discard pending content, and vice versa.
if (pendingChanges && pendingChangesAt && note) {
const dateEdited = data
? note.contentId
? (await db.content?.get(note.contentId))?.dateEdited
: undefined
: note.dateEdited;
if (dateEdited && dateEdited > pendingChangesAt) {
DatabaseLogger.log(
`Discarding stale pending ${
data ? "content" : "title"
} for note ${id}: edited at ${dateEdited}, change captured at ${pendingChangesAt}`
);
return id;
}
}
const locked = note && (await db.vaults.itemExists(note));
if (note?.conflicted) {
@@ -306,11 +342,6 @@ export const useEditor = (
noteData.title = title;
if (ignoreEdit) {
DatabaseLogger.log("Ignoring edits...");
noteData.dateEdited = note?.dateEdited;
}
if (data) {
noteData.content = {
data: data,
@@ -321,6 +352,9 @@ export const useEditor = (
let saved = false;
setTimeout(() => {
if (saved) return;
// Don't report progress on a tab that has moved on to another note.
if (id && useTabStore.getState().getNoteIdForTab(tabId) !== id)
return;
commands.setStatus(
getFormattedDate(note ? note.dateEdited : Date.now(), "date-time"),
strings.saving(),
@@ -436,14 +470,26 @@ export const useEditor = (
}
}
if (
id &&
id === useTabStore.getState().getCurrentNoteId() &&
pendingChanges
) {
postMessage(NativeEvents.title, title || note?.title, tabId);
postMessage(NativeEvents.html, data, tabId);
currentNotes.current[id] = note;
if (id && pendingChanges) {
if (data) {
currentContents.current[id] = {
data: data,
type: "tiptap",
noteId: id
};
}
lastContentChangeTime.current[id] = Date.now();
// Push the restored change into the editor only if the note is
// actually open in a tab, and only into that tab.
const noteTabId = useTabStore.getState().getTabForNote(id);
if (noteTabId !== undefined) {
postMessage(NativeEvents.title, title || note?.title, noteTabId);
if (data) {
postMessage(NativeEvents.html, { data: data }, noteTabId);
}
currentNotes.current[id] = note;
}
}
if (!saveCount.current[tabId]) {
@@ -932,24 +978,25 @@ export const useEditor = (
title,
content,
type,
ignoreEdit,
noteId,
tabId,
pendingChanges
pendingChanges,
sourceNoteId,
pendingChangesAt
}: {
noteId?: string;
title?: string;
content?: string;
type: string;
ignoreEdit: boolean;
tabId: string;
pendingChanges?: boolean;
sourceNoteId?: string;
pendingChangesAt?: number;
}) => {
DatabaseLogger.log(
`saveContent... title: ${!!title}, content: ${!!content}, noteId: ${noteId}`
);
if (
ignoreEdit ||
lock.current ||
(currentLoadingNoteId.current &&
currentLoadingNoteId.current === noteId)
@@ -958,7 +1005,6 @@ export const useEditor = (
lock.current: ${lock.current}
currentLoadingNoteId.current: ${currentLoadingNoteId.current}
ignoreEdit: ${ignoreEdit}
`);
if (lock.current) {
setTimeout(() => {
@@ -971,7 +1017,10 @@ export const useEditor = (
return;
}
if (noteId) {
// A restored pending change is not a live edit: it may still be
// discarded as stale by saveNote, so it must not claim to be the newest
// content until it is actually written.
if (noteId && !pendingChanges) {
lastContentChangeTime.current[noteId] = Date.now();
localTabState.current?.setEditTime(noteId, Date.now());
localTabState?.current?.set(tabId, {
@@ -979,7 +1028,7 @@ export const useEditor = (
});
}
if (type === EditorEvents.content && noteId) {
if (type === EditorEvents.content && noteId && !pendingChanges) {
currentContents.current[noteId as string] = {
data: content,
type: "tiptap",
@@ -992,15 +1041,17 @@ export const useEditor = (
data: content,
type: "tiptap",
id: noteId,
ignoreEdit,
sessionHistoryId: noteId ? editorSessionHistory.get(noteId) : undefined,
tabId: tabId,
pendingChanges
pendingChanges,
sourceNoteId,
pendingChangesAt
};
withTimer(
noteId || "newnote",
`${noteId || tabId}:${type}`,
() => {
if (!params.id) {
if (!params.id && !params.sourceNoteId) {
params.id = useTabStore.getState().getNoteIdForTab(tabId);
}
if (onChange && params.data) {
@@ -1018,7 +1069,7 @@ export const useEditor = (
saveNote(params);
}
},
ignoreEdit ? 0 : 150
150
);
},
[editorSessionHistory, withTimer, onChange, saveNote]

View File

@@ -30,6 +30,7 @@ import SettingsService from "../../services/settings";
import useNavigationStore from "../../stores/use-navigation-store";
import { useNotes } from "../../stores/use-notes-store";
import { openEditor } from "../notes/common";
import { db } from "../../common/database";
export const Home = ({ navigation, route }: NavigationProps<"Notes">) => {
const [notes, loading] = useNotes();
@@ -59,7 +60,8 @@ export const Home = ({ navigation, route }: NavigationProps<"Notes">) => {
placeholder: strings.searchInRoute(route.name),
type: "note",
title: route.name,
route: route.name
route: route.name,
items: db.notes.all
});
}}
id={route.name}

View File

@@ -35,7 +35,10 @@ import Paragraph from "../../components/ui/typography/paragraph";
import { useDBItem } from "../../hooks/use-db-item";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import Navigation, { NavigationProps } from "../../services/navigation";
import { createItemSelectionStore } from "../../stores/item-selection-store";
import {
createItemSelectionStore,
SelectionState
} from "../../stores/item-selection-store";
import { updateNotebook } from "../../utils/notebooks";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
@@ -47,14 +50,12 @@ export const MoveNotes = (props: NavigationProps<"MoveNotes">) => {
const currentNotebook = props.route.params.notebook;
const inputRef = useRef<TextInput>(null);
const [loading, setLoading] = useState(false);
const selectionCount = useItemSelectionStore(
(state) =>
Object.keys(state.selection).filter(
(k) =>
state.selection?.[k] === "selected" ||
state.selection[k] === "deselected"
)?.length > 0
);
const hasPendingChanges = useItemSelectionStore((state) => {
return Object.keys(state.selection).some((id) => {
return state.selection[id] !== state.initialState[id];
});
});
useNavigationFocus(props.navigation, { focusOnInit: true });
@@ -76,20 +77,6 @@ export const MoveNotes = (props: NavigationProps<"MoveNotes">) => {
.catch(() => {
setLoading(false);
});
db.relations
.from(currentNotebook, "note")
.get()
.then((existingNotes) => {
const selection: { [name: string]: any } = {};
existingNotes.forEach((rel) => {
selection[rel.toId] = "selected";
});
useItemSelectionStore.setState({
selection: selection,
initialState: selection
});
});
},
[currentNotebook]
);
@@ -97,6 +84,25 @@ export const MoveNotes = (props: NavigationProps<"MoveNotes">) => {
useEffect(() => {
loadNotes();
const loadSelection = async () => {
const existingNotes = await db.relations
.from(currentNotebook, "note")
.get();
const selection: Record<string, SelectionState> = {};
existingNotes.forEach((rel) => {
selection[rel.toId] = "selected";
});
useItemSelectionStore.setState({
selection,
initialState: selection
});
};
loadSelection();
return () => {
useItemSelectionStore.getState().reset();
};
@@ -159,6 +165,8 @@ export const MoveNotes = (props: NavigationProps<"MoveNotes">) => {
/>
<FlatList
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="handled"
ListEmptyComponent={
<View
style={{
@@ -176,17 +184,19 @@ export const MoveNotes = (props: NavigationProps<"MoveNotes">) => {
)}
</View>
}
style={{
flexGrow: 1
style={{ flex: 1 }}
contentContainerStyle={{
paddingBottom: hasPendingChanges ? 80 : 0
}}
data={loading ? [] : notes?.placeholders}
renderItem={renderItem}
/>
{selectionCount ? (
{hasPendingChanges ? (
<FloatingButton
icon="check"
alwaysVisible
hideOnKeyboard={false}
onPress={async () => {
await db.notes?.addToNotebook(
currentNotebook.id,

View File

@@ -28,6 +28,7 @@ import {
} from "../../components/dialogs/progress";
import Navigation from "../../services/navigation";
import BackupService from "../../services/backup";
import { useUserStore } from "../../stores/use-user-store";
export async function logoutUser() {
const hasUnsyncedChanges = await db.hasUnsyncedChanges();
@@ -47,6 +48,7 @@ export async function logoutUser() {
: undefined,
positivePress: async (_, takeBackup) => {
eSendEvent(eCloseSimpleDialog);
useUserStore.getState().setIsLoggingOut(true);
setTimeout(async () => {
try {
startProgress({
@@ -104,6 +106,7 @@ export async function logoutUser() {
DatabaseLogger.error(e);
ToastManager.error(e as Error, strings.logoutError());
endProgress();
useUserStore.getState().setIsLoggingOut(false);
}
}, 300);
}

View File

@@ -177,9 +177,11 @@ function resetRootState(
if (state.routes.length < 2) return;
const routes = state.routes.filter(
let routes = state.routes.filter(
(route) =>
(route.name !== "Auth" && route.name !== "Welcome") ||
(route.name !== "Auth" &&
route.name !== "Welcome" &&
route.name !== "AddReminder") ||
route.key === focusedRoute.key
);

View File

@@ -63,6 +63,11 @@ const run = async (
clearTimeout(syncTimer);
syncTimer = setTimeout(async () => {
if (useUserStore.getState().isLoggingOut) {
DatabaseLogger.info("Sync skipped — user is logging out");
return;
}
const userstore = useUserStore.getState();
userstore.setSyncing(true);
const user = await db.user.getUser();

View File

@@ -56,17 +56,14 @@ export function createItemSelectionStore(
canEnableMultiSelectMode: multiSelectMode,
initialState: {},
markAs: (item, state) => {
set({
selection: {
...get().selection,
[item.id]:
state === "deselected"
? get().initialState === undefined
? undefined
: "deselected"
: state
}
});
const selection = { ...get().selection };
const initial = get().initialState[item.id];
if (state === "deselected" && initial === undefined) {
delete selection[item.id];
} else {
selection[item.id] = state;
}
set({ selection });
},
multiSelect: false,
toggleMultiSelect: () => {

View File

@@ -74,13 +74,21 @@ export interface RouteParams extends ParamListBase {
Tags: GenericRouteParam;
Favorites: GenericRouteParam;
Trash: GenericRouteParam;
Search: {
placeholder: string;
type: ItemType;
title: string;
route: RouteName;
items?: FilteredSelector<Item>;
};
Search:
| {
placeholder: string;
type: "note";
title: string;
route: RouteName;
items: FilteredSelector<Note>;
}
| {
placeholder: string;
type: Exclude<ItemType, "note">;
title: string;
route: RouteName;
items?: FilteredSelector<Item>;
};
TaggedNotes: NotesScreenParams;
ColoredNotes: NotesScreenParams;
TopicNotes: NotesScreenParams;
@@ -88,7 +96,7 @@ export interface RouteParams extends ParamListBase {
Monographs: NotesScreenParams;
Reminders: GenericRouteParam;
SettingsGroup: GenericRouteParam;
FluidPanelsView: GenericRouteParam;
FluidPanelsView: { initialPage?: "editor" | "home" };
AppLock: GenericRouteParam;
Settings: GenericRouteParam;
Auth: AuthParams;

View File

@@ -27,6 +27,7 @@ import { ThemeDark, ThemeLight, ThemeDefinition } from "@notesnook/theme";
import { DayFormat, WeekFormat, Reminder } from "@notesnook/core";
import { db } from "../common/database";
import { EDITOR_LINE_HEIGHT } from "../utils/constants";
import { ShortcutItem } from "react-native-actions-shortcuts";
export const HostIds = [
"API_HOST",
"AUTH_HOST",
@@ -149,6 +150,7 @@ export interface SettingStore {
refresh: () => void;
inboxEnabled: boolean;
setInboxEnabled: (inboxEnabled: boolean) => void;
pendingShortcut: ShortcutItem | null;
}
const { width, height } = Dimensions.get("window");
@@ -269,5 +271,6 @@ export const useSettingStore = create<SettingStore>((set, get) => ({
});
},
inboxEnabled: false,
setInboxEnabled: (inboxEnabled) => set({ inboxEnabled })
setInboxEnabled: (inboxEnabled) => set({ inboxEnabled }),
pendingShortcut: null
}));

View File

@@ -48,6 +48,8 @@ export interface UserStore {
disableAppLockRequests: boolean;
setDisableAppLockRequests: (disableAppLockRequests: boolean) => void;
profile?: Partial<Profile>;
isLoggingOut: boolean;
setIsLoggingOut: (value: boolean) => void;
}
export const useUserStore = create<UserStore>((set) => ({
@@ -95,5 +97,7 @@ export const useUserStore = create<UserStore>((set) => ({
set({ disableAppLockRequests: false });
}, 1000);
},
profile: undefined
profile: undefined,
isLoggingOut: false,
setIsLoggingOut: (value) => set({ isLoggingOut: value })
}));

View File

@@ -5,6 +5,8 @@ require Pod::Executable.execute_command('node', ['-p',
{paths: [process.argv[1]]},
)', __dir__]).strip
require_relative 'scripts/patch_fmt_consteval'
platform :ios, min_ios_version_supported
prepare_react_native_project!
@@ -66,6 +68,10 @@ post_install do |installer|
:mac_catalyst_enabled => false,
# :ccache_enabled => true
)
# Keep fmt buildable on Xcode >= 26.2. See ios/scripts/patch_fmt_consteval.rb.
PatchFmtConsteval.apply!(installer.sandbox.root)
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['APPLICATION_EXTENSION_API_ONLY'] = 'NO'

View File

@@ -4092,6 +4092,6 @@ SPEC CHECKSUMS:
toolbar-android: c426ed5bd3dcccfed20fd79533efc0d1ae0ef018
Yoga: 689c8e04277f3ad631e60fe2a08e41d411daf8eb
PODFILE CHECKSUM: 3fe13efa8356dcc061862bfa9f453dcd12ede70a
PODFILE CHECKSUM: 30b2045c0f4fc91402a43a9e2a872af803f2d6c3
COCOAPODS: 1.16.2

View File

@@ -1,6 +1,6 @@
// Production iOS build identifiers
IOS_CURRENT_PROJECT_VERSION = 2191
IOS_MARKETING_VERSION = 3.4.7
IOS_CURRENT_PROJECT_VERSION = 2192
IOS_MARKETING_VERSION = 3.4.8
IOS_MAIN_BUNDLE_ID = org.streetwriters.notesnook
IOS_WIDGET_BUNDLE_ID = org.streetwriters.notesnook.notewidget
IOS_SHARE_BUNDLE_ID = org.streetwriters.notesnook.share

View File

@@ -1,6 +1,6 @@
// Production iOS build identifiers
IOS_CURRENT_PROJECT_VERSION = 2191
IOS_MARKETING_VERSION = 3.4.7
IOS_CURRENT_PROJECT_VERSION = 2192
IOS_MARKETING_VERSION = 3.4.8
IOS_MAIN_BUNDLE_ID = org.streetwriters.notesnook
IOS_WIDGET_BUNDLE_ID = org.streetwriters.notesnook.notewidget
IOS_SHARE_BUNDLE_ID = org.streetwriters.notesnook.share

View File

@@ -1,6 +1,6 @@
// Staging iOS build identifiers
IOS_CURRENT_PROJECT_VERSION = 2191
IOS_MARKETING_VERSION = 3.4.7
IOS_CURRENT_PROJECT_VERSION = 2192
IOS_MARKETING_VERSION = 3.4.8
IOS_MAIN_BUNDLE_ID = org.streetwriters.notesnook
IOS_WIDGET_BUNDLE_ID = org.streetwriters.notesnook.notewidget
IOS_SHARE_BUNDLE_ID = org.streetwriters.notesnook.share

View File

@@ -0,0 +1,77 @@
# Xcode >= 26.2 rejects fmt's compile-time format-string check with
#
# call to consteval function 'fmt::fstring<...>::fstring<char[N]>' is not a
# constant expression
#
# React Native 0.81/0.82 vendor fmt 11.0.2, which hits this. It is an upstream
# incompatibility (reproducible in a stock RN app), but left alone it makes those
# RN versions permanently un-buildable on a modern Xcode.
#
# fmt's own escape hatch is FMT_USE_CONSTEVAL: 0 downgrades the format-string
# check from compile-time to run-time. fmt 11.0.2 does not guard its detection
# block with #ifndef, so we cannot simply predefine the macro -- and doing it
# through the build settings is worse anyway:
#
# * a command-line GCC_PREPROCESSOR_DEFINITIONS outranks every per-target
# value, silently dropping COCOAPODS=1, RCT_METRO_PORT, ...
# * a second `post_install` block in the Podfile REPLACES React Native's own.
#
# So we patch the header itself, right after fmt has made up its mind and before
# the first use of the macro. Idempotent, and safe to run on every pod install.
module PatchFmtConsteval
MARKER = 'NOTESNOOK_FMT_CONSTEVAL_PATCH'.freeze
# The line that first consumes the macro; our override goes immediately above
# it, i.e. after the whole detection cascade.
ANCHOR = "#if FMT_USE_CONSTEVAL\n".freeze
OVERRIDE = <<~PATCH.freeze
// #{MARKER}: Xcode >= 26.2 rejects fmt's consteval format-string check
// (see ios/scripts/patch_fmt_consteval.rb). Applied automatically by
// `pod install`; downgrades the check to run-time.
#undef FMT_USE_CONSTEVAL
#define FMT_USE_CONSTEVAL 0
PATCH
# pods_root: the Pods directory (installer.sandbox.root).
def self.apply!(pods_root)
header = File.join(pods_root.to_s, 'fmt', 'include', 'fmt', 'base.h')
unless File.exist?(header)
Pod::UI.warn "fmt: #{header} not found, skipping consteval patch."
return
end
contents = File.read(header)
if contents.include?(MARKER)
Pod::UI.puts 'fmt: consteval patch already applied.'
return
end
index = contents.index(ANCHOR)
if index.nil?
Pod::UI.warn 'fmt: could not find `#if FMT_USE_CONSTEVAL` in base.h; ' \
'the consteval patch was NOT applied. If this fmt version ' \
'still uses a consteval format-string check, builds on ' \
'Xcode >= 26.2 will fail -- update ' \
'ios/scripts/patch_fmt_consteval.rb.'
return
end
contents.insert(index, OVERRIDE)
# CocoaPods checks pod sources out read-only (0444), so make the header
# writable for the write and restore the original mode afterwards.
mode = File.stat(header).mode & 0o7777
begin
File.chmod(mode | 0o200, header)
File.write(header, contents)
ensure
File.chmod(mode, header)
end
Pod::UI.puts 'fmt: patched base.h to set FMT_USE_CONSTEVAL=0 (Xcode >= 26.2 compatibility).'
end
end

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/mobile",
"version": "3.4.7",
"version": "3.4.8",
"private": true,
"license": "GPL-3.0-or-later",
"scripts": {

View File

@@ -128,7 +128,8 @@ const EXTRA_ICON_NAMES = [
"identifier",
"image-area",
"clock-outline",
"delete-sweep-outline"
"delete-sweep-outline",
"sync"
];
const __filename = fileURLToPath(import.meta.url);

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "@notesnook/web",
"description": "Your private note taking space",
"version": "3.4.4",
"version": "3.4.5",
"private": true,
"main": "./src/app.js",
"homepage": "https://notesnook.com/",

View File

@@ -40,7 +40,7 @@ import AuthContainer from "../components/auth-container";
import { useTimer } from "../hooks/use-timer";
import { ErrorText } from "../components/error-text";
import { AuthenticatorType, User } from "@notesnook/core";
import { showLogoutConfirmation } from "../dialogs/confirm";
import { ConfirmDialog, showLogoutConfirmation } from "../dialogs/confirm";
import { TaskManager } from "../common/task-manager";
import { strings } from "@notesnook/intl";
import { ScrollContainer } from "@notesnook/ui";
@@ -907,10 +907,19 @@ export function AuthForm<T extends AuthRoutes>(props: AuthFormProps<T>) {
sx={{
mt: 5,
color: "paragraph",
textDecoration: "none"
textDecoration: "none",
position: "absolute",
top: 0,
right: 5
}}
onClick={() => {
openURL("/notes/", { authenticated: false });
onClick={async () => {
const result = await ConfirmDialog.show({
title: strings.offlineMode(),
message: strings.offlineModeDesc(),
negativeButtonText: strings.cancel(),
positiveButtonText: strings.understand()
});
if (result) openURL("/notes/", { authenticated: false });
}}
>
{strings.skipAndGoToApp()}

View File

@@ -0,0 +1,6 @@
- Added sync status icon in sidebar
- Added new reminder shortcut in app icon context menu
- Improved editor saving reliability
- Minor bug fixes and improvements
Thank you for using Notesnook!

View File

@@ -1,33 +0,0 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: openpgp-mobile
xsBNBGoNaK8BCAD0B33KK4LRAvN1lZLpJhQMyk/+Srss56PjFphMH1MmqMgIRRBP
3RykX+7+ibha+5WIFYpgBEaPM9osZz22XUGhVrUzUxJMScjhLWR6xyuv0qs0Dctg
ePdcupPbJND3j9W4OnOBXwv+Ko/fX5K+enJfPp6fxyWbf3X/1BnAYlHyLngBLz8P
mt5qj0qax5V4ujUVU8ByNFvQkjcA+ip0vol2xQlmKa5UXJ1KYfM7LQWa4gQqdaY8
8FOl6CaaWsXO/vCnIF47JClWGJfJ0rAaREI/Kj+MV+i98BPAcZG8Kj523QmgIHo6
Puahi1q9wqX6Vwe6hjhiTUJSWvNjZXGi9A4PABEBAAHNDU5OIDxOTkBOTi5OTj7C
wLsEEwEIAG8FgmoNaK8CCwcJkC34qZJdxA5jNRQAAAAAABwAEHNhbHRAbm90YXRp
b25zLm9wZW5wZ3Bqcy5vcmfLP2eEUAk99foAqkNQSyW9AhUIAhYAAhkBApsDAh4B
FiEEjXaqZcVUExRCuEhoLfipkl3EDmMAAKW7CACxh26CVTjRLjeq/GNueceWRJoT
qF/OMQY/3mnuLMbaEMuYSc03ml6jiMdqZy6qeKgjH36qvpfu68HxPYEOn3WQ/k9V
2Iug4xDFlqFw0IN2Gqkgur+FYbFCjG1mkqsrlKsN2QHqG8sf+5EXegpvNibI+43Q
HZp+Q5B8HftnBANvZngFJz3t0hCCpUgVOTK4vwK8IKDK7zbMqfnT4k5vNbfpxd30
5xfZkQXrltJLaHb5pzgVMeoM00TaHd8WBDFYn8BF1OI/TbcChYpRPkW7WoKKuZ/1
tIBfv/O5h3ZxgSc4NdL1FuLZ0nTscPoN+uBE29XQat8IOiYiA4DvKLbIQe89zsBN
BGoNaK8BCACppIfZHotgC8zKvCkj1sWAny5Qq/AkYdIJh/b//7NhnWyUgiSDnoEV
1yik33CiQgoORR42YfVCZCMH2ZydzeKdaNKd/fFLeMsog0ddp4cW68drDDVvkGso
vSMwvpSS/J4JJ2KXqIbvscJXGzAaFZ61BoY3kvRKHymHGe2oLs2bPscNug7mm8pm
18+IhNeOtuS6MZzdXr9rmfuTtY9zUIbIaOgY3EiiaRkvcQLPcBTwsoM9b9zNMK+1
ivA65KrtgN+T7axcIRT+QFFoNOw7mjHNQybb6qRABWS8AWouot4B2g9tNHndlhNV
aimI+P1RHaP9VOxJFneWkmvviXqznv4TABEBAAHCwKwEGAEIAGAFgmoNaK8JkC34
qZJdxA5jNRQAAAAAABwAEHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5vcmeywJ1O
IyGqDaRT2EGH4ZZpApsMFiEEjXaqZcVUExRCuEhoLfipkl3EDmMAABm8B/9rKv4l
PNwYXLVQlhyGlF/MvdYvT4Fj2CxtO32Fo++dUlEYJqX++GihXr0HyjdE200Ttb1Q
IpZto9rx5X0QHKGEqVGM+kJ6STOWK5jRlADES6GKE3dZde4Z+QS+BBdEdveZtGwR
GsLArPKjhnbkiBXTrMUkoQa4eGanuXA452io5NsiIeNlMUsC6IAXxuZp8+iBtN9K
65iSrvmasdKgwttbqp0qiI5VudiEAQjrkHDMlGftMqSllZWkavlbpYPIN/Omzh8G
kVbBMAvQtDsxSFACnZCGQ1l3r9qDJWUQyn57qQgrQwYXx+sHIlOcdAMGiPbE49KP
kUYOKopIqix0i2/F
=rfLM
-----END PGP PUBLIC KEY BLOCK-----

View File

@@ -231,6 +231,80 @@ test("delete vault and delete all locked notes", () =>
expect(await db.vaults.default()).toBeUndefined();
}));
test("delete all vaults and their locked notes", () =>
noteTest().then(async ({ db, id: note1Id }) => {
/**
* simulating the case where multiple vaults have been created
* and each vault has a locked note
*/
const key = {
format: "base64",
alg: "aes-256-gcm",
cipher: "key",
iv: "iv",
salt: "salt",
length: 16
};
const vault1Id = await db.vaults.add({
title: "Vault 1",
key
});
const vault2Id = await db.vaults.add({
title: "Vault 2",
key
});
await db.relations.add(
{
id: vault1Id,
type: "vault"
},
{
id: note1Id,
type: "note"
}
);
const note2Id = await db.notes.add(TEST_NOTE);
await db.relations.add(
{
id: vault2Id,
type: "vault"
},
{
id: note2Id,
type: "note"
}
);
await db.vault.delete(true);
expect(
await db.relations
.from(
{
id: vault1Id,
type: "vault"
},
"note"
)
.has(note1Id)
).toBe(false);
expect(
await db.relations
.from(
{
id: vault2Id,
type: "vault"
},
"note"
)
.has(note2Id)
).toBe(false);
expect(await db.notes.exists(note1Id)).toBe(false);
expect(await db.notes.exists(note2Id)).toBe(false);
expect(await db.vaults.default()).toBeUndefined();
expect(await db.vaults.all.count()).toBe(0);
}));
test("vault password is cleared after specified time", () =>
databaseTest().then(async (db) => {
await expect(db.vault.create("password")).resolves.toBe(true);

View File

@@ -0,0 +1,50 @@
/*
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 { databaseTest } from "./utils/index.js";
import { test, expect } from "vitest";
test("remove all vaults", () =>
databaseTest().then(async (db) => {
const key = {
format: "base64" as const,
alg: "aes-256-gcm",
cipher: "key",
iv: "iv",
salt: "salt",
length: 16
};
await db.vaults.add({
title: "Vault 1",
key
});
await db.vaults.add({
title: "Vault 2",
key
});
expect(await db.vaults.all.count()).toBe(2);
expect(await db.vaults.default()).toBeDefined();
await db.vaults.removeAll();
expect(await db.vaults.all.count()).toBe(0);
expect(await db.vaults.default()).toBeUndefined();
}));

View File

@@ -114,7 +114,6 @@ export default class Lookup {
): Promise<VirtualizedGrouping<HighlightedResult>> {
const db = this.db.sql() as unknown as Kysely<RawDatabaseSchema>;
const excludedIds = this.db.trash.cache.notes;
const {
content,
title,

View File

@@ -387,6 +387,10 @@ export class Sync {
const itemsToDecrypt = itemsByKeyVersion.get(keyInfo.version);
if (!itemsToDecrypt || itemsToDecrypt.length === 0) continue;
this.logger.info("Decrypting using key", {
keyInfo: keyInfo.version,
items: itemsToDecrypt.length
});
decrypted.push(
...(await this.db.storage().decryptMulti(keyInfo.key, itemsToDecrypt))
);

View File

@@ -404,11 +404,13 @@ class UserManager {
{ version: KeyVersion; key: SerializedKey }[] | undefined
> {
const masterKey = await this.getMasterKey();
logger.info("master key exists: ", { masterKey: !!masterKey });
if (!masterKey) return;
const dataEncryptionKey = await this.keyManager.get("dataEncryptionKey", {
refetchUser: false
});
logger.info("DEK exists: ", { dataEncryptionKey: !!dataEncryptionKey });
if (!dataEncryptionKey)
return [
{
@@ -424,6 +426,9 @@ class UserManager {
refetchUser: false
}
);
logger.info("legacy DEK exists: ", {
legacyDataEncryptionKey: !!legacyDataEncryptionKey
});
if (legacyDataEncryptionKey)
keys.push({
key: await this.keyManager.unwrapKey(
@@ -436,6 +441,10 @@ class UserManager {
key: await this.keyManager.unwrapKey(dataEncryptionKey, masterKey),
version: KEY_VERSION.DEK
});
logger.info("Keys:", {
keys: keys.length,
keyVersions: keys.map((k) => k.version)
});
return keys;
}

View File

@@ -161,17 +161,30 @@ export default class Vault {
}
}
/**
*
* There's an unintentional and unrelated bug where multiple vaults
* can be created.
* So when user triggers delete, we should delete all vaults.
*/
async delete(deleteAllLockedNotes = false) {
const vault = await this.db.vaults.default();
if (!vault) return;
const vaults = await this.db.vaults.all.items();
if (!vaults.length) return;
if (deleteAllLockedNotes) {
const relations = await this.db.relations.from(vault, "note").get();
const lockedIds = relations.map((r) => r.toId);
await this.db.notes.remove(...lockedIds);
const lockedIds = new Set<string>();
for (const vault of vaults) {
const relations = await this.db.relations.from(vault, "note").get();
for (const { toId } of relations) {
lockedIds.add(toId);
}
}
if (lockedIds.size) {
await this.db.notes.remove(...lockedIds);
}
}
await this.db.vaults.remove(vault.id);
await this.db.vaults.removeAll();
this.password = undefined;
}

View File

@@ -87,4 +87,11 @@ export class Vaults implements ICollection {
async itemExists(reference: ItemReference) {
return (await this.db.relations.to(reference, "vault").count()) > 0;
}
async removeAll() {
const vaults = await this.all.items();
for (const vault of vaults) {
await this.remove(vault.id);
}
}
}

View File

@@ -153,18 +153,17 @@ export function useEditorController({
const titleChange = useCallback(async (title: string) => {
if (!isReactNative()) return;
const currentSessionId = globalThis.sessionId;
post(
EditorEvents.contentchange,
undefined,
tabRef.current.id,
tabRef.current.session?.noteId
);
const editedAt = Date.now();
const tabId = tabRef.current.id;
const noteId = tabRef.current.session?.noteId;
post(EditorEvents.contentchange, undefined, tabId, noteId);
const params = [
{
title
},
tabRef.current.id,
tabRef.current.session?.noteId,
tabId,
noteId,
currentSessionId,
1000
];
@@ -186,12 +185,12 @@ export function useEditorController({
`Saving title failed, setting pending request ${pendingTitleIds.length}`
);
if (params[2]) {
pendingSaveRequests.setTitle(params);
pendingSaveRequests.setTitle(params, editedAt);
}
const element = document.getElementById("editor-saving-failed-overlay");
if (element) {
element.style.display = "flex";
editors[tabRef.current.id]?.commands?.blur();
editors[tabId]?.commands?.blur();
element.focus();
}
});
@@ -216,27 +215,44 @@ export function useEditorController({
logger("info", "Edit skipped, tab is in loading state");
return;
}
if (ignoreEdit) {
logger("info", "Ignoring ignoreEdit update, a save is already pending");
return;
}
const currentSessionId = globalThis.sessionId;
post(
EditorEvents.contentchange,
undefined,
tabRef.current.id,
tabRef.current.session?.noteId
);
const tabId = tabRef.current.id;
const noteId = tabRef.current.session?.noteId;
post(EditorEvents.contentchange, undefined, tabId, noteId);
if (!editor) return;
if (typeof timers.current.change === "number") {
clearTimeout(timers.current?.change);
}
timers.current.change = setTimeout(async () => {
if (tabRef.current.session?.noteId !== noteId) {
logger(
"info",
`Edit discarded, tab ${tabId} moved from note ${noteId} to ${tabRef.current.session?.noteId}`
);
return;
}
if (editorControllers[tabId]?.loading) {
logger("info", "Edit discarded, tab is in loading state");
return;
}
const editedAt = Date.now();
htmlContentRef.current = editor.getHTML();
const params = [
{
html: htmlContentRef.current,
ignoreEdit: ignoreEdit
},
tabRef.current.id,
tabRef.current.session?.noteId,
tabId,
noteId,
currentSessionId,
5000
];
@@ -262,7 +278,7 @@ export function useEditorController({
}`
);
if (params[2]) {
pendingSaveRequests.setContent(params);
pendingSaveRequests.setContent(params, editedAt);
}
const element = document.getElementById(
@@ -275,7 +291,7 @@ export function useEditorController({
});
logger("info", "Editor saving content", params[1], params[2]);
}, 300);
}, 100);
countWords(5000);
},

View File

@@ -23,13 +23,14 @@ class PendingSaveRequests {
static TITLES = "pendingTitles";
static CONTENT = "pendingContents";
async setTitle(value: any) {
async setTitle(value: any, editedAt: number) {
const pendingTitles = JSON.parse(
this.get(PendingSaveRequests.TITLES) || "[]"
);
(pendingTitles as any[]).push({
id: randId("title-pending"),
editedAt,
params: value
});
return localStorage.setItem(
@@ -45,13 +46,14 @@ class PendingSaveRequests {
return pendingTitles;
}
async setContent(value: any) {
async setContent(value: any, editedAt: number) {
const pendingContents = JSON.parse(
this.get(PendingSaveRequests.CONTENT) || "[]"
);
(pendingContents as any[]).push({
id: randId("content-pending"),
editedAt,
params: value
});
return localStorage.setItem(
@@ -118,7 +120,10 @@ class PendingSaveRequests {
const pendingTitles = await this.getPendingTitles();
this.remove(PendingSaveRequests.TITLES);
for (const pending of pendingTitles) {
if (pending.params[0]) pending.params[0].pendingChanges = true;
if (pending.params[0]) {
pending.params[0].pendingChanges = true;
pending.params[0].pendingChangesAt = pending.editedAt;
}
await postAsyncWithTimeout(EditorEvents.title, ...pending.params);
}
};
@@ -127,7 +132,10 @@ class PendingSaveRequests {
const pendingContents = await this.getPendingContent();
this.remove(PendingSaveRequests.CONTENT);
for (const pending of pendingContents) {
if (pending.params[0]) pending.params[0].pendingChanges = true;
if (pending.params[0]) {
pending.params[0].pendingChanges = true;
pending.params[0].pendingChangesAt = pending.editedAt;
}
await postAsyncWithTimeout(EditorEvents.content, ...pending.params);
}
};

View File

@@ -53,7 +53,10 @@
"papaparse": "^5.5.3",
"prism-themes": "^1.9.0",
"prosemirror-codemark": "^0.4.2",
"prosemirror-view": "1.34.2",
"prosemirror-model": "1.25.11",
"prosemirror-state": "1.4.4",
"prosemirror-transform": "1.12.0",
"prosemirror-view": "1.42.2",
"re-resizable": "^6.9.18",
"react-colorful": "^5.6.1",
"redent": "^4.0.0",
@@ -4950,9 +4953,10 @@
}
},
"node_modules/prosemirror-model": {
"version": "1.22.3",
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.22.3.tgz",
"integrity": "sha512-V4XCysitErI+i0rKFILGt/xClnFJaohe/wrrlT2NSZ+zk8ggQfDH4x2wNK7Gm0Hp4CIoWizvXFP7L9KMaCuI0Q==",
"version": "1.25.11",
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.11.tgz",
"integrity": "sha512-QWg9RhnpLlogAmp3p96uEFrE5txQpFynd4vhBAELkwgOCWQs/X0yCzB3/hrHqiPwf91RG5KyWq6553zs9JqIOQ==",
"license": "MIT",
"dependencies": {
"orderedmap": "^2.0.0"
}
@@ -4976,9 +4980,10 @@
}
},
"node_modules/prosemirror-state": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.3.tgz",
"integrity": "sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==",
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-transform": "^1.0.0",
@@ -5023,19 +5028,21 @@
}
},
"node_modules/prosemirror-transform": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.0.tgz",
"integrity": "sha512-9UOgFSgN6Gj2ekQH5CTDJ8Rp/fnKR2IkYfGdzzp5zQMFsS4zDllLVx/+jGcX86YlACpG7UR5fwAXiWzxqWtBTg==",
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz",
"integrity": "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.21.0"
}
},
"node_modules/prosemirror-view": {
"version": "1.34.2",
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.34.2.tgz",
"integrity": "sha512-tPX/V2Xd70vrAGQ/V9CppJtPKnQyQMypJGlLylvdI94k6JaG+4P6fVmXPR1zc1eVTW0gq3c6zsfqwJKCRLaG9Q==",
"version": "1.42.2",
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.42.2.tgz",
"integrity": "sha512-Pdg0l5kXm8aLDquFAnQFTCITg0q44sLqBlHlpsVLD9segdOao8TOfQdAhCrCXyVgPSRr6UDDROOIWA3bIrN9YQ==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.20.0",
"prosemirror-model": "^1.25.8",
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.1.0"
}

View File

@@ -73,7 +73,10 @@
"papaparse": "^5.5.3",
"prism-themes": "^1.9.0",
"prosemirror-codemark": "^0.4.2",
"prosemirror-view": "1.34.2",
"prosemirror-model": "1.25.11",
"prosemirror-state": "1.4.4",
"prosemirror-transform": "1.12.0",
"prosemirror-view": "1.42.2",
"re-resizable": "^6.9.18",
"react-colorful": "^5.6.1",
"redent": "^4.0.0",

View File

@@ -1,8 +1,8 @@
diff --git a/node_modules/prosemirror-model/dist/index.cjs b/node_modules/prosemirror-model/dist/index.cjs
index aa31355..1409b46 100644
index 96ef508..8f56356 100644
--- a/node_modules/prosemirror-model/dist/index.cjs
+++ b/node_modules/prosemirror-model/dist/index.cjs
@@ -95,6 +95,7 @@ var Fragment = function () {
@@ -112,6 +112,7 @@ var Fragment = function () {
var nodeStart = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0;
var parent = arguments.length > 4 ? arguments[4] : undefined;
for (var i = 0, pos = 0; pos < to; i++) {
@@ -10,7 +10,7 @@ index aa31355..1409b46 100644
var child = this.content[i],
end = pos + child.nodeSize;
if (end > from && f(child, nodeStart + pos, parent || null, i) !== false && child.content.size) {
@@ -2817,7 +2818,7 @@ function _renderSpec(doc, structure, xmlNS, blockArraysIn) {
@@ -2860,7 +2861,7 @@ function _renderSpec(doc, structure, xmlNS, blockArraysIn) {
var tagName = structure[0],
suspicious;
if (typeof tagName != "string") throw new RangeError("Invalid array passed to renderSpec");
@@ -20,10 +20,10 @@ index aa31355..1409b46 100644
if (space > 0) {
xmlNS = tagName.slice(0, space);
diff --git a/node_modules/prosemirror-model/dist/index.js b/node_modules/prosemirror-model/dist/index.js
index 0097d9f..19d7031 100644
index 6b00598..c7c4a38 100644
--- a/node_modules/prosemirror-model/dist/index.js
+++ b/node_modules/prosemirror-model/dist/index.js
@@ -84,6 +84,7 @@ class Fragment {
@@ -94,6 +94,7 @@ class Fragment {
*/
nodesBetween(from, to, f, nodeStart = 0, parent) {
for (let i = 0, pos = 0; pos < to; i++) {
@@ -31,7 +31,7 @@ index 0097d9f..19d7031 100644
let child = this.content[i], end = pos + child.nodeSize;
if (end > from && f(child, nodeStart + pos, parent || null, i) !== false && child.content.size) {
let start = pos + 1;
@@ -3397,9 +3398,9 @@ function renderSpec(doc, structure, xmlNS, blockArraysIn) {
@@ -3420,9 +3421,9 @@ function renderSpec(doc, structure, xmlNS, blockArraysIn) {
let tagName = structure[0], suspicious;
if (typeof tagName != "string")
throw new RangeError("Invalid array passed to renderSpec");

View File

@@ -1,46 +1,46 @@
diff --git a/node_modules/prosemirror-view/dist/index.cjs b/node_modules/prosemirror-view/dist/index.cjs
index 8ea57c7..c289489 100644
index a615cb7..c1a6cbb 100644
--- a/node_modules/prosemirror-view/dist/index.cjs
+++ b/node_modules/prosemirror-view/dist/index.cjs
@@ -980,8 +980,8 @@ var ViewDesc = function () {
if (!(force || brKludge && safari) && isEquivalentPosition(anchorDOM.node, anchorDOM.offset, domSel.anchorNode, domSel.anchorOffset) && isEquivalentPosition(headDOM.node, headDOM.offset, domSel.focusNode, domSel.focusOffset)) return;
@@ -1005,8 +1005,8 @@ var ViewDesc = function () {
if (!(force || brKludge && safari) && isEquivalentPosition(anchorDOM.node, anchorDOM.offset, selRange.anchorNode, selRange.anchorOffset) && isEquivalentPosition(headDOM.node, headDOM.offset, selRange.focusNode, selRange.focusOffset)) return;
var domSelExtended = false;
if ((domSel.extend || anchor == head) && !brKludge) {
if ((domSel.extend || anchor == head) && !(brKludge && gecko)) {
- domSel.collapse(anchorDOM.node, anchorDOM.offset);
try {
+ domSel.collapse(anchorDOM.node, anchorDOM.offset);
if (anchor != head) domSel.extend(headDOM.node, headDOM.offset);
domSelExtended = true;
} catch (_) {}
@@ -3456,7 +3456,7 @@ editHandlers.drop = function (view, _event) {
@@ -3647,7 +3647,7 @@ function handleDrop(view, event, dragging) {
});
tr.setSelection(selectionBetween(view, $pos, tr.doc.resolve(end)));
}
- view.focus();
+ if (!dragging || !dragging.nodeView) view.focus();
view.dispatch(tr.setMeta("uiEvent", "drop"));
};
}
handlers.focus = function (view) {
diff --git a/node_modules/prosemirror-view/dist/index.js b/node_modules/prosemirror-view/dist/index.js
index 9583dc3..6899e62 100644
index 61118ee..ec4c853 100644
--- a/node_modules/prosemirror-view/dist/index.js
+++ b/node_modules/prosemirror-view/dist/index.js
@@ -1052,8 +1052,8 @@ class ViewDesc {
@@ -1079,8 +1079,8 @@ class ViewDesc {
// browsers support it yet.
let domSelExtended = false;
if ((domSel.extend || anchor == head) && !brKludge) {
if ((domSel.extend || anchor == head) && !(brKludge && gecko)) {
- domSel.collapse(anchorDOM.node, anchorDOM.offset);
try {
+ domSel.collapse(anchorDOM.node, anchorDOM.offset);
if (anchor != head)
domSel.extend(headDOM.node, headDOM.offset);
domSelExtended = true;
@@ -3731,7 +3731,7 @@ editHandlers.drop = (view, _event) => {
@@ -3885,7 +3885,7 @@ function handleDrop(view, event, dragging) {
tr.mapping.maps[tr.mapping.maps.length - 1].forEach((_from, _to, _newFrom, newTo) => end = newTo);
tr.setSelection(selectionBetween(view, $pos, tr.doc.resolve(end)));
}
- view.focus();
+ if (!dragging || !dragging.nodeView) view.focus();
view.dispatch(tr.setMeta("uiEvent", "drop"));
};
}
handlers.focus = view => {

View File

@@ -236,8 +236,27 @@ type LanguageSelectorProps = {
onClose: () => void;
};
function LanguageSelector(props: LanguageSelectorProps) {
const { onLanguageSelected, selectedLanguage, onClose } = props;
const [languages, setLanguages] = useState(Languages);
const { selectedLanguage, onClose } = props;
const recentlyUsed =
config.get<Record<string, number>>("recentlyUsedCodeBlockLanguages", {}) ??
{};
const [languages, setLanguages] = useState(() =>
[...Languages].sort((a, b) => {
const aCount = recentlyUsed[a.filename] || 0;
const bCount = recentlyUsed[b.filename] || 0;
if (aCount !== bCount) return bCount - aCount;
return 0;
})
);
const onLanguageSelected = (language: string) => {
props.onLanguageSelected(language);
config.set("recentlyUsedCodeBlockLanguages", {
...recentlyUsed,
[language]: Date.now()
});
};
return (
<Popup onClose={onClose}>

View File

@@ -18,8 +18,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { FunctionComponent, SyntheticEvent } from "react";
import { NodeView, Decoration, DecorationSource } from "prosemirror-view";
import { Node as PMNode, Slice } from "prosemirror-model";
import {
NodeView,
Decoration,
DecorationSource,
ViewMutationRecord
} from "prosemirror-view";
import { Node as PMNode } from "prosemirror-model";
import { NodeSelection } from "prosemirror-state";
import { PortalProviderAPI } from "./react-portal-provider.js";
import {
@@ -29,19 +34,18 @@ import {
ContentDOM
} from "./types.js";
import { Editor, NodeViewRendererProps } from "@tiptap/core";
import { __serializeForClipboard, EditorView } from "prosemirror-view";
import { EmotionThemeProvider } from "@notesnook/theme";
import { isAndroid, isiOS } from "../../utils/platform.js";
import { useToolbarStore } from "../../toolbar/stores/toolbar-store.js";
// This is hacky workaround to manually handle serialization when
// drag/dropping on mobile devices.
declare module "prosemirror-view" {
export function __serializeForClipboard(
view: EditorView,
slice: Slice
): { dom: HTMLElement; text: string };
}
// declare module "prosemirror-view" {
// export function __serializeForClipboard(
// view: EditorView,
// slice: Slice
// ): { dom: HTMLElement; text: string };
// }
const portalProviderAPI = new PortalProviderAPI();
export class ReactNodeView<P extends ReactNodeViewProps> implements NodeView {
private domRef!: HTMLElement;
@@ -401,7 +405,7 @@ export class ReactNodeView<P extends ReactNodeViewProps> implements NodeView {
}
ignoreMutation(
mutation: MutationRecord | { type: "selection"; target: Element }
mutation: ViewMutationRecord // MutationRecord | { type: "selection"; target: Element }
) {
if (!this.dom || !this.contentDOM) {
return true;
@@ -501,7 +505,7 @@ function forceHandleDrag(event: DragEvent, editor: Editor) {
if (!event.dataTransfer) return;
const { view } = editor;
const slice = view.state.selection.content();
const { dom, text } = __serializeForClipboard(view, slice);
const { dom, text } = view.serializeForClipboard(slice);
event.dataTransfer.clearData();
event.dataTransfer.setData("Text", text);

View File

@@ -1659,7 +1659,7 @@ msgstr "Clear data & reset account"
msgid "Clear default notebook"
msgstr "Clear default notebook"
#: src/strings.ts:2800
#: src/strings.ts:2803
msgid "Clear history"
msgstr "Clear history"
@@ -2311,7 +2311,7 @@ msgstr "Delete account"
msgid "Delete all"
msgstr "Delete all"
#: src/strings.ts:2802
#: src/strings.ts:2805
msgid "Delete all version history for this note?"
msgstr "Delete all version history for this note?"
@@ -4733,6 +4733,10 @@ msgstr "Off"
msgid "Offline"
msgstr "Offline"
#: src/strings.ts:2810
msgid "Offline mode"
msgstr "Offline mode"
#: src/strings.ts:2688
msgid "OK"
msgstr "OK"
@@ -6156,8 +6160,8 @@ msgid "Select folder with backup files"
msgstr "Select folder with backup files"
#: src/strings.ts:112
msgid "Select how you would like to recieve the code"
msgstr "Select how you would like to recieve the code"
msgid "Select how you would like to receive the code"
msgstr "Select how you would like to receive the code"
#: src/strings.ts:2366
msgid "Select language"
@@ -6477,10 +6481,6 @@ msgstr "Size"
msgid "Skip"
msgstr "Skip"
#: src/strings.ts:1831
msgid "Skip & go directly to the app"
msgstr "Skip & go directly to the app"
#: src/strings.ts:673
msgid "Skip introduction"
msgstr "Skip introduction"
@@ -7435,6 +7435,10 @@ msgstr "Use native OS titlebar instead of replacing it with a custom one. Requir
msgid "Use native titlebar"
msgstr "Use native titlebar"
#: src/strings.ts:1831
msgid "Use offline"
msgstr "Use offline"
#: src/strings.ts:1801
msgid "Use recovery key"
msgstr "Use recovery key"
@@ -7485,6 +7489,10 @@ msgstr "User verification failed"
msgid "Using {instance} (v{version})"
msgstr "Using {instance} (v{version})"
#: src/strings.ts:2812
msgid "Using Notesnook without an account will **NOT** sync your notes across devices and could result in data loss if you lose access to your device or uninstall the app. Make sure to backup your notes regularly."
msgstr "Using Notesnook without an account will **NOT** sync your notes across devices and could result in data loss if you lose access to your device or uninstall the app. Make sure to backup your notes regularly."
#: src/strings.ts:2265
msgid "Using official Notesnook instance"
msgstr "Using official Notesnook instance"
@@ -7553,7 +7561,7 @@ msgstr "Verifying your email"
msgid "Version"
msgstr "Version"
#: src/strings.ts:2801
#: src/strings.ts:2804
msgid "Version history cleared"
msgstr "Version history cleared"
@@ -7768,8 +7776,8 @@ msgid "Yes, you can cancel your trial anytime. No questions asked."
msgstr "Yes, you can cancel your trial anytime. No questions asked."
#: src/strings.ts:106
msgid "You also agree to recieve marketing emails from us which you can opt-out of from app settings."
msgstr "You also agree to recieve marketing emails from us which you can opt-out of from app settings."
msgid "You also agree to receive marketing emails from us which you can opt-out of from app settings."
msgstr "You also agree to receive marketing emails from us which you can opt-out of from app settings."
#: src/strings.ts:2604
msgid "You are already subscribed to this plan."

View File

@@ -1659,7 +1659,7 @@ msgstr ""
msgid "Clear default notebook"
msgstr ""
#: src/strings.ts:2800
#: src/strings.ts:2803
msgid "Clear history"
msgstr ""
@@ -2300,7 +2300,7 @@ msgstr ""
msgid "Delete all"
msgstr ""
#: src/strings.ts:2802
#: src/strings.ts:2805
msgid "Delete all version history for this note?"
msgstr ""
@@ -4707,6 +4707,10 @@ msgstr ""
msgid "Offline"
msgstr ""
#: src/strings.ts:2810
msgid "Offline mode"
msgstr ""
#: src/strings.ts:2688
msgid "OK"
msgstr ""
@@ -6130,7 +6134,7 @@ msgid "Select folder with backup files"
msgstr ""
#: src/strings.ts:112
msgid "Select how you would like to recieve the code"
msgid "Select how you would like to receive the code"
msgstr ""
#: src/strings.ts:2366
@@ -6443,10 +6447,6 @@ msgstr ""
msgid "Skip"
msgstr ""
#: src/strings.ts:1831
msgid "Skip & go directly to the app"
msgstr ""
#: src/strings.ts:673
msgid "Skip introduction"
msgstr ""
@@ -7394,6 +7394,10 @@ msgstr ""
msgid "Use native titlebar"
msgstr ""
#: src/strings.ts:1831
msgid "Use offline"
msgstr ""
#: src/strings.ts:1801
msgid "Use recovery key"
msgstr ""
@@ -7435,6 +7439,10 @@ msgstr ""
msgid "Using {instance} (v{version})"
msgstr ""
#: src/strings.ts:2812
msgid "Using Notesnook without an account will **NOT** sync your notes across devices and could result in data loss if you lose access to your device or uninstall the app. Make sure to backup your notes regularly."
msgstr ""
#: src/strings.ts:2265
msgid "Using official Notesnook instance"
msgstr ""
@@ -7503,7 +7511,7 @@ msgstr ""
msgid "Version"
msgstr ""
#: src/strings.ts:2801
#: src/strings.ts:2804
msgid "Version history cleared"
msgstr ""
@@ -7718,7 +7726,7 @@ msgid "Yes, you can cancel your trial anytime. No questions asked."
msgstr ""
#: src/strings.ts:106
msgid "You also agree to recieve marketing emails from us which you can opt-out of from app settings."
msgid "You also agree to receive marketing emails from us which you can opt-out of from app settings."
msgstr ""
#: src/strings.ts:2604

View File

@@ -103,13 +103,13 @@ export const strings = {
2: () => t`and `,
3: () => t`Privacy Policy. `,
4: () =>
t`You also agree to recieve marketing emails from us which you can opt-out of from app settings.`
t`You also agree to receive marketing emails from us which you can opt-out of from app settings.`
},
alreadyHaveAccount: () => t`Already have an account?`,
login: () => t`Login`,
"2fa": () => t`Two factor authentication`,
select2faMethod: () => t`Select method for two-factor authentication`,
select2faCodeHelpText: () => t`Select how you would like to recieve the code`,
select2faCodeHelpText: () => t`Select how you would like to receive the code`,
"2faCodeHelpText": {
email: () =>
t`Enter the 6 digit code sent to your email to continue logging in`,
@@ -1828,7 +1828,7 @@ For example:
cancelSub: () => t`Cancel subscription`,
unlockWithSecurityKey: () => t`Unlock with security key`,
reloginToYourAccount: () => t`Relogin to your account`,
skipAndGoToApp: () => t`Skip & go directly to the app`,
skipAndGoToApp: () => t`Use offline`,
startAccountRecovery: () => t`Start account recovery`,
dontHaveRecoveryKey: () => t`Don't have your account recovery key?`,
dontHaveBackupFile: () => t`Don't have backup file?`,
@@ -2806,5 +2806,8 @@ Continue without attachments?`,
deleteVersion: () => doActions.delete.version(1),
deleteVersionConfirmation: () =>
actionConfirmations.permanentlyDelete.version(1),
versionDeleted: () => actions.deleted.version(1)
versionDeleted: () => actions.deleted.version(1),
offlineMode: () => t`Offline mode`,
offlineModeDesc: () =>
t`Using Notesnook without an account will **NOT** sync your notes across devices and could result in data loss if you lose access to your device or uninstall the app. Make sure to backup your notes regularly.`
};