Compare commits

..

4 Commits

Author SHA1 Message Date
Ammar Ahmed
dda177a944 mobile: fix encryption on ios 2024-12-21 14:15:29 +05:00
Ammar Ahmed
cc75f6588a core: fix fallback key encryption 2024-12-21 14:14:24 +05:00
Abdullah Atta
ee8701eff8 core: add support for fallback hashing
on ios some users using passwords with multi byte characters
generates the wrong hash. This commit adds support for fallback
to allow those users to continue to login once the fix has been applied.
2024-12-21 14:14:24 +05:00
Ammar Ahmed
fe3c5484c3 mobile: fix encryption on ios 2024-12-21 14:14:24 +05:00
76 changed files with 3736 additions and 4420 deletions

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/desktop",
"version": "3.0.23",
"version": "3.0.22",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/desktop",
"version": "3.0.23",
"version": "3.0.22",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {

View File

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

View File

@@ -339,7 +339,7 @@ export async function hash(
DatabaseLogger.log(`Hashing password: fallback: ${options?.usesFallback}`);
if (options?.usesFallback && Platform.OS !== "ios") {
return "";
return null;
}
return (
@@ -350,17 +350,7 @@ export async function hash(
}
export async function generateCryptoKey(password: string, salt?: string) {
return Sodium.deriveKey(password, salt) as Promise<SerializedKey>;
}
export async function generateCryptoKeyFallback(
password: string,
salt?: string
): Promise<SerializedKey> {
return Sodium.deriveKeyFallback?.(
password,
salt as string
) as Promise<SerializedKey>;
return (await Sodium.deriveKey(password, salt)) as Promise<SerializedKey>;
}
export function getAlgorithm(base64Variant: number) {

View File

@@ -27,8 +27,7 @@ import {
encryptMulti,
generateCryptoKey,
getCryptoKey,
hash,
generateCryptoKeyFallback
hash
} from "./encryption";
import { MMKV } from "./mmkv";
@@ -147,6 +146,5 @@ export const Storage: IStorage = {
decryptMulti,
deriveCryptoKey,
generateCryptoKey,
generateCryptoKeyFallback,
deriveCryptoKeyFallback
};

View File

@@ -24,12 +24,12 @@ import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import * as ScopedStorage from "react-native-scoped-storage";
import { subscribe, zip } from "react-native-zip-archive";
import filesystem from ".";
import { ShareComponent } from "../../components/sheets/export-notes/share";
import { presentSheet, ToastManager } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { IOS_APPGROUPID } from "../../utils/constants";
import { DatabaseLogger, db } from "../database";
import filesystem from "../filesystem";
import { createCacheDir, exists } from "./io";
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";

View File

@@ -77,6 +77,7 @@ export const ChangePassword = () => {
throw new Error(strings.backupFailed() + `: ${result.error}`);
}
await db.user.clearSessions();
await db.user.changePassword(oldPassword.current, password.current);
ToastManager.show({
heading: strings.passwordChangedSuccessfully(),
@@ -88,7 +89,6 @@ export const ChangePassword = () => {
await sleep(300);
eSendEvent(eOpenRecoveryKeyDialog);
} catch (e) {
console.log(e.stack);
setLoading(false);
ToastManager.show({
heading: strings.passwordChangeFailed(),

View File

@@ -128,6 +128,7 @@ const Intro = ({ navigation }) => {
style={{
width: deviceMode !== "mobile" ? width / 2 : "100%",
backgroundColor: colors.secondary.background,
marginBottom: 20,
borderBottomWidth: 1,
borderBottomColor: colors.primary.border,
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
@@ -137,7 +138,7 @@ const Intro = ({ navigation }) => {
marginTop: deviceMode !== "mobile" ? 50 : null,
paddingTop: insets.top + 10,
paddingBottom: insets.top + 10,
minHeight: height * 0.7 - (insets.top + insets.bottom)
minHeight: height * 0.7
}}
>
<SwiperFlatList

View File

@@ -134,10 +134,8 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
) : (
<>
<Input
key="code-input"
fwdRef={codeInputRef}
placeholder={strings.code()}
defaultValue=""
placeholder={strings.verifyNewEmail()}
onChangeText={(code) => {
emailChangeData.current.code = code;
}}

View File

@@ -17,15 +17,9 @@ 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 {
EV,
EVENTS,
EventManagerSubscription,
SYNC_CHECK_IDS,
SyncStatusEvent,
User
} from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { User } from "@notesnook/core";
import { EV, EVENTS, SYNC_CHECK_IDS, SyncStatusEvent } from "@notesnook/core";
import { EventManagerSubscription } from "@notesnook/core";
import notifee from "@notifee/react-native";
import NetInfo, { NetInfoSubscription } from "@react-native-community/netinfo";
import React, { useCallback, useEffect, useRef } from "react";
@@ -47,7 +41,6 @@ import * as RNIap from "react-native-iap";
import { DatabaseLogger, db, setupDatabase } from "../common/database";
import { initializeLogger } from "../common/database/logger";
import { MMKV } from "../common/database/mmkv";
import { endProgress, startProgress } from "../components/dialogs/progress";
import Migrate from "../components/sheets/migrate";
import NewFeature from "../components/sheets/new-feature";
import { Walkthrough } from "../components/walkthroughs";
@@ -59,7 +52,6 @@ import {
} from "../screens/editor/tiptap/utils";
import { useDragState } from "../screens/settings/editor/state";
import BackupService from "../services/backup";
import BiometricService from "../services/biometrics";
import {
ToastManager,
eSendEvent,
@@ -74,17 +66,14 @@ import {
setRecoveryKeyMessage,
setUpdateAvailableMessage
} from "../services/message";
import Navigation from "../services/navigation";
import Notifications from "../services/notifications";
import PremiumService from "../services/premium";
import SettingsService from "../services/settings";
import Sync from "../services/sync";
import { clearAllStores, initAfterSync } from "../stores";
import { refreshAllStores } from "../stores/create-db-collection-store";
import { initAfterSync } from "../stores";
import { useAttachmentStore } from "../stores/use-attachment-store";
import { useMessageStore } from "../stores/use-message-store";
import { useSettingStore } from "../stores/use-setting-store";
import { changeSystemBarColors } from "../stores/use-theme-store";
import { SyncStatus, useUserStore } from "../stores/use-user-store";
import { updateStatusBarColor } from "../utils/colors";
import { BETA } from "../utils/constants";
@@ -100,8 +89,11 @@ import {
} from "../utils/events";
import { getGithubVersion } from "../utils/github-version";
import { tabBarRef } from "../utils/global-refs";
import { NotesnookModule } from "../utils/notesnook-module";
import { sleep } from "../utils/time";
import { NotesnookModule } from "../utils/notesnook-module";
import { changeSystemBarColors } from "../stores/use-theme-store";
import { strings } from "@notesnook/intl";
import { endProgress, startProgress } from "../components/dialogs/progress";
const onCheckSyncStatus = async (type: SyncStatusEvent) => {
const { disableSync, disableAutoSync } = SettingsService.get();
@@ -148,7 +140,6 @@ const onUploadedAttachmentProgress = (data: any) => {
};
const onUserSessionExpired = async () => {
console.log("LOGGED OUT USER....");
SettingsService.set({
sessionExpired: true
});
@@ -217,19 +208,11 @@ const onRequestPartialSync = async (
};
const onLogout = async (reason: string) => {
DatabaseLogger.log("User Logged Out " + reason);
setLoginMessage();
await PremiumService.setPremiumStatus();
await BiometricService.resetCredentials();
MMKV.clearStore();
clearAllStores();
setImmediate(() => {
refreshAllStores();
DatabaseLogger.log("User Logged Out" + reason);
Notifications.setupReminders(true);
SettingsService.set({
introCompleted: true
});
Navigation.queueRoutesForUpdate();
SettingsService.resetSettings();
useUserStore.getState().setUser(null);
useUserStore.getState().setSyncing(false);
};
async function checkForShareExtensionLaunchedInBackground() {
@@ -598,8 +581,7 @@ export const useAppEvents = () => {
EV.subscribe(EVENTS.migrationStarted, (name) => {
if (
name !== "notesnook" ||
!SettingsService.getProperty("introCompleted") ||
Config.isTesting === "true"
!SettingsService.getProperty("introCompleted")
)
return;
startProgress({
@@ -612,8 +594,7 @@ export const useAppEvents = () => {
EV.subscribe(EVENTS.migrationFinished, (name) => {
if (
name !== "notesnook" ||
!SettingsService.getProperty("introCompleted") ||
Config.isTesting === "true"
!SettingsService.getProperty("introCompleted")
)
return;
endProgress();

View File

@@ -97,7 +97,7 @@ const _TabsHolder = () => {
tabBarRef.current?.goToPage(1, false);
return;
}
if (item && item.type === "notesnook.action.newnote") {
if (item.type === "notesnook.action.newnote") {
clearAppState();
if (!tabBarRef.current) {
await sleep(3000);

View File

@@ -91,6 +91,7 @@ export const Reminders = ({
/>
<FloatingButton
title={strings.setReminder()}
onPress={() => {
ReminderSheet.present();
}}

View File

@@ -17,9 +17,6 @@ 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 { sanitizeFilename } from "@notesnook/common";
import { strings } from "@notesnook/intl";
import { useThemeColors, VariantsWithStaticColors } from "@notesnook/theme";
import Clipboard from "@react-native-clipboard/clipboard";
import React, {
Dispatch,
@@ -29,11 +26,11 @@ import React, {
useState
} from "react";
import { ActivityIndicator, Linking, Platform, View } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import { FlatList } from "react-native-gesture-handler";
import * as ScopedStorage from "react-native-scoped-storage";
import RNFetchBlob from "react-native-blob-util";
import { db } from "../../common/database";
import filesystem from "../../common/filesystem";
import Storage from "../../common/database/storage";
import DialogHeader from "../../components/dialog/dialog-header";
import { Button } from "../../components/ui/button";
import { IconButton } from "../../components/ui/icon-button";
@@ -49,10 +46,13 @@ import {
presentSheet,
ToastManager
} from "../../services/event-manager";
import { useThemeColors, VariantsWithStaticColors } from "@notesnook/theme";
import { useUserStore } from "../../stores/use-user-store";
import { eCloseSheet } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { sanitizeFilename } from "@notesnook/common";
import { strings } from "@notesnook/intl";
const mfaMethods: MFAMethod[] = [
{
id: "app",
@@ -525,7 +525,7 @@ export const MFARecoveryCodes = ({
if (!file) return;
path = file.uri;
} else {
path = await filesystem.checkAndCreateDir("/");
path = await Storage.checkAndCreateDir("/");
await RNFetchBlob.fs.writeFile(
path + fileName,
codeString,

View File

@@ -45,7 +45,7 @@ const Home = ({
});
const renderItem = ({ item }: { item: SettingSection; index: number }) =>
item.id === "account" ? (
item.name === "account" ? (
<SettingsUserSection item={item} />
) : (
<SectionGroup item={item} />

View File

@@ -18,10 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { formatBytes } from "@notesnook/common";
import { User } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import notifee from "@notifee/react-native";
import Clipboard from "@react-native-clipboard/clipboard";
import dayjs from "dayjs";
import React from "react";
import { Appearance, Linking, Platform } from "react-native";
@@ -30,6 +28,7 @@ import * as RNIap from "react-native-iap";
import { enabled } from "react-native-privacy-snapshot";
import ScreenGuardModule from "react-native-screenguard";
import { DatabaseLogger, db } from "../../common/database";
import { MMKV } from "../../common/database/mmkv";
import filesystem from "../../common/filesystem";
import { ChangePassword } from "../../components/auth/change-password";
import { presentDialog } from "../../components/dialog/functions";
@@ -55,11 +54,14 @@ import {
openVault,
presentSheet
} from "../../services/event-manager";
import { setLoginMessage } from "../../services/message";
import Navigation from "../../services/navigation";
import Notifications from "../../services/notifications";
import PremiumService from "../../services/premium";
import SettingsService from "../../services/settings";
import Sync from "../../services/sync";
import { clearAllStores } from "../../stores";
import { refreshAllStores } from "../../stores/create-db-collection-store";
import { useThemeStore } from "../../stores/use-theme-store";
import { useUserStore } from "../../stores/use-user-store";
import { SUBSCRIPTION_STATUS } from "../../utils/constants";
@@ -75,6 +77,9 @@ import { useDragState } from "./editor/state";
import { verifyUser, verifyUserWithApplock } from "./functions";
import { SettingSection } from "./types";
import { getTimeLeft } from "./user-section";
import Clipboard from "@react-native-clipboard/clipboard";
type User = any;
export const settingsGroups: SettingSection[] = [
{
@@ -114,10 +119,6 @@ export const settingsGroups: SettingSection[] = [
"MMMM D, YYYY"
);
if (user.subscription.provider === 4) {
return strings.subEndsOn(expiryDate);
}
return user.subscription?.type === 2
? strings.signedUpOn(startDate)
: user.subscription?.type === 1
@@ -133,39 +134,6 @@ export const settingsGroups: SettingSection[] = [
: strings.neverHesitate();
}
},
{
id: "redeem-gift-code",
name: strings.redeemGiftCode(),
description: strings.redeemGiftCodeDesc(),
hidden: (current) => {
return !current as boolean;
},
useHook: () =>
useUserStore(
(state) =>
state.user?.subscription.type == SUBSCRIPTION_STATUS.TRIAL ||
state.user?.subscription.type == SUBSCRIPTION_STATUS.BASIC
),
icon: "gift",
modifer: () => {
presentDialog({
title: strings.redeemGiftCode(),
paragraph: strings.redeemGiftCodeDesc(),
input: true,
inputPlaceholder: strings.code(),
positiveText: strings.redeem(),
positivePress: async (value) => {
db.subscriptions.redeemCode(value).catch((e) => {
ToastManager.show({
heading: "Error redeeming code",
message: (e as Error).message,
type: "error"
});
});
}
});
}
},
{
id: "account-settings",
type: "screen",
@@ -479,6 +447,18 @@ export const settingsGroups: SettingSection[] = [
});
await db.user?.logout();
setLoginMessage();
await PremiumService.setPremiumStatus();
await BiometricService.resetCredentials();
MMKV.clearStore();
clearAllStores();
setImmediate(() => {
refreshAllStores();
});
Navigation.queueRoutesForUpdate();
SettingsService.resetSettings();
useUserStore.getState().setUser(null);
useUserStore.getState().setSyncing(false);
endProgress();
} catch (e) {
DatabaseLogger.error(e);
@@ -1194,12 +1174,7 @@ export const settingsGroups: SettingSection[] = [
{
id: "select-backup-dir",
name: strings.selectBackupDir(),
description: () => {
const desc = strings.selectBackupDirDesc(
SettingsService.get().backupDirectoryAndroid?.path || ""
);
return desc[0] + " " + desc[1];
},
description: strings.selectBackupDirDesc(),
icon: "folder",
hidden: () =>
!!SettingsService.get().backupDirectoryAndroid ||

View File

@@ -126,19 +126,18 @@ export const Subscription = () => {
user.subscription?.type !== SUBSCRIPTION_STATUS.PREMIUM_EXPIRED &&
user.subscription?.type !== SUBSCRIPTION_STATUS.BASIC ? (
<Button
title={subscriptionProviderInfo?.title()}
title={subscriptionProviderInfo?.title}
onPress={() => {
presentSheet({
title: subscriptionProviderInfo.title(),
paragraph: subscriptionProviderInfo.desc()
title: subscriptionProviderInfo.title,
paragraph: subscriptionProviderInfo.desc
});
}}
style={{
alignSelf: "flex-start",
width: "100%",
paddingHorizontal: 0
borderRadius: 100
}}
fontSize={SIZE.xs}
fontSize={SIZE.sm}
height={30}
type="secondaryAccented"
/>

View File

@@ -85,13 +85,9 @@ async function checkBackupDirExists(reset = false, context = "global") {
resolve(await getDirectoryAndroid());
return;
}
const desc = strings.selectBackupDirDesc(
SettingsService.get().backupDirectoryAndroid?.path || ""
);
presentDialog({
title: strings.selectBackupDir(),
paragraph: desc[0] + " " + desc,
paragraph: strings.selectBackupDirDesc(),
positivePress: async () => {
resolve(await getDirectoryAndroid());
},

View File

@@ -23,7 +23,7 @@ import FingerprintScanner, {
} from "@ammarahmed/react-native-fingerprint-scanner";
import * as Keychain from "react-native-keychain";
import { MMKV } from "../common/database/mmkv";
import { Storage } from "../common/database/storage";
import Storage from "../common/database/storage";
import { useSettingStore } from "../stores/use-setting-store";
import { ToastOptions, ToastManager } from "./event-manager";
import { useUserStore } from "../stores/use-user-store";

View File

@@ -116,7 +116,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled true
versionCode 3034
versionCode 3033
versionName getNpmVersion()
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'

View File

@@ -129,16 +129,6 @@
<data android:mimeType="application/*" />
</intent-filter>
<intent-filter android:label="Make Note">
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/*" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
<data android:mimeType="image/*" />
<data android:mimeType="application/*" />
</intent-filter>
<intent-filter android:label="Make Note">
<action android:name="android.intent.action.PROCESS_TEXT" />
<category android:name="android.intent.category.DEFAULT" />

View File

@@ -1,5 +1,9 @@
- You can now share multiple files to Notesnook
- Fix file and image sharing not working
- Many other bug fixes and small improvements
- Added full support for localization in Notesnook
- Improved search experience
- Allow user to cancel logging in
- Fixed scrolling focused line into view
- Support self hosted monograph server
- Fixed markdown link pasting in editor
- Many other bug fixes and improvements
Thank you for using Notesnook!

View File

@@ -5,11 +5,11 @@
<key>provisioningProfiles</key>
<dict>
<key>org.streetwriters.notesnook</key>
<string>Notesnook App Distribution 2025</string>
<string>Notesnook App Distribution 2024</string>
<key>org.streetwriters.notesnook.notewidget</key>
<string>Notesnook Widget Distribution 2025</string>
<string>Notesnook Widget Distribution 2024</string>
<key>org.streetwriters.notesnook.share</key>
<string>Notesnook Share Distribution 2025</string>
<string>Notesnook Share Distribution 2024</string>
</dict>
</dict>
</plist>

View File

@@ -1063,7 +1063,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2121;
CURRENT_PROJECT_VERSION = 2120;
DEVELOPMENT_TEAM = 53CWBG3QUC;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1137,7 +1137,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.0.24;
MARKETING_VERSION = 3.0.23;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -1168,7 +1168,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 2121;
CURRENT_PROJECT_VERSION = 2120;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1242,7 +1242,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.0.24;
MARKETING_VERSION = 3.0.23;
ONLY_ACTIVE_ARCH = NO;
OTHER_LDFLAGS = (
"$(inherited)",
@@ -1252,7 +1252,7 @@
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook;
PRODUCT_NAME = Notesnook;
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Notesnook App Distribution 2025";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Notesnook App Distribution 2024";
SWIFT_OBJC_BRIDGING_HEADER = "Notesnook-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -1401,7 +1401,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2121;
CURRENT_PROJECT_VERSION = 2120;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1413,7 +1413,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.24;
MARKETING_VERSION = 3.0.23;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
@@ -1444,7 +1444,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2121;
CURRENT_PROJECT_VERSION = 2120;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1457,12 +1457,12 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.24;
MARKETING_VERSION = 3.0.23;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Notesnook Widget Distribution 2025";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Notesnook Widget Distribution 2024";
SKIP_INSTALL = YES;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
@@ -1487,7 +1487,7 @@
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2121;
CURRENT_PROJECT_VERSION = 2120;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1561,7 +1561,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.24;
MARKETING_VERSION = 3.0.23;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
@@ -1592,7 +1592,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2121;
CURRENT_PROJECT_VERSION = 2120;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1667,12 +1667,12 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.0.24;
MARKETING_VERSION = 3.0.23;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Notesnook Share Distribution 2025";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Notesnook Share Distribution 2024";
SKIP_INSTALL = YES;
SWIFT_OBJC_BRIDGING_HEADER = "Make Note/Make Note-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -1744,7 +1744,10 @@
ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = "$(inherited)";
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
OTHER_LDFLAGS = "$(inherited) ";
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;
@@ -1808,7 +1811,10 @@
ONLY_ACTIVE_ARCH = NO;
OTHER_CFLAGS = "$(inherited)";
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
OTHER_LDFLAGS = "$(inherited) ";
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;

View File

@@ -1019,7 +1019,7 @@ PODS:
- react-native-screenguard (1.0.0):
- React-Core
- SDWebImage (~> 5.11.1)
- react-native-share-extension (2.7.0):
- react-native-share-extension (2.6.0):
- React
- react-native-sodium (1.6.1):
- React
@@ -1858,7 +1858,7 @@ SPEC CHECKSUMS:
react-native-quick-sqlite: 18e1367c34faac90e37f6eb3e78c196e9b674b5d
react-native-safe-area-context: b7daa1a8df36095a032dff095a1ea8963cb48371
react-native-screenguard: 8b36a3df84c76cd2b82c477f71c26fa1c8cc14a0
react-native-share-extension: 17e42444d0d9fbfeb0a7392899def70f9534c9c4
react-native-share-extension: 25437eb1039f7409be6e80a7edf8d02b42e1dc99
react-native-sodium: 4cb76086943a7f60c42b40ebca866695b360a196
react-native-theme-switch-animation: d3eb50365a3829ce5572628888fa514752703f61
react-native-webview: 553abd09f58e340fdc7746c9e2ae096839e99911

View File

@@ -73,7 +73,7 @@
"react-native-begin-background-task": "github:blockfirm/react-native-begin-background-task",
"react-native-privacy-snapshot": "github:standardnotes/react-native-privacy-snapshot",
"@ammarahmed/react-native-fingerprint-scanner": "^5.0.0",
"@ammarahmed/react-native-share-extension": "^2.8.0"
"@ammarahmed/react-native-share-extension": "^2.7.0"
},
"devDependencies": {
"detox": "^20.27.6",

View File

@@ -14,6 +14,7 @@
"app/"
],
"dependencies": {
"@ammarahmed/react-native-share-extension": "^2.7.0",
"@notesnook/common": "file:../../packages/common",
"@notesnook/core": "file:../../packages/core",
"@notesnook/crypto": "file:../../packages/crypto",
@@ -28931,8 +28932,7 @@
"@ammarahmed/react-native-background-fetch": "^4.2.2",
"@ammarahmed/react-native-eventsource": "1.1.0",
"@ammarahmed/react-native-fingerprint-scanner": "^5.0.0",
"@ammarahmed/react-native-share-extension": "^2.8.0",
"@ammarahmed/react-native-sodium": "^1.6.1",
"@ammarahmed/react-native-sodium": "1.5.6",
"@bam.tech/react-native-image-resizer": "3.0.5",
"@callstack/repack": "^4.1.1",
"@formatjs/intl-locale": "4.0.0",
@@ -29070,9 +29070,9 @@
}
},
"node_modules/@ammarahmed/react-native-share-extension": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@ammarahmed/react-native-share-extension/-/react-native-share-extension-2.8.0.tgz",
"integrity": "sha512-eDFVKiJJxjnIORveMUKVJGIhKfRNjqJWIoKS+JM88q8I8e3jHys9moyJAr9QM5oW/tZx4yyItl08yRXGnr7Grg==",
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/@ammarahmed/react-native-share-extension/-/react-native-share-extension-2.7.0.tgz",
"integrity": "sha512-M7mNKv8k+j3SkD0NcRAOItoF7lMa61OYnTD7JOAZiBRHWkZDysc/2u6dbQGDcd6VMRif+xWRrhLILH+zFHkgPg==",
"dependencies": {
"react-native": "^0.63.1"
}
@@ -35720,14 +35720,15 @@
"license": "MIT"
},
"node_modules/call-bind": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.0",
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.2"
"set-function-length": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
@@ -35736,18 +35737,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
"integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/caller-callsite": {
"version": "2.0.0",
"license": "MIT",
@@ -36784,19 +36773,6 @@
"node": ">=0.10"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/duplexer": {
"version": "0.1.2",
"dev": true,
@@ -36911,6 +36887,29 @@
"node": ">= 0.8"
}
},
"node_modules/encoding": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"optional": true,
"peer": true,
"dependencies": {
"iconv-lite": "^0.6.2"
}
},
"node_modules/encoding/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"optional": true,
"peer": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/enhanced-resolve": {
"version": "5.17.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
@@ -37016,9 +37015,12 @@
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"engines": {
"node": ">= 0.4"
}
@@ -37036,17 +37038,6 @@
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz",
"integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw=="
},
"node_modules/es-object-atoms": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
"integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.0.1",
"dev": true,
@@ -38228,20 +38219,15 @@
}
},
"node_modules/get-intrinsic": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz",
"integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"dunder-proto": "^1.0.0",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.0.0",
"function-bind": "^1.1.2",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.0.0"
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
},
"engines": {
"node": ">= 0.4"
@@ -38395,11 +38381,10 @@
"license": "MIT"
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"engines": {
"node": ">= 0.4"
"version": "1.0.1",
"license": "MIT",
"dependencies": {
"get-intrinsic": "^1.1.3"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -38490,7 +38475,6 @@
},
"node_modules/has-proto": {
"version": "1.0.1",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -38500,9 +38484,8 @@
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"version": "1.0.3",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
@@ -41262,14 +41245,6 @@
"version": "1.2.5",
"license": "Apache-2.0"
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mdn-data": {
"version": "2.0.14",
"license": "CC0-1.0"
@@ -43145,28 +43120,6 @@
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
},
"peerDependencies": {
"react": "^18.3.1"
}
},
"node_modules/react-dom/node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"node_modules/react-freeze": {
"version": "1.0.3",
"license": "MIT",
@@ -44320,6 +44273,13 @@
"node": ">=10"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"optional": true,
"peer": true
},
"node_modules/sanitize-filename": {
"version": "1.6.3",
"dev": true,

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/mobile",
"version": "3.0.24",
"version": "3.0.23",
"private": true,
"license": "GPL-3.0-or-later",
"workspaces": [
@@ -52,4 +52,4 @@
"@trpc/server": "^10.45.2",
"@tanstack/react-query": "^4.36.1"
}
}
}

View File

@@ -81,8 +81,7 @@ const EXTRA_ICON_NAMES = [
"notebook-plus",
"arrow-right-bold-box-outline",
"arrow-up-bold",
"login",
"gift"
"login"
];
const __filename = fileURLToPath(import.meta.url);

View File

@@ -43,7 +43,7 @@ import { SafeAreaProvider } from "react-native-safe-area-context";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import isURL from "validator/lib/isURL";
import { DatabaseLogger, db } from "../app/common/database";
import { Storage } from "../app/common/database/storage";
import Storage from "../app/common/database/storage";
import { Button } from "../app/components/ui/button";
import Heading from "../app/components/ui/typography/heading";
import Paragraph from "../app/components/ui/typography/paragraph";
@@ -620,45 +620,41 @@ const ShareView = () => {
>
Tap to remove an attachment.
</Paragraph>
{rawFiles.some((item) => isImage(item.type)) ? (
<TouchableOpacity
activeOpacity={1}
<TouchableOpacity
activeOpacity={1}
style={{
flexDirection: "row",
alignSelf: "center",
alignItems: "center",
width: "100%",
marginTop: 6
}}
onPress={() => {
setCompress(!compress);
}}
>
<Icon
size={20}
name={
compress
? "checkbox-marked"
: "checkbox-blank-outline"
}
color={
compress ? colors.primary.accent : colors.primary.icon
}
/>
<Text
style={{
flexDirection: "row",
alignSelf: "center",
alignItems: "center",
width: "100%",
marginTop: 6
}}
onPress={() => {
setCompress(!compress);
flexShrink: 1,
marginLeft: 3,
fontSize: 12
}}
>
<Icon
size={20}
name={
compress
? "checkbox-marked"
: "checkbox-blank-outline"
}
color={
compress
? colors.primary.accent
: colors.primary.icon
}
/>
<Text
style={{
flexShrink: 1,
marginLeft: 3,
fontSize: 12
}}
>
Compress image(s) (recommended)
</Text>
</TouchableOpacity>
) : null}
Compress image (recommended)
</Text>
</TouchableOpacity>
</View>
) : null}
<View

View File

@@ -1,4 +1,4 @@
Subtotal: ₹400
Sales tax: ₹300
Discount: -₹400
Total: ₹400
Total: ₹400/yr

View File

@@ -1,4 +1,4 @@
Subtotal: ₹400
Sales tax: ₹300
Discount: -₹400
Total: ₹400
Total: ₹400/yr

View File

@@ -1,4 +1,4 @@
Subtotal: ₹400
Sales tax: ₹300
Discount: -₹400
Total: ₹400
Total: ₹400/yr

View File

@@ -1,4 +1,4 @@
Subtotal: $200
Sales tax: $0
Discount: -$200
Total: $200
Total: $200/yr

View File

@@ -1,4 +1,4 @@
Subtotal: $200
Sales tax: $0
Discount: -$200
Total: $200
Total: $200/yr

View File

@@ -1,4 +1,4 @@
Subtotal: $200
Sales tax: $0
Discount: -$200
Total: $200
Total: $200/yr

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { test, expect } from "@playwright/test";
import { AppModel } from "./models/app.model";
import { getTestId, NOTE, TITLE_ONLY_NOTE } from "./utils";
import { NOTE, TITLE_ONLY_NOTE } from "./utils";
test("focus mode", async ({ page }) => {
const app = new AppModel(page);
@@ -270,103 +270,3 @@ test("#1468 count words separated by newlines", async ({ page }) => {
expect((await notes.editor.getWordCount()) === 10).toBeTruthy();
});
test("disable autosave when note crosses MAX_AUTO_SAVEABLE_WORDS", async ({
page
}) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const content = "a ".repeat(100);
await notes.createNote({
title: "many words",
content
});
expect(
await app.toasts.waitForToast(
"Auto-save is disabled for large notes. Press Ctrl + S to save."
)
).toBe(true);
await expect(notes.editor.notSavedIcon).toBeVisible();
});
test("when autosave is disabled, pressing ctrl+s should save the note", async ({
page
}) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const content = "a ".repeat(100);
await notes.createNote({
title: NOTE.title,
content
});
await page.keyboard.press("Control+s");
await expect(notes.editor.savedIcon).toBeVisible();
});
test("when autosave is disabled, switching to another note should save the note", async ({
page
}) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const content = "a ".repeat(100);
const note1 = await notes.createNote({
title: "Test note 1"
});
const note2 = await notes.createNote({
title: "Test note 2"
});
await note1?.openNote();
await notes.editor.setContent(content);
await note2?.openNote();
await note1?.openNote();
await expect(notes.editor.savedIcon).toBeVisible();
expect(await notes.editor.getContent("text")).toBe(content.trim());
});
test("when autosave is disabled, creating a new note should save the note", async ({
page
}) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const content = "a ".repeat(100);
const note = await notes.createNote({
title: NOTE.title,
content
});
await notes.newNote();
await note?.openNote();
await expect(notes.editor.savedIcon).toBeVisible();
expect(await notes.editor.getContent("text")).toBe(content.trim());
});
test("when autosave is disabled, closing the note should save it", async ({
page
}) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const content = "a ".repeat(100);
const note = await notes.createNote({
title: "Title",
content
});
const noteTab = await notes.editor.findTab((await note!.getId())!);
await noteTab?.close();
await note?.openNote();
await expect(notes.editor.savedIcon).toBeVisible();
expect(await notes.editor.getContent("text")).toBe(content.trim());
});

View File

@@ -46,7 +46,7 @@ export class BaseItemModel {
}
async getId() {
return (await this.locator.getAttribute("id"))?.replace("id_", "");
return await this.locator.getAttribute("id");
}
async getTitle() {

View File

@@ -19,8 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Locator, Page } from "@playwright/test";
import { getTestId } from "../utils";
import { TabItemModel } from "./tab-item.model";
import { iterateList } from "./utils";
export class EditorModel {
private readonly page: Page;
@@ -35,9 +33,6 @@ export class EditorModel {
private readonly wordCountText: Locator;
private readonly dateEditedText: Locator;
private readonly searchButton: Locator;
private readonly tabsList: Locator;
readonly savedIcon: Locator;
readonly notSavedIcon: Locator;
constructor(page: Page) {
this.page = page;
@@ -56,9 +51,6 @@ export class EditorModel {
this.wordCountText = page.locator(getTestId("editor-word-count"));
this.dateEditedText = page.locator(getTestId("editor-date-edited"));
this.searchButton = page.locator(getTestId("Search"));
this.savedIcon = page.locator(getTestId("editor-save-state-saved"));
this.notSavedIcon = page.locator(getTestId("editor-save-state-notsaved"));
this.tabsList = page.locator(getTestId("tabs"));
}
async waitForLoading(title?: string, content?: string) {
@@ -235,11 +227,4 @@ export class EditorModel {
.replace(" words", "")
);
}
async findTab(id: string) {
for await (const item of iterateList(this.tabsList.locator(".tab"))) {
const tabModel = new TabItemModel(item, this.page);
if ((await tabModel.getId()) === id) return tabModel;
}
}
}

View File

@@ -1,36 +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 { Locator, Page } from "@playwright/test";
import { getTestId } from "../utils";
export class TabItemModel {
private readonly closeButton: Locator;
constructor(private readonly locator: Locator, page: Page) {
this.closeButton = locator.locator(getTestId("tab-close-button"));
}
async getId() {
const testId = await this.locator.getAttribute("data-test-id");
return testId?.replace("tab-", "");
}
close() {
return this.closeButton.click();
}
}

View File

@@ -20,7 +20,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { test, expect } from "@playwright/test";
import { AppModel } from "./models/app.model";
import {
getTestId,
groupByOptions,
NOTE,
orderByOptions,

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/web",
"version": "3.0.23",
"version": "3.0.22",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/web",
"version": "3.0.23",
"version": "3.0.22",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {

View File

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

View File

@@ -206,8 +206,7 @@ function AuthContainer(props) {
>
<Text variant={"subBody"}>
{version.status === "fulfilled" &&
!!version.value &&
version.value.instance !== "default" ? (
version.value?.instance !== "default" ? (
<>
{strings.usingInstance(
version.value.instance,

View File

@@ -30,7 +30,6 @@ import {
Lock,
NormalMode,
Note,
NoteRemove,
Pin,
Properties,
Publish,
@@ -45,7 +44,6 @@ import {
} from "../icons";
import { ScrollContainer } from "@notesnook/ui";
import {
SaveState,
SessionType,
isLockedSession,
useEditorStore
@@ -262,7 +260,7 @@ function TabStrip() {
style={{ flex: 1 }}
trackStyle={() => ({
backgroundColor: "transparent",
"--ms-track-size": "6px"
pointerEvents: "none"
})}
thumbStyle={() => ({ height: 3 })}
onWheel={(e) => {
@@ -283,7 +281,6 @@ function TabStrip() {
e.stopPropagation();
useEditorStore.getState().newSession();
}}
data-test-id="tabs"
>
<ReorderableList
items={sessions}
@@ -305,95 +302,89 @@ function TabStrip() {
sessions.splice(to, 0, fromTab);
useEditorStore.setState({ sessions });
}}
renderItem={({ item: session, index: i }) => {
const isUnsaved =
session.type === "default" &&
session.saveState === SaveState.NotSaved;
return (
<Tab
id={session.id}
key={session.id}
title={
session.title ||
("note" in session ? session.note.title : "Untitled")
}
isUnsaved={isUnsaved}
isTemporary={!!session.preview}
isActive={session.id === activeSessionId}
isPinned={!!session.pinned}
isLocked={isLockedSession(session)}
type={session.type}
onKeepOpen={() =>
useEditorStore
.getState()
.updateSession(
session.id,
[session.type],
(s) => (s.preview = false)
)
}
onFocus={() => {
if (session.id !== activeSessionId) {
useEditorStore.getState().openSession(session.id);
}
}}
onClose={() =>
useEditorStore.getState().closeSessions(session.id)
}
onCloseAll={() =>
useEditorStore
.getState()
.closeSessions(
...sessions.filter((s) => !s.pinned).map((s) => s.id)
)
}
onCloseOthers={() =>
useEditorStore
.getState()
.closeSessions(
...sessions
.filter((s) => s.id !== session.id && !s.pinned)
.map((s) => s.id)
)
}
onCloseToTheRight={() =>
useEditorStore
.getState()
.closeSessions(
...sessions
.filter((s, index) => index > i && !s.pinned)
.map((s) => s.id)
)
}
onCloseToTheLeft={() =>
useEditorStore
.getState()
.closeSessions(
...sessions
.filter((s, index) => index < i && !s.pinned)
.map((s) => s.id)
)
}
onRevealInList={() =>
AppEventManager.publish(
AppEvents.revealItemInList,
"note" in session ? session.note.id : session.id,
true
renderItem={({ item: session, index: i }) => (
<Tab
id={session.id}
key={session.id}
title={
session.title ||
("note" in session ? session.note.title : "Untitled")
}
isTemporary={!!session.preview}
isActive={session.id === activeSessionId}
isPinned={!!session.pinned}
isLocked={isLockedSession(session)}
type={session.type}
onKeepOpen={() =>
useEditorStore
.getState()
.updateSession(
session.id,
[session.type],
(s) => (s.preview = false)
)
}
onFocus={() => {
if (session.id !== activeSessionId) {
useEditorStore.getState().openSession(session.id);
}
onPin={() => {
useEditorStore.setState((state) => {
// preview tabs can never be pinned.
if (!session.pinned) state.sessions[i].preview = false;
state.sessions[i].pinned = !session.pinned;
state.sessions.sort((a, b) =>
a.pinned === b.pinned ? 0 : a.pinned ? -1 : 1
);
});
}}
/>
);
}}
}}
onClose={() =>
useEditorStore.getState().closeSessions(session.id)
}
onCloseAll={() =>
useEditorStore
.getState()
.closeSessions(
...sessions.filter((s) => !s.pinned).map((s) => s.id)
)
}
onCloseOthers={() =>
useEditorStore
.getState()
.closeSessions(
...sessions
.filter((s) => s.id !== session.id && !s.pinned)
.map((s) => s.id)
)
}
onCloseToTheRight={() =>
useEditorStore
.getState()
.closeSessions(
...sessions
.filter((s, index) => index > i && !s.pinned)
.map((s) => s.id)
)
}
onCloseToTheLeft={() =>
useEditorStore
.getState()
.closeSessions(
...sessions
.filter((s, index) => index < i && !s.pinned)
.map((s) => s.id)
)
}
onRevealInList={() =>
AppEventManager.publish(
AppEvents.revealItemInList,
"note" in session ? session.note.id : session.id,
true
)
}
onPin={() => {
useEditorStore.setState((state) => {
// preview tabs can never be pinned.
if (!session.pinned) state.sessions[i].preview = false;
state.sessions[i].pinned = !session.pinned;
state.sessions.sort((a, b) =>
a.pinned === b.pinned ? 0 : a.pinned ? -1 : 1
);
});
}}
/>
)}
/>
</Flex>
</ScrollContainer>
@@ -407,7 +398,6 @@ type TabProps = {
isTemporary: boolean;
isPinned: boolean;
isLocked: boolean;
isUnsaved: boolean;
type: SessionType;
onKeepOpen: () => void;
onFocus: () => void;
@@ -427,7 +417,6 @@ function Tab(props: TabProps) {
isTemporary,
isPinned,
isLocked,
isUnsaved,
type,
onKeepOpen,
onFocus,
@@ -447,8 +436,6 @@ function Tab(props: TabProps) {
? Readonly
: type === "deleted"
? Trash
: isUnsaved
? NoteRemove
: Note;
const { attributes, listeners, setNodeRef, transform, transition, active } =
useSortable({ id });
@@ -457,7 +444,6 @@ function Tab(props: TabProps) {
<Flex
ref={setNodeRef}
className="tab"
data-test-id={`tab-${id}`}
sx={{
borderRadius: "default",
cursor: "pointer",
@@ -558,13 +544,7 @@ function Tab(props: TabProps) {
if (e.button == 0) onFocus();
}}
>
<Icon
data-test-id={`tab-icon${isUnsaved ? "-unsaved" : ""}`}
size={16}
color={
isUnsaved ? "accent-error" : isActive ? "accent-selected" : "icon"
}
/>
<Icon size={16} color={isActive ? "accent-selected" : "icon"} />
<Text
variant="body"
sx={{
@@ -610,7 +590,6 @@ function Tab(props: TabProps) {
}
}}
className="closeTabButton"
data-test-id={"tab-close-button"}
size={16}
/>
)}

View File

@@ -121,13 +121,6 @@ function EditorFooter() {
) : null}
{SaveStateIcon && (
<SaveStateIcon
data-test-id={`editor-save-state-${
saveState === SaveState.Saved
? "saved"
: saveState === SaveState.NotSaved
? "notsaved"
: "loading"
}`}
size={13}
color={
saveState === SaveState.Saved

View File

@@ -71,12 +71,9 @@ import { logger } from "../../utils/logger";
import { PanelGroup, Panel, PanelResizeHandle } from "react-resizable-panels";
import { NoteLinkingDialog } from "../../dialogs/note-linking-dialog";
import { strings } from "@notesnook/intl";
import { onPageVisibilityChanged } from "../../utils/page-visibility";
const PDFPreview = React.lazy(() => import("../pdf-preview"));
const autoSaveToast = { show: true, hide: () => {} };
async function saveContent(
noteId: string,
ignoreEdit: boolean,
@@ -442,17 +439,9 @@ export function Editor(props: EditorProps) {
readonly: false,
focusMode: false
};
const saveSessionContentIfNotSaved = useEditorStore(
(store) => store.saveSessionContentIfNotSaved
);
const setEditorSaveState = useEditorStore((store) => store.setSaveState);
useScrollToBlock(session);
useEffect(() => {
if (!autoSaveToast.show) {
autoSaveToast.hide();
}
const event = AppEventManager.subscribe(
AppEvents.UPDATE_ATTACHMENT_PROGRESS,
({ hash, loaded, total }: AttachmentProgress) => {
@@ -469,15 +458,6 @@ export function Editor(props: EditorProps) {
};
}, [id]);
useEffect(() => {
const unsub = onPageVisibilityChanged((_, hidden) => {
if (hidden) {
saveSessionContentIfNotSaved(id);
}
});
return () => unsub();
}, []);
return (
<EditorChrome {...props}>
<Tiptap
@@ -579,25 +559,6 @@ export function Editor(props: EditorProps) {
const link = await NoteLinkingDialog.show({ attributes });
return link || undefined;
}}
onAutoSaveDisabled={() => {
setEditorSaveState(id, SaveState.NotSaved);
if (autoSaveToast.show === false) return;
const { hide } = showToast(
"error",
"Auto-save is disabled for large notes. Press Ctrl + S to save.",
[
{
text: "Dismiss",
onClick: () => {
hide();
}
}
],
Infinity
);
autoSaveToast.show = false;
autoSaveToast.hide = hide;
}}
>
{headless ? null : (
<>

View File

@@ -63,7 +63,6 @@ import useTablet from "../../hooks/use-tablet";
import { TimeFormat } from "@notesnook/core";
import { BuyDialog } from "../../dialogs/buy-dialog";
import { EDITOR_ZOOM } from "./common";
import { ScrollContainer } from "@notesnook/ui";
export type OnChangeHandler = (
content: () => string,
@@ -90,7 +89,6 @@ type TipTapProps = {
) => Promise<LinkAttributes | undefined>;
onAttachFile?: (file: File) => void;
onFocus?: () => void;
onAutoSaveDisabled: () => void;
content?: () => string | undefined;
readonly?: boolean;
nonce?: number;
@@ -134,11 +132,12 @@ function TipTap(props: TipTapProps) {
onInsertInternalLink,
onContentChange,
onFocus = () => {},
onAutoSaveDisabled,
content,
editorContainer,
readonly,
nonce,
isMobile,
isTablet,
downloadOptions,
fontSize,
fontFamily,
@@ -348,9 +347,6 @@ function TipTap(props: TipTapProps) {
(s) => s.editors[id]?.statistics?.words.total,
(totalWords) => {
autoSave.current = !totalWords || totalWords < MAX_AUTO_SAVEABLE_WORDS;
if (!autoSave.current) {
onAutoSaveDisabled();
}
}
);
return () => {
@@ -370,36 +366,14 @@ function TipTap(props: TipTapProps) {
zIndex: 2
}}
>
<ScrollContainer
className="toolbarScroll"
suppressScrollY
style={{ display: "flex" }}
trackStyle={() => ({
backgroundColor: "transparent",
"--ms-track-size": "6px"
})}
thumbStyle={() => ({ height: 3 })}
onWheel={(e) => {
const scrollcontainer = document.querySelector(
".active .toolbarScroll"
);
if (!scrollcontainer) return;
if (e.deltaY > 0) scrollcontainer.scrollLeft += 100;
else if (e.deltaY < 0) scrollcontainer.scrollLeft -= 100;
}}
>
<Toolbar
editor={editor}
location={"top"}
sx={{
flexWrap: "unset",
overflowX: "unset"
}}
tools={toolbarConfig}
defaultFontFamily={fontFamily}
defaultFontSize={fontSize}
/>
</ScrollContainer>
<Toolbar
editor={editor}
location={"top"}
sx={isTablet || isMobile ? { flexWrap: "nowrap" } : {}}
tools={toolbarConfig}
defaultFontFamily={fontFamily}
defaultFontSize={fontSize}
/>
</ScopedThemeProvider>
</>
);
@@ -602,9 +576,7 @@ function toIEditor(editor: Editor): IEditor {
},
{ query: (a) => a.hash === hash, preventUpdate: true }
),
startSearch: () => editor.commands.startSearch(),
getContent: () =>
getHTMLFromFragment(editor.state.doc.content, editor.schema)
startSearch: () => editor.commands.startSearch()
};
}

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Attachment } from "@notesnook/editor";
export const MAX_AUTO_SAVEABLE_WORDS = IS_TESTING ? 100 : 100_000;
export const MAX_AUTO_SAVEABLE_WORDS = 100_000;
export type NoteStatistics = {
words: {
@@ -39,5 +39,4 @@ export interface IEditor {
attachFile: (file: Attachment) => void;
sendAttachmentProgress: (hash: string, progress: number) => void;
startSearch: () => void;
getContent: () => string;
}

View File

@@ -33,11 +33,7 @@ import { db } from "../../common/db";
import { createDialect } from "../../common/sqlite";
import { getDeviceInfo } from "../../utils/platform";
const IGNORED_ERRORS = [
"Error in input stream",
"network error",
"NetworkError when attempting to fetch resource."
];
const IGNORED_ERRORS = ["Error in input stream", "network error"];
export function GlobalErrorHandler(props: PropsWithChildren) {
const { showBoundary } = useErrorBoundary();

View File

@@ -74,7 +74,7 @@ const groupByMenu: (options: GroupingMenuOptions) => MenuItem | null = (
icon: GroupBy.path,
menu: {
items: map(options, [
{ key: "none", title: strings.groupByStrings.none() },
{ key: "none", title: strings.groupByStrings.None() },
{ key: "default", title: strings.groupByStrings.default() },
{ key: "year", title: strings.groupByStrings.year() },
{ key: "month", title: strings.groupByStrings.month() },

View File

@@ -218,8 +218,7 @@ import {
mdiServerSecurity,
mdiOpenInNew,
mdiTagOutline,
mdiChatQuestionOutline,
mdiNoteRemoveOutline
mdiChatQuestionOutline
} from "@mdi/js";
import { useTheme } from "@emotion/react";
import { Theme } from "@notesnook/theme";
@@ -314,7 +313,6 @@ function createIcon(path: string, rotate = false) {
export const Plus = createIcon(mdiPlus);
export const Note = createIcon(mdiNoteOutline);
export const NoteRemove = createIcon(mdiNoteRemoveOutline);
export const Notes = createIcon(mdiNoteMultipleOutline);
export const Minus = createIcon(mdiMinus);
export const Notebook = createIcon(mdiBookOutline);

View File

@@ -147,6 +147,7 @@ function getDate(item: Item, groupType?: GroupingKey): number {
groupType
? db.settings.getGroupOptions(groupType)
: {
groupBy: "default",
sortBy: "dateEdited",
sortDirection: "desc"
},

View File

@@ -93,14 +93,29 @@ const features: Record<FeatureKeys, Feature> = {
]
: [
{
title: "Notesnook Gift Cards",
title: "5x faster startup",
subtitle:
"You can now gift Notesnook Pro to your friends and family. Gift cards are available in 1, 3, and 5-year plans at https://notesnook.com/giftcards."
"We have optimized the app startup time by 5x (3000ms to 600ms). This means you can now start taking notes faster than ever."
},
{
title: "Sorted search results",
title: "Improved ToC UX",
subtitle:
"Search results are now sorted by date created by default."
"We have made a bunch of improvements to the ToC (Table of Contents): it no longer covers the note content, and its open/closed state is preserved between app restarts."
},
{
title: "Faster checkout",
subtitle:
"Checkout is now 1-step. You can now directly enter your payment details and complete the purchase without any intermediate steps."
},
{
title: "Zoom in Editor",
subtitle:
"You can now zoom in/out of the editor using the Ctrl/Cmd + mouse wheel. This is separate from the default font size."
},
{
title: "Improved search",
subtitle:
"Search now defaults to AND mode instead of OR mode which better matches the expected behavior. You can still use OR if you prefer the old behavior."
}
],
cta: {

View File

@@ -45,6 +45,7 @@ export const AuthenticationSettings: SettingsGroup[] = [
title: strings.changePassword(),
variant: "secondary",
action: async () => {
if (!(await createBackup())) return;
const result = await showPasswordDialog({
title: strings.changePassword(),
message: strings.changePasswordDesc(),
@@ -59,7 +60,6 @@ export const AuthenticationSettings: SettingsGroup[] = [
}
},
validate: async ({ oldPassword, newPassword }) => {
if (!(await createBackup())) return false;
await db.user.clearSessions();
return (
(await db.user.changePassword(oldPassword, newPassword)) ||

View File

@@ -30,8 +30,13 @@ import { Features } from "../../buy-dialog/features";
import { ConfirmDialog } from "../../confirm";
import { BuyDialog } from "../../buy-dialog";
import { strings } from "@notesnook/intl";
import { PromptDialog } from "../../prompt";
const PROVIDER_MAP = {
0: "Streetwriters",
1: "iOS",
2: "Android",
3: "Web"
} as const;
export function SubscriptionStatus() {
const user = useUserStore((store) => store.user);
@@ -39,8 +44,7 @@ export function SubscriptionStatus() {
await db.user.activateTrial();
});
const provider =
strings.subscriptionProviderInfo[user?.subscription?.provider || 0];
const provider = PROVIDER_MAP[user?.subscription?.provider || 0];
const {
isTrial,
isBeta,
@@ -68,7 +72,7 @@ export function SubscriptionStatus() {
const expiryDate = dayjs(user?.subscription?.expiry).format("MMMM D, YYYY");
const startDate = dayjs(user?.subscription?.start).format("MMMM D, YYYY");
return isPro
? provider.type === "Streetwriters" || provider.type === "Gift card"
? provider === "Streetwriters"
? `Ending on ${expiryDate}`
: `Next payment on ${expiryDate}.`
: isProCancelled
@@ -128,10 +132,10 @@ export function SubscriptionStatus() {
: "Access only to basic features including unlimited notes & end-to-end encrypted syncing to unlimited devices."}
</Text>
<Text sx={{ mt: 2 }} variant="subBody">
{subtitle}. {provider.desc()}
{subtitle}
</Text>
<Flex sx={{ gap: 1, mt: 2 }}>
{provider.type === "Web" && (isPro || isProCancelled) ? (
{provider === "Web" && (isPro || isProCancelled) ? (
<>
{isPro && (
<Button
@@ -205,26 +209,6 @@ export function SubscriptionStatus() {
)}
</Button>
)}
<Button
variant="secondary"
onClick={async () => {
const giftCode = await PromptDialog.show({
title: strings.redeemGiftCode(),
description: strings.redeemGiftCodeDesc()
});
if (giftCode) {
await TaskManager.startTask({
type: "modal",
title: strings.redeemingGiftCode(),
subtitle: strings.pleaseWait() + "...",
action: () => db.subscriptions.redeemCode(giftCode)
}).catch((e) => showToast("error", e.message));
}
}}
sx={{ bg: "background" }}
>
{strings.redeemGiftCode()}
</Button>
</>
)}
</Flex>

View File

@@ -172,17 +172,6 @@ export class NNStorage implements IStorage {
return key;
}
}
// noop
generateCryptoKeyFallback(
password: string,
salt?: string
): Promise<SerializedKey> {
return this.generateCryptoKey(password, salt);
}
// noop
async deriveCryptoKeyFallback(): Promise<void> {}
}
const dec = new TextDecoder();

View File

@@ -49,7 +49,6 @@ import { hashNavigate } from "../navigation";
import { AppEventManager, AppEvents } from "../common/app-events";
import Vault from "../common/vault";
import { Mutex } from "async-mutex";
import { useEditorManager } from "../components/editor/manager";
export enum SaveState {
NotSaved = -1,
@@ -502,12 +501,6 @@ class EditorStore extends BaseStore<EditorStore> {
const session = this.get().sessions.find((s) => s.id === id);
if (!session) id = undefined;
const activeSession = this.getActiveSession();
if (activeSession) {
this.saveSessionContentIfNotSaved(activeSession.id);
}
if (
id &&
!settingStore.get().hideNoteTitle &&
@@ -855,6 +848,7 @@ class EditorStore extends BaseStore<EditorStore> {
sessionId
});
}
setDocumentTitle(
settingStore.get().hideNoteTitle ? undefined : note.title
);
@@ -876,25 +870,6 @@ class EditorStore extends BaseStore<EditorStore> {
});
};
saveSessionContentIfNotSaved = (sessionId: string) => {
const sessionSaveState = this.getSession(sessionId, ["default"])?.saveState;
if (sessionSaveState === SaveState.NotSaved) {
const editor = useEditorManager.getState().getEditor(sessionId);
const content = editor?.editor?.getContent();
this.saveSession(
sessionId,
content
? {
content: {
data: content,
type: "tiptap"
}
}
: {}
);
}
};
newSession = () => {
const state = useEditorStore.getState();
const session = state.sessions.find((session) => session.type === "new");
@@ -921,8 +896,6 @@ class EditorStore extends BaseStore<EditorStore> {
continue;
}
this.saveSessionContentIfNotSaved(session.id);
db.fs().cancel(session.id).catch(console.error);
if (state.history.includes(session.id))
state.history.splice(state.history.indexOf(session.id), 1);

View File

@@ -31,7 +31,6 @@ import {
import { path } from "@notesnook-importer/core/dist/src/utils/path";
import { type ZipEntry } from "./streams/unzip-stream";
import { hashBuffer, writeEncryptedFile } from "../interfaces/fs";
import { Notebook as NotebookType } from "@notesnook/core";
export async function* importFiles(zipFiles: File[]) {
const { createUnzipIterator } = await import("./streams/unzip-stream");
@@ -220,29 +219,26 @@ async function importLegacyNotebook(
async function importNotebook(
notebook: Notebook,
parent?: NotebookType
parentId?: string
): Promise<string[]> {
if (!notebook) return [];
const selector = parent
? db.relations.from(parent, "notebook").selector
: db.notebooks.roots;
let nb = await selector.find((eb) =>
eb("notebooks.title", "==", notebook.title)
);
if (!nb) {
const id = await db.notebooks.add({
const id =
(await db.notebooks.find(notebook.title))?.id ||
(await db.notebooks.add({
title: notebook.title
});
if (!id) return [];
nb = await db.notebooks.notebook(id);
if (parent && nb) await db.relations.add(parent, nb);
}
if (!nb) return [];
if (notebook.children.length === 0) return [nb.id];
}));
if (!id) throw new Error(`Failed to import notebook: ${notebook.title}`);
const assignedNotebooks: string[] = [];
for (const child of notebook.children || [])
assignedNotebooks.push(...(await importNotebook(child, nb)));
if (parentId)
await db.relations.add(
{ type: "notebook", id: parentId },
{ type: "notebook", id: id }
);
const assignedNotebooks: string[] = notebook.children.length > 0 ? [] : [id];
for (const child of notebook.children || []) {
assignedNotebooks.push(...(await importNotebook(child, id)));
}
return assignedNotebooks;
}

View File

@@ -310,8 +310,7 @@ function RecoveryMethods(props: BaseRecoveryComponentProps<"methods">) {
sx={{
color: method.isDangerous
? "var(--paragraph-error)"
: "var(--paragraph-secondary)",
whiteSpace: "pre-wrap"
: "var(--paragraph-secondary)"
}}
>
{method.description()}

View File

@@ -1,84 +0,0 @@
---
title: Gift cards
description: Gift your friends and family a Notesnook Pro subscription.
---
# Gift cards
Gift cards allow you to gift anyone a free Notesnook Pro subscription. It's a simple 3-step process:
1. You purchase a gift card.
2. You send someone the gift code.
3. They redeem it and claim the benefits.
## Purchasing a gift card
You can purchase a gift card from [https://notesnook.com/giftcards](https://notesnook.com/giftcards). There are 3 gift cards currently:
- 1 year gift card
- 3 years gift card
- 5 years gift card
> info
>
> Gift cards are not attached to a user account and can be claimed by any Notesnook user.
## Redeem a gift code
> info
>
> You won't be able to redeem a gift code if you already have a Notesnook subscription (even if its cancelled).
# [Desktop/Web](#/tab/web)
1. Go to `Settings`
2. Go to `Subscription settings`
3. Click on `Redeem a gift code` button
4. Enter the gift code you received
5. Click on `Submit` and wait for the app to verify your gift code.
6. Once the process succeeds, you should be upgraded to Pro.
# [Mobile](#/tab/mobile)
1. Go to `Settings`
2. Go to `Account settings`
3. Click on `Redeem a gift code`
4. Enter the gift code you received
5. Click on `Redeem` and wait for the app to verify your gift code.
6. Once the process succeeds, you should be upgraded to Pro.
---
> info
>
> Once you redeem a gift code, the person who purchased it will receive an email informing them that one of their gift code was claimed. The email **does not** contain any information about who claimed the gift code.
## FAQs
### Who can redeem a gift code?
Anyone who doesn't have a Notesnook Pro subscription can redeem a gift code and upgrade their account. One user can only redeem a single gift code at a time.
### Can I use a gift card to extend my subscription?
No.
### Is there an expiry date to a gift code?
Yes, these gift codes will expire after 1 year from the time of purchase.
### What can I do with a gift code?
Whatever you like. Sell it, gift it, use it.
### Can I refund a gift code I purchased?
No. Gift codes are non-refundable.
### Are gift cards auto-renewable?
No. Gift cards are a one-time purchase.
### Can I use cryptocurrency to purchase a gift card?
Currently, no. But we are actively working on a solution to support this so stay tuned.

View File

@@ -55,7 +55,6 @@ navigation:
- path: recovering-your-account.md
- path: deleting-your-account.md
- path: app-lock.md
- path: gift-cards.md
- path: privacy-mode.md
- path: web-clipper

View File

@@ -1,5 +0,0 @@
- You can now share multiple files to Notesnook
- Fix file and image sharing not working
- Many other bug fixes and small improvements
Thank you for using Notesnook!

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { match } from "fuzzyjs";
import Database from "./index.js";
import { Item, Note, SortOptions, TrashItem } from "../types.js";
import { Item, Note, TrashItem } from "../types.js";
import { DatabaseSchema, RawDatabaseSchema } from "../database/index.js";
import { AnyColumnWithTable, Kysely, sql } from "@streetwriters/kysely";
import { FilteredSelector } from "../database/sql-collection.js";
@@ -27,7 +27,6 @@ import { VirtualizedGrouping } from "../utils/virtualized-grouping.js";
import { logger } from "../logger.js";
import { rebuildSearchIndex } from "../database/fts.js";
import { transformQuery } from "../utils/query-transformer.js";
import { getSortSelectors } from "../utils/grouping.js";
type SearchResults<T> = {
sorted: (limit?: number) => Promise<VirtualizedGrouping<T>>;
@@ -44,7 +43,7 @@ export default class Lookup {
constructor(private readonly db: Database) {}
notes(query: string, notes?: FilteredSelector<Note>): SearchResults<Note> {
return this.toSearchResults(async (limit, sortOptions) => {
return this.toSearchResults(async (limit) => {
const db = this.db.sql() as unknown as Kysely<RawDatabaseSchema>;
const excludedIds = this.db.trash.cache.notes;
@@ -72,16 +71,13 @@ export default class Lookup {
)
.where("data", "match", query)
.select(["noteId as id", "rank"])
.$castTo<{
id: string;
rank: number;
}>()
.$castTo<{ id: string; rank: number }>()
)
.as("results")
)
.select(["results.id"])
.groupBy("results.id")
.orderBy(sql`SUM(results.rank)`, sortOptions?.sortDirection || "desc")
.orderBy(sql`SUM(results.rank)`, "desc")
.$if(!!limit, (eb) => eb.limit(limit!))
// filter out ids that have no note against them
@@ -123,17 +119,9 @@ export default class Lookup {
}
trash(query: string): SearchResults<TrashItem> {
const sortOptions: SortOptions = {
sortBy: "dateDeleted",
sortDirection: "desc"
};
return {
sorted: async (limit?: number) => {
const { ids, items } = await this.filterTrash(
query,
limit,
sortOptions
);
const { ids, items } = await this.filterTrash(query, limit);
return new VirtualizedGrouping<TrashItem>(
ids.length,
this.db.options.batchSize,
@@ -147,7 +135,7 @@ export default class Lookup {
);
},
items: async (limit?: number) => {
const { items } = await this.filterTrash(query, limit, sortOptions);
const { items } = await this.filterTrash(query, limit);
return items;
},
ids: () => this.filterTrash(query).then(({ ids }) => ids)
@@ -169,8 +157,7 @@ export default class Lookup {
fields: FuzzySearchField<T>[]
) {
return this.toSearchResults(
(limit, sortOptions) =>
this.filter(selector, query, fields, limit, sortOptions),
(limit) => this.filter(selector, query, fields, limit),
selector
);
}
@@ -179,8 +166,7 @@ export default class Lookup {
selector: FilteredSelector<T>,
query: string,
fields: FuzzySearchField<T>[],
limit?: number,
sortOptions?: SortOptions
limit?: number
) {
const results: Map<string, number> = new Map();
const columns = fields.map((f) => f.column);
@@ -197,46 +183,24 @@ export default class Lookup {
}
selector.fields([]);
const sorted = Array.from(results.entries());
if (!sortOptions)
// || sortOptions.sortBy === "relevance")
sorted.sort(
// sortOptions?.sortDirection === "desc"
// ? (a, b) => a[1] - b[1]
// :
(a, b) => b[1] - a[1]
);
return sorted.map((a) => a[0]);
return Array.from(results.entries())
.sort((a, b) => a[1] - b[1])
.map((a) => a[0]);
}
private toSearchResults<T extends Item>(
ids: (limit?: number, sortOptions?: SortOptions) => Promise<string[]>,
ids: (limit?: number) => Promise<string[]>,
selector: FilteredSelector<T>
): SearchResults<T> {
const sortOptions: SortOptions = {
sortBy: "dateCreated",
sortDirection: "desc"
};
return {
sorted: async (limit?: number) =>
this.toVirtualizedGrouping(
await ids(limit, sortOptions),
selector,
sortOptions
),
items: async (limit?: number) =>
this.toItems(await ids(limit, sortOptions), selector, sortOptions),
this.toVirtualizedGrouping(await ids(limit), selector),
items: async (limit?: number) => this.toItems(await ids(limit), selector),
ids
};
}
private async filterTrash(
query: string,
limit?: number,
sortOptions?: SortOptions
) {
private async filterTrash(query: string, limit?: number) {
const items = await this.db.trash.all();
const results: Map<string, { rank: number; item: TrashItem }> = new Map();
@@ -248,20 +212,10 @@ export default class Lookup {
results.set(item.id, { rank: result.score, item });
}
}
const sorted = Array.from(results.entries());
if (!sortOptions)
// || sortOptions.sortBy === "relevance")
sorted.sort(
// sortOptions?.sortDirection === "desc"
// ? (a, b) => a[1].rank - b[1].rank
// :
(a, b) => b[1].rank - a[1].rank
);
else {
const selector = getSortSelectors(sortOptions)[sortOptions.sortDirection];
sorted.sort((a, b) => selector(a[1].item, b[1].item));
}
const sorted = Array.from(results.entries()).sort(
(a, b) => a[1].rank - b[1].rank
);
return {
ids: sorted.map((a) => a[0]),
items: sorted.map((a) => a[1].item)
@@ -270,33 +224,28 @@ export default class Lookup {
private toVirtualizedGrouping<T extends Item>(
ids: string[],
selector: FilteredSelector<T>,
sortOptions?: SortOptions
selector: FilteredSelector<T>
) {
// if (sortOptions?.sortBy === "relevance") sortOptions = undefined;
return new VirtualizedGrouping<T>(
ids.length,
this.db.options.batchSize,
() => Promise.resolve(ids),
async (start, end) => {
const items = await selector.items(ids.slice(start, end), sortOptions);
const items = await selector.records(ids);
return {
ids: ids.slice(start, end),
items
items: Object.values(items).slice(start, end)
};
}
// (items) => groupArray(items, () => `${items.length} results`)
);
}
private toItems<T extends Item>(
ids: string[],
selector: FilteredSelector<T>,
sortOptions?: SortOptions
selector: FilteredSelector<T>
) {
if (!ids.length) return [];
// if (sortOptions?.sortBy === "relevance") sortOptions = undefined;
return selector.items(ids, sortOptions);
return selector.items(ids);
}
async rebuild() {

View File

@@ -90,16 +90,4 @@ export default class Subscriptions {
token
);
}
async redeemCode(code: string) {
const token = await this.tokenManager.getAccessToken();
if (!token) return;
return http.post.json(
`${hosts.SUBSCRIPTIONS_HOST}/subscriptions/redeem`,
{
code
},
token
);
}
}

View File

@@ -190,6 +190,7 @@ class UserManager {
salt: user.salt
});
}
await this.db.kv().write("usesFallbackPWHash", usesFallback);
EV.publish(EVENTS.userLoggedIn, user);
} catch (e) {
await this.tokenManager.saveToken(token);
@@ -332,7 +333,7 @@ class UserManager {
`${constants.API_HOST}${ENDPOINTS.deleteUser}`,
{
password: await this.db.storage().hash(password, user.email, {
usesFallback: await this.usesFallbackPWHash(password)
usesFallback: await this.db.kv().read("usesFallbackPWHash")
})
},
token
@@ -484,12 +485,17 @@ class UserManager {
type: "change_email",
new_email: newEmail,
password: await this.db.storage().hash(password, email, {
usesFallback: await this.usesFallbackPWHash(password)
usesFallback: await this.db.kv().read("usesFallbackPWHash")
}),
verification_code: code
},
token
);
await this.db.storage().deriveCryptoKey({
password,
salt: user.salt
});
}
recoverAccount(email: string) {
@@ -540,11 +546,6 @@ class UserManager {
if (data.encryptionKey) await this.db.sync({ type: "fetch", force: true });
if (old_password)
old_password = await this.db.storage().hash(old_password, email, {
usesFallback: await this.usesFallbackPWHash(old_password)
});
await this.db.storage().deriveCryptoKey({
password: new_password,
salt
@@ -563,6 +564,10 @@ class UserManager {
await this.updateUser({ attachmentsKey: user.attachmentsKey });
}
if (old_password)
old_password = await this.db.storage().hash(old_password, email, {
usesFallback: await this.db.kv().read("usesFallbackPWHash")
});
if (new_password)
new_password = await this.db.storage().hash(new_password, email);
@@ -575,33 +580,10 @@ class UserManager {
},
token
);
await this.db.kv().write("usesFallbackPWHash", false);
return true;
}
private async usesFallbackPWHash(password: string) {
const user = await this.getUser();
const encryptionKey = await this.getEncryptionKey();
if (!user || !encryptionKey) return false;
const fallbackCryptoKey = await this.db
.storage()
.generateCryptoKeyFallback(password, user.salt);
if (!fallbackCryptoKey) return false;
const cryptoKey = await this.db
.storage()
.generateCryptoKey(password, user.salt);
if (!encryptionKey.key || !fallbackCryptoKey.key || !cryptoKey.key)
throw new Error("Failed to generate crypto keys.");
if (
fallbackCryptoKey.key !== encryptionKey.key &&
cryptoKey.key !== encryptionKey.key
)
throw new Error("Wrong password.");
return fallbackCryptoKey.key === encryptionKey.key;
}
}
export default UserManager;

View File

@@ -30,6 +30,7 @@ interface KV {
deviceId: string;
lastBackupTime: number;
fullOfflineMode: boolean;
usesFallbackPWHash: boolean;
}
export const KEYS: (keyof KV)[] = [
@@ -40,7 +41,8 @@ export const KEYS: (keyof KV)[] = [
"monographs",
"deviceId",
"lastBackupTime",
"fullOfflineMode"
"fullOfflineMode",
"usesFallbackPWHash"
];
export class KVStorage {

View File

@@ -402,9 +402,7 @@ export class FilteredSelector<T extends Item> {
return (
await this.filter
.$if(!!sortOptions, (eb) =>
eb.$call(
this.buildSortExpression({ ...sortOptions!, groupBy: "none" })
)
eb.$call(this.buildSortExpression(sortOptions!))
)
.select("id")
.execute()
@@ -416,7 +414,7 @@ export class FilteredSelector<T extends Item> {
return (await this.filter
.$if(!!ids && ids.length > 0, (eb) => eb.where("id", "in", ids!))
.$if(!!sortOptions, (eb) =>
eb.$call(this.buildSortExpression({ ...sortOptions!, groupBy: "none" }))
eb.$call(this.buildSortExpression(sortOptions!))
)
.$if(this._fields.length === 0, (eb) => eb.selectAll())
.$if(this._fields.length > 0, (eb) => eb.select(this._fields))
@@ -530,9 +528,7 @@ export class FilteredSelector<T extends Item> {
if (options.groupBy === "abc") fields.push("title");
else if (options.sortBy === "title" && options.groupBy !== "none")
fields.push("dateCreated");
else if (options.sortBy !== "dueDate")
// && options.sortBy !== "relevance")
fields.push(options.sortBy);
else if (options.sortBy !== "dueDate") fields.push(options.sortBy);
return Array.from(
groupArray(
@@ -554,7 +550,7 @@ export class FilteredSelector<T extends Item> {
() => this.ids(options),
async (start, end) => {
const items = (await this.filter
.$call(this.buildSortExpression({ ...options, groupBy: "none" }))
.$call(this.buildSortExpression(options))
.offset(start)
.limit(end - start)
.selectAll()
@@ -600,13 +596,18 @@ export class FilteredSelector<T extends Item> {
}
}
private buildSortExpression(options: GroupOptions, hasDueDate?: boolean) {
private buildSortExpression(
options: GroupOptions | SortOptions,
hasDueDate?: boolean
) {
sanitizeSortOptions(this.type, options);
const sortBy: Set<SortOptions["sortBy"]> = new Set();
if (options.groupBy === "abc") sortBy.add("title");
else if (options.sortBy === "title" && options.groupBy !== "none")
sortBy.add("dateCreated");
if (isGroupOptions(options)) {
if (options.groupBy === "abc") sortBy.add("title");
else if (options.sortBy === "title" && options.groupBy !== "none")
sortBy.add("dateCreated");
}
sortBy.add(options.sortBy);
return <T>(
@@ -643,8 +644,7 @@ export class FilteredSelector<T extends Item> {
(qb) => qb.parens(createUpcomingReminderTimeQuery()),
options.sortDirection
);
} // if (item !== "relevance")
else qb = qb.orderBy(item, options.sortDirection);
} else qb = qb.orderBy(item, options.sortDirection);
continue;
}

View File

@@ -66,10 +66,6 @@ export interface IStorage {
getCryptoKey(): Promise<string | undefined>;
generateCryptoKey(password: string, salt?: string): Promise<SerializedKey>;
generateCryptoKeyFallback(
password: string,
salt?: string
): Promise<SerializedKey>;
deriveCryptoKeyFallback(credentials: SerializedKey): Promise<void>;
// async generateRandomKey() {

View File

@@ -536,7 +536,7 @@ export type User = {
cancelURL: string | null;
expiry: number;
productId: string;
provider: 0 | 1 | 2 | 3 | 4;
provider: 0 | 1 | 2 | 3;
start: number;
type: 0 | 1 | 2 | 5 | 6 | 7;
updateURL: string | null;

View File

@@ -18,13 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { isReminderActive } from "../collections/reminders.js";
import {
GroupHeader,
GroupOptions,
ItemType,
Reminder,
SortOptions
} from "../types.js";
import { GroupHeader, GroupOptions, ItemType, Reminder } from "../types.js";
import { getWeekGroupFromTimestamp, MONTHS_FULL } from "../utils/date.js";
type PartialGroupableItem = {
@@ -39,7 +33,7 @@ type PartialGroupableItem = {
export type GroupKeySelectorFunction<T> = (item: T) => string;
export const getSortValue = (
options: SortOptions | undefined,
options: GroupOptions | undefined,
item: PartialGroupableItem
) => {
if (
@@ -59,7 +53,7 @@ export const getSortValue = (
};
export function getSortSelectors<T extends PartialGroupableItem>(
options: SortOptions
options: GroupOptions
) {
if (options.sortBy === "title")
return {

View File

@@ -230,9 +230,6 @@ export function TaskListComponent(
ref={forwardRef}
dir={textDirection}
contentEditable={editor.isEditable && !readonly}
onPaste={(e) => {
if (readonly) e.preventDefault();
}}
sx={{
ul: {
display: "block",

View File

@@ -32,11 +32,7 @@ import {
findParentNodeClosestToPos,
getExactChangedNodes
} from "../../utils/prosemirror.js";
import {
countCheckedItems,
findRootTaskList,
toggleChildren
} from "./utils.js";
import { countCheckedItems, findRootTaskList, toggleChildren } from "./utils.js";
import { Node as ProsemirrorNode } from "@tiptap/pm/model";
import { TaskItemNode } from "../task-item/index.js";
@@ -53,16 +49,7 @@ export const TaskListNode = TaskList.extend({
return {
stats: {
default: { checked: 0, total: 0 },
rendered: false,
parseHTML: (element) => {
// do not update stats for nested task lists
if (!!element.closest("ul")) return { checked: 0, total: 0 };
const total = element.querySelectorAll("li.checklist--item").length;
const checked = element.querySelectorAll(
"li.checklist--item.checked"
).length;
return { checked, total };
}
rendered: false
},
title: {
default: null,
@@ -235,6 +222,26 @@ export const TaskListNode = TaskList.extend({
// the task list.
new Plugin({
key: new PluginKey("task-list-state-management"),
view(view) {
const { tr } = view.state;
tr.doc.descendants((node, pos) => {
if (node.type.name === TaskList.name) {
tr.setNodeMarkup(pos, undefined, {
...node.attrs,
stats: countCheckedItems(node)
});
return false;
}
});
tr.setMeta("preventUpdate", true);
tr.setMeta("addToHistory", false);
try {
view.dispatch(tr);
} catch (e) {
// ignore
}
return {};
},
appendTransaction(transactions, oldState, newState) {
if (!transactions[0].docChanged) return;

View File

@@ -34,6 +34,7 @@ import {
useToolbarStore
} from "./stores/toolbar-store.js";
import { ToolbarDefinition } from "./types.js";
import { ScrollContainer } from "@notesnook/ui";
type ToolbarProps = FlexProps & {
editor: Editor;
@@ -89,34 +90,44 @@ export function Toolbar(props: ToolbarProps) {
return (
<>
<Flex
className={["editor-toolbar", className].join(" ")}
sx={{
flexWrap: isMobile ? "nowrap" : "wrap",
overflowX: isMobile ? "auto" : "hidden",
bg: "background",
borderRadius: isMobile ? "0px" : "default",
...sx
}}
{...flexProps}
>
{toolbarTools.map((tools) => {
return (
<ToolbarGroup
key={tools.join("")}
tools={tools}
editor={editor}
groupId={tools.join("")}
sx={{
borderRight: "1px solid var(--separator)",
":last-of-type": { borderRight: "none" },
alignItems: "center"
}}
/>
);
<ScrollContainer
className="tabsScroll"
suppressScrollY
style={{ flex: 1 }}
trackStyle={() => ({
backgroundColor: "transparent",
pointerEvents: "none"
})}
</Flex>
<EditorFloatingMenus editor={editor} />
thumbStyle={() => ({ height: 3 })}
>
<Flex
className={["editor-toolbar", className].join(" ")}
sx={{
flexWrap: isMobile ? "nowrap" : "wrap",
bg: "background",
borderRadius: isMobile ? "0px" : "default",
...sx
}}
{...flexProps}
>
{toolbarTools.map((tools) => {
return (
<ToolbarGroup
key={tools.join("")}
tools={tools}
editor={editor}
groupId={tools.join("")}
sx={{
borderRight: "1px solid var(--separator)",
":last-of-type": { borderRight: "none" },
alignItems: "center"
}}
/>
);
})}
</Flex>
<EditorFloatingMenus editor={editor} />
</ScrollContainer>
</>
);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -663,7 +663,7 @@ $headline$: Use starting line of the note as title.`,
reloadEditor: () => t`Taking too long? Reload editor`,
copy: () => t`Copy`,
resendCode: (seconds?: number) =>
t`Resend code${seconds ? ` in ${seconds}` : ""}`,
t`Resend code${seconds ? `in ${seconds}` : ""}`,
change2faMethod: () => t`Change 2FA method`,
copyCodes: () => t`Copy codes`,
saveToFile: () => t`Save to file`,
@@ -678,12 +678,6 @@ $headline$: Use starting line of the note as title.`,
monthShort: () => t`mo`,
yearShort: () => t`yr`,
subscriptionProviderInfo: {
0: {
type: "Streetwriters",
title: () => t`Subscription awarded from Streetwriters`,
desc: () =>
t`You were awarded a subscription to Notesnook Pro by Streetwriters.`
},
1: {
type: "iOS",
title: () => t`Subscribed on iOS`,
@@ -703,11 +697,6 @@ $headline$: Use starting line of the note as title.`,
title: () => t`Subscribed on Web`,
desc: () => t`You subscribed to Notesnook Pro on the Web/Desktop App.`,
icon: "web"
},
4: {
type: "Gift card",
title: () => t`Subscribed using gift card`,
desc: () => t`You subscribed to Notesnook Pro using a gift card.`
}
},
dark: () => t`Dark`,
@@ -1712,12 +1701,11 @@ For example:
]().toLocaleLowerCase()} in case you lose access to your ${strings[
primary
]().toLocaleLowerCase()}.`,
transactionStatusToText: (
key: keyof typeof TRANSACTION_STATUS | ({} & string)
) => {
return key in TRANSACTION_STATUS
? TRANSACTION_STATUS[key as keyof typeof TRANSACTION_STATUS]()
: key;
transactionStatusToText: {
completed: () => t`Completed`,
refunded: () => t`"Refunded`,
partially_refunded: () => t`Partially refunded`,
disputed: () => t`Disputed`
},
viewReceipt: () => t`View receipt`,
customDictWords: (count: number) =>
@@ -2401,9 +2389,5 @@ Use this if changes from other devices are not appearing on this device. This wi
one: `Move all notes in this notebook to trash`,
other: `Move all notes in these notebooks to trash`
}),
emailCopied: () => t`Email copied`,
redeemGiftCode: () => t`Redeem gift code`,
redeemGiftCodeDesc: () => t`Enter the gift code to redeem your subscription.`,
redeemingGiftCode: () => t`Redeeming gift code`,
redeem: () => t`Redeem`
emailCopied: () => t`Email copied`
};