Compare commits

...

45 Commits

Author SHA1 Message Date
01zulfi
b8fca5cd98 Merge pull request #9765 from streetwriters/release/3.3.16
web: bump version to 3.3.16
2026-04-28 15:48:34 +05:00
01zulfi
4d87155fdd web: bump version to 3.3.16
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-28 10:17:27 +05:00
01zulfi
ed38bb6299 editor: fix crash in backspace handler (#9740)
* editor: fix crash in backspace handler
* replace nextNode with .nodeAfter

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* editor: fix position of node when deleting nextNode

---------

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-04-28 09:08:31 +05:00
Abdullah Atta
7c9e3e79df web: small refactor to make the build pass (#9761) 2026-04-27 12:54:44 +05:00
Chloe Oletto
43b90070a9 core: Limit note subtitle/headline length. (#9732)
Signed-off by: Chloe Oletto <NeedsChloesure@riseup.net>
2026-04-23 22:27:33 +05:00
01zulfi
9bea4adcf9 web: show titlebar on auth pages (#9093)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-22 13:51:21 +05:00
Chloe Oletto
a5790de23c desktop: Stop urgent reminder causes segfault & app freeze on Linux. (#9737)
* desktop: Stop urgent reminder causes segfault & app freeze on Linux.

Signed-off by: Chloe Oletto <NeedsChloesure@riseup.net>

* desktop: Linux segfault
Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* Apply suggestion from @thecodrr

Signed-off-by: Abdullah Atta <thecodrr@protonmail.com>

---------

Signed-off-by: Abdullah Atta <thecodrr@protonmail.com>
Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
2026-04-22 11:56:47 +05:00
Abdullah Atta
eb4edeea8f web: fix crash on non-paddle related iframe event (#9747)
Stack trace:
```
undefined is not an object (evaluating 'w.data')
E@https://app.notesnook.com/assets/paddle-Z43isEce-h-M.js:23:45428
E@https://app.notesnook.com/assets/paddle-Z43isEce-h-M.js:23:45846
```
2026-04-22 09:46:29 +05:00
01zulfi
e3ccd504da ui: fix submenu cutting off when there are a lot of lazy items (#9739)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-22 09:11:28 +05:00
Ammar Ahmed
4627d7d5df Merge pull request #9736 from streetwriters/mobile/release-3.3.20
mobile: release 3.3.20
2026-04-22 08:46:43 +05:00
01zulfi
f216c1b65d web: fix scrollbar blocking date in compact note list item (#9719)
* web: fix scrollbar blocking date in compact note list item
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: adjust notes list scrollbar width, compact item padding, & group header padding
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-21 11:08:30 +05:00
Abdullah Atta
4692ba7183 desktop: use newer appimage bundler (#9744)
this should fix libfuse and libz errors
2026-04-21 10:55:52 +05:00
Ammar Ahmed
543825918c mobile: fix useEffect deps in use-feature-manager hook 2026-04-21 08:46:19 +05:00
Ammar Ahmed
9f995b861b mobile: fix toast position incorrect with keyboard opened 2026-04-21 08:45:45 +05:00
Ammar Ahmed
9963a6c655 mobile: delete vault action keeps loading when entering incorrect password 2026-04-21 08:45:29 +05:00
Ammar Ahmed
1696ec5c72 mobile: fix unable to switch between yearly/monthly plans 2026-04-21 08:14:38 +05:00
Ammar Ahmed
7c95faad21 mobile: fix clear trash option is disabled on login 2026-04-20 15:43:39 +05:00
Ammar Ahmed
ca80441c14 mobile: fix trash count does not update on calling clear trash 2026-04-20 15:30:26 +05:00
01zulfi
9c497fd2bb Merge pull request #9724 from streetwriters/release/3.3.15
web: bump version to 3.3.15
2026-04-20 12:21:46 +05:00
Ammar Ahmed
ef5e5e5b4c mobile: release 3.3.20 2026-04-20 12:03:12 +05:00
Abdullah Atta
ede900fdbd ci: add linux arm64 preview builds (#9735) 2026-04-20 11:56:10 +05:00
01zulfi
1758f03f93 intl: regen locale files (#9734)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-20 11:39:13 +05:00
01zulfi
f18db748e0 web: bump version to 3.3.15
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-20 10:38:18 +05:00
01zulfi
2fba2af126 web: fix pdf viewer's search dialog unreadable UI in dark mode (#9720)
* web: fix pdf viewer's search dialog unreadable UI in dark mode
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: fix pdf search dialog ui

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-04-18 10:46:09 +05:00
Ammar Ahmed
1ef3d712e1 mobile: fix parent notebook can be moved into it's child notebooks. (#9691)
* mobile: fix parent notebook can be moved into it's child notebooks.

1. Ensure notebook that is being moved and all it's children are not visible in move-notebook dialog
2. Ensure child cannot be moved into it's parent again
3. Ensure child can be moved into it's sibling notebooks

* mobile: add e2e tests

* mobile: fix toast position
2026-04-18 09:28:39 +05:00
Ammar Ahmed
405adff8f4 mobile: fix linting not working in vscode and fix lint errors (#9710) 2026-04-18 08:52:39 +05:00
Ammar Ahmed
9a21ca1cf4 mobile: fix discount value shows as undefined on iOS when buying plan (#9711)
* mobile: fix discount value is undefined

* mobile: fix discount calculation and price formatting

Respect spacing when writing price in a currency.
Fix incorrect discount calculation when comparing products of a plan based on int values, use parseFloat to parse correct values for comparision

* mobile: fix ui inconsistencies

* mobile: conditionally include trial plan conditions based on GitHub release status
2026-04-18 08:40:50 +05:00
01zulfi
49252e66ce web: fix RCE via stored XSS in note html export (#9709)
* sandbox the export iframe
* escape fields when building html template

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-17 18:10:06 +05:00
Abdullah Atta
4ba2e900c5 desktop: disable autoInstallOnAppQuit (#9726) 2026-04-17 17:07:21 +05:00
01zulfi
b6714b559e web: make dates text reactive to date format setting (#9707)
the following dates text are now reactive:
* dates in note list item
* date in notebook header
* dates in properties panel
* date in reminder list item

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-16 12:42:10 +05:00
Ammar Ahmed
a6c1757cf6 mobile: fix list-items do not show correct notes count in side menu (#9697)
* mobile: fix list-items do not show correct notes count in side menu

* mobile: fix notebook note count not updated on deleting note

* mobile: fix monograph count does not update on unpublishing note in side-menu

* mobile: fix notebook and tag shortcut count does not update in sidemenu
2026-04-15 21:43:45 +05:00
01zulfi
3d847ea00c web: fix time field not aligning in reminder dialog (#9202)
* also capitalize month string

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-15 12:49:22 +05:00
Abdullah Atta
63453942ee Merge pull request #9686 from streetwriters/web/clear-trash
web: add clear trash option in trash nav item context menu
2026-04-15 12:48:50 +05:00
Ammar Ahmed
cb5a7904d3 mobile: add clear trash option in Trash properties
mobile: show clear trash success message

mobile: add e2e tests

mobile: fix clear trash option remains disabled after moving item to trash after login

mobile: Disable "Move to trash" for deleted notes.
2026-04-15 12:48:24 +05:00
01zulfi
f2c088fb18 web: add clear trash option in trash nav item context menu
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

web: disable clear trash if empty && update confirm message
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-15 12:48:10 +05:00
01zulfi
4e2fd9dde6 web: show error text on settings number input's out of range (#9171)
* web: show error text on settings number input's out of range
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: minor refactor and fixes

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-04-15 08:04:58 +05:00
Ammar Ahmed
92c1ca7b6f mobile: fix configure toolbar do not render properly on adding/removing groups (#9699)
* editor: make editor tools object a function

Required for string localization to work properly

* mobile: fix tools and groups do not re-render properly on changing preset or adding tools

* mobile: fix empty toast message shows on removing tools from group
2026-04-14 22:46:41 +05:00
01zulfi
5fcad35cdc Merge pull request #9712 from streetwriters/release/3.3.14
web: bump version to 3.3.14
2026-04-14 15:14:39 +05:00
01zulfi
2b11fcea41 web: bump version to 3.3.14
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-14 13:14:49 +05:00
Chloe Oletto
3f0c24d27b core: duplicating a note should copy all the flags (#9250)
Signed-off by: Chloe Oletto <NeedsChloesure@riseup.net>
2026-04-14 08:23:50 +05:00
01zulfi
71088fca99 editor: handle crash on inserting incorrect syntax in math nodes (#9690)
* editor: handle crash on inserting incorrect syntax in math nodes
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* editor: fix 'math error' text shown on newly empty math node
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-13 20:35:57 +05:00
01zulfi
d6f7f0c0be editor: fix crash when app is unlocked after a relock (#9698)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-13 20:35:16 +05:00
01zulfi
4044b68701 webclipper: fix notebooks getting unselected on tag select (#9706)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-04-13 14:55:20 +05:00
Abdullah Atta
610ced00af global: whitelist postinstall packages on bootstrap (#9629)
* global: enable npm ignore scripts by default

* config: disable ignore-scripts in .npmrc

* mobile: update lockfiles

---------

Co-authored-by: Ammar Ahmed <ammarahmed6506@gmail.com>
2026-04-13 14:27:32 +05:00
Abdullah Atta
f9fecda1f1 ci: enable CSC_FOR_PULL_REQUEST for desktop preview builds (#9695) 2026-04-10 14:45:24 +05:00
154 changed files with 3673 additions and 6528 deletions

View File

@@ -101,12 +101,12 @@ jobs:
- name: Build dmg
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CSC_LINK: ${{ secrets.mac_certs }}
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
APPLE_API_KEY: ~/private_keys/AuthKey_${{ secrets.api_key_id }}.p8
APPLE_API_KEY_ID: ${{ secrets.api_key_id }}
APPLE_API_ISSUER: ${{ secrets.api_key_issuer_id }}
CSC_FOR_PULL_REQUEST: true
run: |
npm run tx @notesnook/desktop:release
cd apps/desktop
@@ -152,8 +152,6 @@ jobs:
run: npm run tx @notesnook/desktop:release
- name: Build AppImage
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: yarn electron-builder --config=electron-builder.config.js --linux AppImage:x64 -p never
working-directory: ./apps/desktop
@@ -164,6 +162,49 @@ jobs:
name: linux-x64-build
path: apps/desktop/output/*.AppImage
build-linux-arm64:
name: Build for Linux arm64
needs: build
runs-on: ubuntu-22.04-arm
outputs:
linux-arm64-artifact-url: ${{ steps.artifact-upload-step.outputs.artifact-url }}
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Build Electron wrapper
run: npm run tx @notesnook/desktop:release
- name: Build AppImage
run: yarn electron-builder --config=electron-builder.config.js --linux AppImage:arm64 -p never
working-directory: ./apps/desktop
- name: Upload AppImage artifact
id: artifact-upload-step
uses: actions/upload-artifact@v4
with:
name: linux-arm64-build
path: apps/desktop/output/*.AppImage
build-windows:
name: Build for Windows
needs: build
@@ -215,7 +256,8 @@ jobs:
post-pr-comment:
name: Post PR comment with preview URLs
needs: [build, build-macos, build-linux-x64, build-windows]
needs:
[build, build-macos, build-linux-x64, build-linux-arm64, build-windows]
runs-on: ubuntu-latest
steps:
- name: Post or update PR comment
@@ -223,6 +265,7 @@ jobs:
env:
macos_artifact_url: ${{ needs.build-macos.outputs.macos-artifact-url }}
linux_x64_artifact_url: ${{ needs.build-linux-x64.outputs.linux-x64-artifact-url }}
linux_arm64_artifact_url: ${{ needs.build-linux-arm64.outputs.linux-arm64-artifact-url }}
windows_artifact_url: ${{ needs.build-windows.outputs.windows-artifact-url }}
with:
script: |
@@ -231,6 +274,7 @@ jobs:
const previewUrl = [
{ platform: 'macOS', url: process.env.macos_artifact_url },
{ platform: 'Linux x64', url: process.env.linux_x64_artifact_url },
{ platform: 'Linux arm64', url: process.env.linux_arm64_artifact_url },
{ platform: 'Windows x64', url: process.env.windows_artifact_url },
].filter(u => u.url).map(u => `- [${u.platform}](${u.url})`).join('\n');
const body = `${marker}\n**Desktop Previews**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.GITHUB_SHA}\n`;

2
.npmrc Normal file
View File

@@ -0,0 +1,2 @@
min-release-age=7 # days
# ignore-scripts=true

View File

@@ -198,6 +198,9 @@ module.exports = {
}
}
},
toolsets: {
appimage: "1.0.2"
},
snap: {
autoStart: false,
confinement: "strict",

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.3.13",
"version": "3.3.16",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/cjs/index.js",
@@ -48,7 +48,7 @@
"@types/yargs": "^17.0.33",
"chokidar": "^4.0.3",
"electron": "^37.0.0",
"electron-builder": "^26.0.12",
"electron-builder": "^26.8.1",
"esbuild": "0.21.5",
"node-abi": "^4.5.0",
"node-gyp-build": "^4.8.4",

View File

@@ -27,6 +27,7 @@ import crypto from "crypto";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const root = path.join(__dirname, "..");
const RUNNING_PROCESSES = [];
const RESTARTABLE_PROCESSES = [];
let lastBundleHash = null;
@@ -36,7 +37,7 @@ const ENV = {
FORCE_COLOR: "false",
COLOR: "0"
};
process.chdir(path.join(__dirname, ".."));
process.chdir(root);
await onChange(true);
@@ -55,13 +56,16 @@ async function onChange(first) {
if (first) {
await fs.rm("./build/", { force: true, recursive: true });
await exec("npm rebuild electron --verbose --foreground-scripts");
await exec(
"npm run postinstall --verbose",
path.join(root, "node_modules", "electron")
);
await exec("yarn electron-builder install-app-deps");
await exec("yarn electron-builder install-app-deps", root);
}
await exec(`yarn run bundle`);
await exec(`yarn run build`);
await exec(`yarn run bundle`, root);
await exec(`yarn run build`, root);
if (await isBundleSame()) {
console.log("Bundle is same. Doing nothing.");
@@ -111,7 +115,7 @@ function spawnAndWaitUntil(cmd, cwd, predicate) {
async function exec(cmd, cwd) {
try {
console.log(">", cmd);
console.log(">", cmd, cwd);
return execSync(cmd, {
env: ENV,

View File

@@ -178,7 +178,9 @@ export const osIntegrationRouter = t.router({
})
});
notification.show();
if (input.urgency === "critical") {
if (input.urgency === "critical" && process.platform !== "linux") {
// due to an Electron bug in versions below 40.x, shell.beep() causes a segfault on Linux.
// TODO: Remove when migrating to Electron 40.x or newer.
shell.beep();
}

View File

@@ -37,7 +37,11 @@ async function configureAutoUpdater() {
config.releaseTrack === "stable" &&
autoUpdater.currentVersion.prerelease.length > 0;
autoUpdater.allowPrerelease = false;
autoUpdater.autoInstallOnAppQuit = true;
// Do NOT auto-install on quit. On Windows, if the system shuts down while
// the NSIS installer is running, it first removes all old files and then
// gets killed before copying new ones — leaving an empty install directory.
// Updates should only be installed when the user explicitly triggers it.
autoUpdater.autoInstallOnAppQuit = false;
autoUpdater.disableWebInstaller = true;
}

View File

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

View File

@@ -1,8 +1,3 @@
- Fix clearing note title adds generated title instead of staying empty
- Improved table cell selection, table scrolling, and table cell editing on mobile
- Fixed app on launch randomly focuses editor instead of notes list
- Fixed file size check shows error but continues adding the file to editor
- Note title headline format should get content from first paragraph
- Minor bug fixes and performance improvements
- Bug fixes and improvements
Thank you for using Notesnook!
Thank you for using Notesnook!

View File

@@ -23,7 +23,7 @@ import {
THEME_COMPATIBILITY_VERSION,
useThemeEngineStore
} from "@notesnook/theme";
import React, { useEffect } from "react";
import React, { PropsWithChildren, useEffect } from "react";
import { Appearance, I18nManager, Linking, StatusBar } from "react-native";
import "react-native-gesture-handler";
import { GestureHandlerRootView } from "react-native-gesture-handler";
@@ -106,8 +106,10 @@ let currTheme =
: SettingsService.getProperty("lighTheme");
useThemeEngineStore.getState().setTheme(currTheme);
export const withTheme = (Element: (props: any) => JSX.Element) => {
return function AppWithThemeProvider(props: any) {
export const withTheme = (
Element: (props: PropsWithChildren) => JSX.Element
) => {
return function AppWithThemeProvider(props: PropsWithChildren) {
const [colorScheme, darkTheme, lightTheme] = useThemeStore((state) => [
state.colorScheme,
state.darkTheme,

View File

@@ -25,7 +25,6 @@ import * as Keychain from "react-native-keychain";
import { MMKVLoader, ProcessingModes } from "react-native-mmkv-storage";
import { generateSecureRandom } from "react-native-securerandom";
import { DatabaseLogger } from ".";
import { ToastManager } from "../../services/event-manager";
import { MMKV } from "./mmkv";
// Database key cipher is persisted across different user sessions hence it has
@@ -205,9 +204,8 @@ export async function getDatabaseKey(appLockPassword?: string) {
}
if (await Keychain.hasInternetCredentials("notesnook")) {
const userKeyCredentials = await Keychain.getInternetCredentials(
"notesnook"
);
const userKeyCredentials =
await Keychain.getInternetCredentials("notesnook");
if (userKeyCredentials) {
const userKeyCipher: Cipher = (await encrypt(

View File

@@ -72,7 +72,7 @@ export async function setupDatabase(password?: string) {
({
compress: Gzip.deflate,
decompress: Gzip.inflate
} as ICompressor),
}) as ICompressor,
batchSize: 50,
sqliteOptions: {
dialect: (name) => ({

View File

@@ -109,8 +109,8 @@ class RNSqliteConnection implements DatabaseConnection {
query.kind === "SelectQueryNode"
? "query"
: query.kind === "RawNode"
? "raw"
: "exec";
? "raw"
: "exec";
const result = await this.db.executeAsync(sql, parameters as any[]);

View File

@@ -112,8 +112,8 @@ export async function writeEncryptedBase64(
async function deleteLocalFile(filename: string) {
try {
await createCacheDir();
let path = cacheDir + `/${filename}`;
let exists = await RNFetchBlob.fs.exists(path);
const path = cacheDir + `/${filename}`;
const exists = await RNFetchBlob.fs.exists(path);
if (Platform.OS === "ios" && !exists) {
const iosAppGroup =
Platform.OS === "ios"
@@ -309,7 +309,9 @@ export async function deleteDCacheFiles() {
});
}
}
} catch (e) {}
} catch (e) {
/** Empty */
}
}
export async function getCachePathForFile(filename: string) {

View File

@@ -36,7 +36,7 @@ export const Announcement = () => {
state.announcements,
state.remove
]);
let announcement = announcements.length > 0 ? announcements[0] : null;
const announcement = announcements.length > 0 ? announcements[0] : null;
const selectionMode = useSelectionStore((state) => state.selectionMode);
return !announcement || selectionMode ? null : (

View File

@@ -32,7 +32,7 @@ import { Action } from "../../stores/use-message-store";
export const Cta = (props: BodyItemProps) => {
const { colors } = useThemeColors();
let buttons =
const buttons =
props.item.actions.filter((item) => allowedOnPlatform(item.platforms)) ||
[];

View File

@@ -53,7 +53,6 @@ import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import { strings } from "@notesnook/intl";
import { AppFontSize } from "../../utils/size";
import { editorController } from "../../screens/editor/tiptap/utils";
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
@@ -239,8 +238,8 @@ const AppLocked = () => {
deviceMode !== "mobile"
? "50%"
: Platform.OS == "ios"
? "95%"
: "100%",
? "95%"
: "100%",
paddingHorizontal: 12,
marginBottom: 30,
marginTop: 15,

View File

@@ -46,7 +46,6 @@ import {
eOnLoadNote
} from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { sleep } from "../../utils/time";
import { Dialog } from "../dialog";
import { presentDialog } from "../dialog/functions";
import { openNote } from "../list-items/note/wrapper";

View File

@@ -17,7 +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 { LegendList } from "@legendapp/list";
import {
Attachment,
FilteredSelector,

View File

@@ -53,7 +53,7 @@ export const ForgotPassword = ({ userEmail }: { userEmail: string }) => {
}
setLoading(true);
try {
let lastRecoveryEmailTime = SettingsService.get().lastRecoveryEmailTime;
const lastRecoveryEmailTime = SettingsService.get().lastRecoveryEmailTime;
if (
lastRecoveryEmailTime &&
Date.now() - lastRecoveryEmailTime < 60000 * 3

View File

@@ -22,14 +22,9 @@ import { useThemeColors } from "@notesnook/theme";
import { RouteProp, useRoute } from "@react-navigation/native";
import React, { useEffect, useState } from "react";
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import { DDS } from "../../services/device-detection";
import {
eSendEvent,
presentSheet,
ToastManager
} from "../../services/event-manager";
import { eSendEvent, presentSheet } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import PremiumService from "../../services/premium";
import SettingsService from "../../services/settings";
@@ -195,8 +190,8 @@ export const Login = ({
? "50%"
: "49.99%"
: focused
? "100%"
: "99.9%",
? "100%"
: "99.9%",
backgroundColor: colors.primary.background,
alignSelf: "center",
paddingHorizontal: DDS.isTab ? 0 : DefaultAppStyles.GAP,

View File

@@ -92,7 +92,7 @@ export const SessionExpired = () => {
const open = React.useCallback(async () => {
try {
let res = await db.tokenManager.getToken();
const res = await db.tokenManager.getToken();
if (!res) throw new Error("no token found");
if (db.tokenManager._isTokenExpired(res))
throw new Error("token expired");
@@ -102,7 +102,7 @@ export const SessionExpired = () => {
Sync.run("global", false, "full", async (complete) => {
if (!complete) {
let user = await db.user.getUser();
const user = await db.user.getUser();
if (!user) return;
email.current = user.email;
setVisible(true);
@@ -115,7 +115,7 @@ export const SessionExpired = () => {
setVisible(false);
});
} catch (e) {
let user = await db.user.getUser();
const user = await db.user.getUser();
if (!user) return;
email.current = user.email;
setFocused(false);

View File

@@ -99,7 +99,7 @@ export const Signup = ({
try {
setCurrentStep(SignupSteps.createAccount);
await db.user.signup(email.current!.toLowerCase(), password.current!);
let user = await db.user.getUser();
const user = await db.user.getUser();
setUser(user);
setLastSynced(await db.lastSynced());
clearMessage();

View File

@@ -1,3 +1,22 @@
/*
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 React from "react";
import { useThemeColors } from "@notesnook/theme";
import { useRef } from "react";
import { View } from "react-native";

View File

@@ -23,7 +23,6 @@ import {
ColorValue,
KeyboardAvoidingView,
Modal,
Platform,
SafeAreaView,
StyleSheet,
TouchableOpacity,
@@ -136,8 +135,8 @@ const BaseDialog = ({
backgroundColor: background
? background
: transparent
? "transparent"
: "rgba(0,0,0,0.3)"
? "transparent"
: "rgba(0,0,0,0.3)"
}}
>
<KeyboardAvoidingView
@@ -155,8 +154,8 @@ const BaseDialog = ({
justifyContent: centered
? "center"
: bottom
? "flex-end"
: "flex-start"
? "flex-end"
: "flex-start"
}
]}
>

View File

@@ -22,7 +22,6 @@ import { Text, View, ViewStyle } from "react-native";
import { useThemeColors } from "@notesnook/theme";
import { AppFontSize } from "../../utils/size";
import { Button, ButtonProps } from "../ui/button";
import { PressableProps } from "../ui/pressable";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { DefaultAppStyles } from "../../utils/styles";

View File

@@ -110,61 +110,6 @@ export const VaultDialog: React.FC = () => {
const confirmPasswordRef = useRef<string | null>(null);
const newPasswordRef = useRef<string | null>(null);
const open = useCallback(async (data: Vault) => {
const biometry = await BiometricService.isBiometryAvailable();
const available = !!biometry;
const fingerprint = await BiometricService.hasInternetCredentials();
if (data.item) {
const locked = await db.vaults.itemExists(data.item);
noteLockedRef.current = locked;
if (!locked) {
const content = await db.content.findByNoteId(data.item!.id);
if (content && isEncryptedContent(content)) {
noteLockedRef.current = true;
}
}
}
// Set refs
noteRef.current = data.item;
titleRef.current = data.title || strings.goToEditor();
descriptionRef.current = data.description || null;
paragraphRef.current = data.paragraph || null;
buttonTitleRef.current = data.buttonTitle || null;
positiveButtonTypeRef.current = data.positiveButtonType || "transparent";
customActionTitleRef.current = data.customActionTitle || null;
customActionParagraphRef.current = data.customActionParagraph || null;
onUnlockRef.current = data.onUnlock;
requestTypeRef.current = data.requestType;
// Set UI state
setIsBiometryAvailable(available);
setIsBiometryEnrolled(fingerprint);
setBiometricUnlock(fingerprint);
setWrongPassword(false);
setPasswordsDontMatch(false);
setDeleteAll(false);
setLoading(false);
// Auto-unlock with fingerprint if applicable
const canAutoUnlock =
fingerprint &&
data.requestType !== VaultRequestType.EnableFingerprint &&
data.requestType !== VaultRequestType.RevokeFingerprint &&
data.requestType !== VaultRequestType.ChangePassword &&
data.requestType !== VaultRequestType.ClearVault &&
data.requestType !== VaultRequestType.DeleteVault &&
data.requestType !== VaultRequestType.CustomAction &&
data.requestType !== VaultRequestType.PermanentUnlock;
if (canAutoUnlock) {
await onPressFingerprintAuth(data.title, data.description);
} else {
setVisible(true);
}
}, []);
const close = useCallback(() => {
if (loading) {
ToastManager.show({
@@ -249,6 +194,7 @@ export const VaultDialog: React.FC = () => {
close();
}, 100);
} else {
setLoading(false);
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
@@ -282,6 +228,10 @@ export const VaultDialog: React.FC = () => {
setLoading(false);
close();
eSendEvent("vaultUpdated");
ToastManager.show({
message: strings.vaultCleared(),
type: "success"
});
} catch (e) {
ToastManager.show({
heading: strings.passwordIncorrect(),
@@ -292,6 +242,45 @@ export const VaultDialog: React.FC = () => {
setLoading(false);
}, [close]);
const enrollFingerprint = useCallback(
async (password: string) => {
setLoading(true);
try {
await db.vault.unlock(password);
await BiometricService.storeCredentials(password);
setLoading(false);
eSendEvent("vaultUpdated");
ToastManager.show({
heading: strings.biometricUnlockEnabled(),
type: "success",
context: "global"
});
close();
} catch (e) {
close();
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
setLoading(false);
}
},
[close]
);
const takeErrorAction = useCallback(() => {
setWrongPassword(true);
setVisible(true);
setTimeout(() => {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
}, 500);
}, []);
const lockNote = useCallback(async () => {
if (!passwordRef.current || passwordRef.current.trim() === "") {
ToastManager.show({
@@ -333,7 +322,13 @@ export const VaultDialog: React.FC = () => {
.catch((e) => {
takeErrorAction();
});
}, [close, biometricUnlock, isBiometryEnrolled]);
}, [
biometricUnlock,
isBiometryEnrolled,
close,
enrollFingerprint,
takeErrorAction
]);
const openInEditor = useCallback(
(note: Note & { content?: NoteContent<false> }) => {
@@ -387,19 +382,7 @@ export const VaultDialog: React.FC = () => {
} catch (e) {
takeErrorAction();
}
}, [close]);
const takeErrorAction = useCallback(() => {
setWrongPassword(true);
setVisible(true);
setTimeout(() => {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
}, 500);
}, []);
}, [close, takeErrorAction]);
const openNote = useCallback(async () => {
try {
@@ -440,6 +423,7 @@ export const VaultDialog: React.FC = () => {
}, [
biometricUnlock,
isBiometryEnrolled,
enrollFingerprint,
openInEditor,
shareNote,
deleteNote,
@@ -464,33 +448,6 @@ export const VaultDialog: React.FC = () => {
}
}, [permanantUnlock, openNote]);
const enrollFingerprint = useCallback(
async (password: string) => {
setLoading(true);
try {
await db.vault.unlock(password);
await BiometricService.storeCredentials(password);
setLoading(false);
eSendEvent("vaultUpdated");
ToastManager.show({
heading: strings.biometricUnlockEnabled(),
type: "success",
context: "global"
});
close();
} catch (e) {
close();
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
setLoading(false);
}
},
[close]
);
const createVault = useCallback(async () => {
await db.vault.create(passwordRef.current || "");
@@ -536,31 +493,6 @@ export const VaultDialog: React.FC = () => {
}
}, []);
const onPressFingerprintAuth = useCallback(
async (title?: string, description?: string) => {
try {
const credentials = await BiometricService.getCredentials(
title || titleRef.current,
description || descriptionRef.current || ""
);
if (!credentials) throw new Error("Failed to get user credentials");
if (credentials?.password) {
passwordRef.current = credentials.password;
onPress();
} else {
eSendEvent(eCloseActionSheet);
await sleep(300);
setVisible(true);
}
} catch (e) {
console.error(e);
}
},
[]
);
const onPress = useCallback(async () => {
const requestType = requestTypeRef.current;
@@ -689,6 +621,89 @@ export const VaultDialog: React.FC = () => {
deleteVault
]);
const onPressFingerprintAuth = useCallback(
async (title?: string, description?: string) => {
try {
const credentials = await BiometricService.getCredentials(
title || titleRef.current,
description || descriptionRef.current || ""
);
if (!credentials) throw new Error("Failed to get user credentials");
if (credentials?.password) {
passwordRef.current = credentials.password;
onPress();
} else {
eSendEvent(eCloseActionSheet);
await sleep(300);
setVisible(true);
}
} catch (e) {
console.error(e);
}
},
[onPress]
);
const open = useCallback(
async (data: Vault) => {
const biometry = await BiometricService.isBiometryAvailable();
const available = !!biometry;
const fingerprint = await BiometricService.hasInternetCredentials();
if (data.item) {
const locked = await db.vaults.itemExists(data.item);
noteLockedRef.current = locked;
if (!locked) {
const content = await db.content.findByNoteId(data.item!.id);
if (content && isEncryptedContent(content)) {
noteLockedRef.current = true;
}
}
}
// Set refs
noteRef.current = data.item;
titleRef.current = data.title || strings.goToEditor();
descriptionRef.current = data.description || null;
paragraphRef.current = data.paragraph || null;
buttonTitleRef.current = data.buttonTitle || null;
positiveButtonTypeRef.current = data.positiveButtonType || "transparent";
customActionTitleRef.current = data.customActionTitle || null;
customActionParagraphRef.current = data.customActionParagraph || null;
onUnlockRef.current = data.onUnlock;
requestTypeRef.current = data.requestType;
// Set UI state
setIsBiometryAvailable(available);
setIsBiometryEnrolled(fingerprint);
setBiometricUnlock(fingerprint);
setWrongPassword(false);
setPasswordsDontMatch(false);
setDeleteAll(false);
setLoading(false);
// Auto-unlock with fingerprint if applicable
const canAutoUnlock =
fingerprint &&
data.requestType !== VaultRequestType.EnableFingerprint &&
data.requestType !== VaultRequestType.RevokeFingerprint &&
data.requestType !== VaultRequestType.ChangePassword &&
data.requestType !== VaultRequestType.ClearVault &&
data.requestType !== VaultRequestType.DeleteVault &&
data.requestType !== VaultRequestType.CustomAction &&
data.requestType !== VaultRequestType.PermanentUnlock;
if (canAutoUnlock) {
await onPressFingerprintAuth(data.title, data.description);
} else {
setVisible(true);
}
},
[onPressFingerprintAuth]
);
useEffect(() => {
eSubscribeEvent(eOpenVaultDialog, open);
eSubscribeEvent(eCloseVaultDialog, close);
@@ -731,7 +746,8 @@ export const VaultDialog: React.FC = () => {
width: DDS.isTab ? 350 : "85%",
borderRadius: 10,
backgroundColor: colors.primary.background,
paddingTop: 12
paddingTop: 12,
overflow: "hidden"
}}
>
<DialogHeader
@@ -798,7 +814,8 @@ export const VaultDialog: React.FC = () => {
!isBiometryAvailable ||
isCreateVault ||
isChangePassword ||
isCustomAction ? null : (
isCustomAction ||
isDeleteVault ? null : (
<Button
onPress={() =>
onPressFingerprintAuth(strings.unlockNote(), "")

View File

@@ -22,7 +22,6 @@ import React, {
RefObject,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState
} from "react";
@@ -39,7 +38,6 @@ import Animated, {
WithSpringConfig,
withTiming
} from "react-native-reanimated";
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
import { eSendEvent } from "../../services/event-manager";
import { useSettingStore } from "../../stores/use-setting-store";
import { eClearEditor } from "../../utils/events";

View File

@@ -77,10 +77,10 @@ export default function List(props: ListProps) {
props.renderedInRoute === "Notes"
? "home"
: props.renderedInRoute === "Favorites"
? "favorites"
: props.renderedInRoute === "Trash" || props.dataType === "trash"
? "trash"
: `${props.dataType}s`;
? "favorites"
: props.renderedInRoute === "Trash" || props.dataType === "trash"
? "trash"
: `${props.dataType}s`;
const groupOptions = useGroupOptions(groupType);
@@ -94,7 +94,7 @@ export default function List(props: ListProps) {
(item: number | boolean, index: number) => {
return props.data?.type(index);
},
[]
[props.data]
);
const renderItem = React.useCallback(

View File

@@ -30,7 +30,7 @@ import { fluidTabsRef } from "../../utils/global-refs";
import { AppFontSize } from "../../utils/size";
import { useSideBarDraggingStore } from "../side-menu/dragging-store";
import { IconButton } from "../ui/icon-button";
import { isFeatureAvailable, useIsFeatureAvailable } from "@notesnook/common";
import { useIsFeatureAvailable } from "@notesnook/common";
import PaywallSheet from "../sheets/paywall";
import { strings } from "@notesnook/intl";
import { ToastManager } from "../../services/event-manager";
@@ -142,7 +142,7 @@ function ReorderableList<T extends { id: string }>({
items.push(...data.filter((i) => !itemOrderState.includes(i.id)));
return items;
}, [data, customizableSidebarFeature?.isAllowed]);
}, [customizableSidebarFeature?.isAllowed, data, itemOrderState]);
return (
<View style={styles.container}>

View File

@@ -20,7 +20,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { getFormattedDate } from "@notesnook/common";
import {
EncryptedContentItem,
isEncryptedContent,
Note,
UnencryptedContentItem
} from "@notesnook/core";
@@ -75,9 +74,9 @@ const MergeConflicts = () => {
const { height } = useSettingStore((state) => state.dimensions);
const applyChanges = async () => {
let contentToSave = selectedContent;
const contentToSave = selectedContent;
if (!contentToSave) return;
let note = await db.notes.note(contentToSave.noteId);
const note = await db.notes.note(contentToSave.noteId);
if (!note) return;
await db.notes.add({
id: note.id,
@@ -462,7 +461,8 @@ const MergeConflicts = () => {
<ReadonlyEditor
editorId="conflictSecondary"
onLoad={async (loadContent) => {
const note = await db.notes.note(content.current?.noteId!);
if (!content.current?.noteId) return;
const note = await db.notes.note(content.current?.noteId);
if (!note) return;
loadContent({
id: note.id,

View File

@@ -63,22 +63,25 @@ const HistoryItem = ({
}${_end_time}`;
};
const preview = useCallback(async (item: HistorySession) => {
const content = await db.noteHistory.content(item.id);
presentSheet({
component: (
<NotePreview
session={{
...item,
session: getDate(item.dateCreated, item.dateModified)
}}
content={content}
note={note}
/>
),
context: "note_history"
});
}, []);
const preview = useCallback(
async (item: HistorySession) => {
const content = await db.noteHistory.content(item.id);
presentSheet({
component: (
<NotePreview
session={{
...item,
session: getDate(item.dateCreated, item.dateModified)
}}
content={content}
note={note}
/>
),
context: "note_history"
});
},
[note]
);
return (
<Pressable
@@ -138,7 +141,7 @@ export default function NoteHistory({
({ index }: { index: number }) => (
<HistoryItem index={index} items={history} note={note} />
),
[history]
[history, note]
);
return (

View File

@@ -42,7 +42,6 @@ import {
isEncryptedContent,
Note,
NoteContent,
SessionContentItem,
TrashOrItem
} from "@notesnook/core";

View File

@@ -18,14 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { getFeaturesTable } from "@notesnook/common";
import {
EV,
EVENTS,
Plan,
SKUResponse,
SubscriptionPlan,
User
} from "@notesnook/core";
import { EVENTS, Plan, SubscriptionPlan, User } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
@@ -55,6 +48,7 @@ import {
TECHLORE_SVG,
XDA_SVG
} from "../../assets/images/assets";
import { db } from "../../common/database";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import usePricingPlans, {
PlanOverView,
@@ -76,7 +70,6 @@ import { IconButton } from "../ui/icon-button";
import { SvgView } from "../ui/svg";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { db } from "../../common/database";
const Steps = {
select: 1,
@@ -119,7 +112,7 @@ const PayWall = (props: NavigationProps<"PayWall">) => {
}
setStep(Steps.buy);
}
}, [routeParams.state]);
}, [pricingPlans, routeParams.state]);
useEffect(() => {
let listener: NativeEventSubscription;
@@ -137,7 +130,7 @@ const PayWall = (props: NavigationProps<"PayWall">) => {
return () => {
listener?.remove();
};
}, [isFocused, step]);
}, [isFocused, routeParams.context, step]);
useEffect(() => {
const sub = db.eventManager.subscribe(
@@ -154,7 +147,7 @@ const PayWall = (props: NavigationProps<"PayWall">) => {
return () => {
sub?.unsubscribe();
};
}, []);
}, [routeParams.context]);
const is5YearPlanSelected = (
isGithubRelease
@@ -936,7 +929,10 @@ const PricingPlanCard = ({
setStep: (step: number) => void;
}) => {
const { colors } = useThemeColors();
const [regionalDiscount, setRegionaDiscount] = useState<SKUResponse>();
const regionalDiscount =
annualBilling && plan.id === "pro"
? pricingPlans?.regionalDiscount
: undefined;
const { width } = useWindowDimensions();
const isTablet = width > 600;
@@ -959,26 +955,6 @@ const PricingPlanCard = ({
annualBilling
);
useEffect(() => {
if (pricingPlans?.isGithubRelease || !annualBilling) return;
pricingPlans
?.getRegionalDiscount(
plan.id,
pricingPlans.isGithubRelease
? (WebPlan?.period as string)
: `notesnook.${plan.id}.${annualBilling ? "yearly" : "monthly"}`
)
.then((value) => {
setRegionaDiscount(value);
});
}, [annualBilling]);
useEffect(() => {
if (!annualBilling) {
setRegionaDiscount(undefined);
}
}, [annualBilling]);
const isSubscribed =
product?.productId &&
pricingPlans?.user?.subscription?.productId?.includes(plan.id) &&
@@ -1008,8 +984,8 @@ const PricingPlanCard = ({
: "monthly"
}`
: pricingPlans.isGithubRelease
? (WebPlan?.period as string)
: (product?.productId as string)
? (WebPlan?.period as string)
: (product?.productId as string)
);
setStep(Steps.buy);
}}

View File

@@ -27,7 +27,11 @@ import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../e2e/test.ids";
import { db } from "../../common/database";
import { eSendEvent, ToastManager } from "../../services/event-manager";
import {
eSendEvent,
sendItemUpdateEvent,
ToastManager
} from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { useMenuStore } from "../../stores/use-menu-store";
import { useRelationStore } from "../../stores/use-relation-store";
@@ -63,6 +67,7 @@ const ColorItem = ({ item, note }: { item: Color; note: Note }) => {
useRelationStore.getState().update();
setColorNotes();
Navigation.queueRoutesForUpdate();
sendItemUpdateEvent(item.id, "color");
eSendEvent(refreshNotesPage);
};
@@ -127,7 +132,7 @@ export const ColorTags = ({ item }: { item: Note }) => {
}
useSettingStore.getState().setSheetKeyboardHandler(false);
setVisible(true);
}, []);
}, [colorFeature]);
return (
<>
@@ -140,6 +145,7 @@ export const ColorTags = ({ item }: { item: Note }) => {
useRelationStore.getState().update();
useMenuStore.getState().setColorNotes();
Navigation.queueRoutesForUpdate();
sendItemUpdateEvent(color.id, "color");
eSendEvent(refreshNotesPage);
}}
/>

View File

@@ -35,7 +35,7 @@ export const DateMeta = ({ item }: { item: Item }) => {
const [dateCreated, setDateCreated] = useState(item.dateCreated);
function getDateMeta() {
let keys = Object.keys(item);
const keys = Object.keys(item);
if (keys.includes("dateEdited"))
keys.splice(
keys.findIndex((k) => k === "dateModified"),

View File

@@ -173,15 +173,15 @@ export const Items = ({
DDS.isTab
? AppFontSize.xxl
: shouldShrink
? AppFontSize.xxl
: AppFontSize.lg
? AppFontSize.xxl
: AppFontSize.lg
}
color={
item.checked
? item.activeColor || colors.primary.accent
: item.id.match(/(delete|trash)/g)
? colors.error.icon
: colors.secondary.icon
? colors.error.icon
: colors.secondary.icon
}
/>
</Pressable>
@@ -212,8 +212,8 @@ export const Items = ({
text: item.checked
? item.activeColor || colors.primary.accent
: item.id === "delete" || item.id === "trash"
? colors.error.paragraph
: colors.primary.paragraph
? colors.error.paragraph
: colors.primary.paragraph
}}
testID={"icon-" + item.id}
onPress={item.onPress}
@@ -277,8 +277,8 @@ export const Items = ({
item.checked
? item.activeColor || colors.primary.accent
: item.id === "delete" || item.id === "trash"
? colors.error.icon
: colors.secondary.icon
? colors.error.icon
: colors.secondary.icon
}
/>
@@ -318,7 +318,9 @@ export const Items = ({
[
colors.error.icon,
colors.primary.accent,
colors.primary.border,
colors.secondary.icon,
colors.static.orange,
columnItemWidth,
topBarSorting
]

View File

@@ -77,7 +77,7 @@ export const TagStrip = ({ item, close }) => {
.then((tags) => {
setTags(tags);
});
}, []);
}, [item]);
return tags?.length > 0 ? (
<View

View File

@@ -42,8 +42,6 @@ import { DefaultAppStyles } from "../../utils/styles";
import { sleep } from "../../utils/time";
import { presentDialog } from "../dialog/functions";
import ExportNotesSheet from "../sheets/export-notes";
import { MoveNotebook } from "../../screens/move-notebook";
import { IconButton } from "../ui/icon-button";
import NativeTooltip from "../../utils/tooltip";
import ManageTags from "../../screens/manage-tags";

View File

@@ -16,13 +16,14 @@ 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 { Plan, SKUResponse } from "@notesnook/core";
import { Plan } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import dayjs from "dayjs";
import React, { useEffect, useState } from "react";
import React, { useState } from "react";
import {
Linking,
Platform,
ScrollView,
Text,
TouchableOpacity,
@@ -31,7 +32,6 @@ import {
import Config from "react-native-config";
import * as RNIap from "react-native-iap";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { WebView } from "react-native-webview";
import { db } from "../../../common/database";
import usePricingPlans from "../../../hooks/use-pricing-plans";
import { ToastManager } from "../../../services/event-manager";
@@ -211,16 +211,19 @@ export const BuyPlan = (props: {
? strings["5yearPlanConditions"]()
: [
strings.trialPlanConditions[0](
billingDuration?.duration as number
billingDuration?.duration as number as never
),
strings.trialPlanConditions[1](0)
...(isGithubRelease
? []
: [strings.trialPlanConditions[1](Platform.OS as never)])
]
).map((item) => (
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: 10
gap: 10,
flex: 1
}}
key={item}
>
@@ -229,7 +232,13 @@ export const BuyPlan = (props: {
size={AppFontSize.lg}
name="check"
/>
<Paragraph>{item}</Paragraph>
<Paragraph
style={{
flexShrink: 1
}}
>
{item}
</Paragraph>
</View>
))}
</View>
@@ -243,8 +252,8 @@ export const BuyPlan = (props: {
is5YearPlanSelected
? strings.purchase()
: pricingPlans?.userCanRequestTrial
? strings.subscribeAndStartTrial()
: strings.subscribe()
? strings.subscribeAndStartTrial()
: strings.subscribe()
}
onPress={async () => {
if (isGithubRelease) {
@@ -321,7 +330,10 @@ const ProductItem = (props: {
productId: string;
}) => {
const { colors } = useThemeColors();
const [regionalDiscount, setRegionaDiscount] = useState<SKUResponse>();
const regionalDiscount =
props.productId === "notesnook.pro.yearly"
? props.pricingPlans.regionalDiscount
: undefined;
const product =
props.pricingPlans?.currentPlan?.subscriptions?.[
regionalDiscount?.sku || props.productId
@@ -358,27 +370,19 @@ const ProductItem = (props: {
props.pricingPlans.user?.subscription?.productId ===
(product as Plan)?.id);
useEffect(() => {
props.pricingPlans
?.getRegionalDiscount(
props.pricingPlans.currentPlan?.id as string,
props.pricingPlans.isGithubRelease
? ((product as Plan)?.period as string)
: props.productId
)
.then((value) => {
if (
value &&
value.sku?.startsWith(
(props.pricingPlans.selectedProduct as RNIap.Subscription)
?.productId
)
) {
props.pricingPlans.selectProduct(value?.sku as string);
}
setRegionaDiscount(value);
});
}, []);
const discountValue =
(isAnnual && !isGithubRelease) ||
(isGithubRelease && (product as Plan)?.discount?.amount)
? regionalDiscount
? regionalDiscount.discount
: isGithubRelease
? (product as Plan).discount?.amount
: props.pricingPlans.compareProductPrice(
props.pricingPlans.currentPlan?.id as string,
`notesnook.${props.pricingPlans.currentPlan?.id}.yearly`,
`notesnook.${props.pricingPlans.currentPlan?.id}.monthly`
)
: undefined;
return (
<TouchableOpacity
@@ -420,12 +424,11 @@ const ProductItem = (props: {
{isAnnual
? strings.yearly()
: is5YearProduct
? strings.fiveYearPlan()
: strings.monthly()}
? strings.fiveYearPlan()
: strings.monthly()}
</Heading>
{(isAnnual && !isGithubRelease) ||
(isGithubRelease && (product as Plan)?.discount?.amount) ? (
{discountValue ? (
<View
style={{
backgroundColor: colors.static.red,
@@ -436,18 +439,7 @@ const ProductItem = (props: {
}}
>
<Heading color={colors.static.white} size={AppFontSize.xs}>
{strings.bestValue()} -{" "}
{strings.percentOff(
(regionalDiscount
? regionalDiscount.discount
: isGithubRelease
? (product as Plan).discount?.amount
: props.pricingPlans.compareProductPrice(
props.pricingPlans.currentPlan?.id as string,
`notesnook.${props.pricingPlans.currentPlan?.id}.yearly`,
`notesnook.${props.pricingPlans.currentPlan?.id}.monthly`
)) as string
)}
{strings.bestValue()} - {strings.percentOff(`${discountValue}`)}
</Heading>
</View>
) : null}

View File

@@ -21,7 +21,6 @@ import React from "react";
import { View } from "react-native";
import FileViewer from "react-native-file-viewer";
import { ToastManager } from "../../../services/event-manager";
import { AppFontSize } from "../../../utils/size";
import { Button } from "../../ui/button";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../../utils/styles";

View File

@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useAreFeaturesAvailable } from "@notesnook/common";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import React, { useEffect } from "react";
import { View } from "react-native";
import {
eSendEvent,
@@ -28,7 +28,11 @@ import {
ToastManager
} from "../../../services/event-manager";
import SettingsService from "../../../services/settings";
import { eCloseSheet } from "../../../utils/events";
import {
eAfterSync,
eCloseSheet,
eMenuItemUpdate
} from "../../../utils/events";
import { SideMenuItem } from "../../../utils/menu-items";
import { AppFontSize } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
@@ -37,12 +41,25 @@ import AppIcon from "../../ui/AppIcon";
import { Pressable } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph";
import PaywallSheet from "../paywall";
import { presentDialog } from "../../dialog/functions";
import { db } from "../../../common/database";
import { useTrashStore } from "../../../stores/use-trash-store";
import { useSettingStore } from "../../../stores/use-setting-store";
export const MenuItemProperties = ({ item }: { item: SideMenuItem }) => {
const { colors } = useThemeColors();
const featuresAvailable = useAreFeaturesAvailable([
"customHomepage",
"customizableSidebar"
]);
const isAppLoading = useSettingStore((state) => state.isAppLoading);
const trash = useTrashStore((state) => state.items);
useEffect(() => {
if (!isAppLoading) {
useTrashStore.getState().refresh();
}
}, [isAppLoading]);
return !featuresAvailable ? null : (
<View
style={{
@@ -100,7 +117,38 @@ export const MenuItemProperties = ({ item }: { item: SideMenuItem }) => {
},
icon: "sort-ascending",
locked: !featuresAvailable?.customizableSidebar.isAllowed
}
},
...(item.id === "Trash"
? [
{
title: strings.clearTrash(),
onPress: async () => {
if (!trash || trash?.length === 0) return;
eSendEvent(eCloseSheet);
setTimeout(() => {
presentDialog({
title: strings.clearTrashConfirm(),
paragraph: strings.clearTrashDesc(),
positiveText: strings.clear(),
positivePress: async () => {
await db.trash.clear();
useTrashStore.getState().clear();
eSendEvent(eMenuItemUpdate);
eSendEvent(eAfterSync);
ToastManager.show({
message: strings.trashCleared(),
type: "success"
});
return true;
}
});
}, 500);
},
icon: "delete-sweep-outline",
disabled: !trash || trash?.length === 0
}
]
: [])
].map((item) => (
<Pressable
key={item.title}
@@ -112,7 +160,7 @@ export const MenuItemProperties = ({ item }: { item: SideMenuItem }) => {
gap: DefaultAppStyles.GAP_SMALL,
borderRadius: 0,
paddingHorizontal: DefaultAppStyles.GAP,
opacity: item.locked ? 0.6 : 1
opacity: item.disabled || item.locked ? 0.6 : 1
}}
onPress={() => {
item.onPress();

View File

@@ -17,7 +17,7 @@ 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 { Notebook, VirtualizedGrouping } from "@notesnook/core";
import { Notebook } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";

View File

@@ -1,3 +1,22 @@
/*
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 React from "react";
import { FeatureId, FeatureResult } from "@notesnook/common";
import { SubscriptionPlan, SubscriptionProvider } from "@notesnook/core";
import { strings } from "@notesnook/intl";

View File

@@ -1,3 +1,22 @@
/*
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 React from "react";
import {
FeatureUsage,
formatBytes,
@@ -76,10 +95,10 @@ export function PlanLimits() {
{item.total === Infinity
? strings.unlimited()
: item.id === "storage"
? `${formatBytes(item.used)}/${formatBytes(
item.total
)} ${strings.used()}`
: `${item.used}/${item.total} ${strings.used()}`}
? `${formatBytes(item.used)}/${formatBytes(
item.total
)} ${strings.used()}`
: `${item.used}/${item.total} ${strings.used()}`}
</Paragraph>
</View>
</View>

View File

@@ -17,7 +17,7 @@ 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 { hosts, Monograph, Note } from "@notesnook/core";
import { hosts, Note } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import Clipboard from "@react-native-clipboard/clipboard";
@@ -28,12 +28,15 @@ import {
TouchableOpacity,
View
} from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
//@ts-ignore
import ToggleSwitch from "toggle-switch-react-native";
import { db } from "../../../common/database";
import { requestInAppReview } from "../../../services/app-review";
import { presentSheet, ToastManager } from "../../../services/event-manager";
import {
eSendEvent,
presentSheet,
ToastManager
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { useAttachmentStore } from "../../../stores/use-attachment-store";
import { openLinkInBrowser } from "../../../utils/functions";
@@ -46,7 +49,8 @@ import Input from "../../ui/input";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { useAsync } from "react-async-hook";
import { isFeatureAvailable, useIsFeatureAvailable } from "@notesnook/common";
import { eMenuItemUpdate } from "../../../utils/events";
import { useIsFeatureAvailable } from "@notesnook/common";
async function fetchMonographData(noteId: string) {
const monographId = db.monographs.monograph(noteId);
@@ -55,7 +59,7 @@ async function fetchMonographData(noteId: string) {
: undefined;
return {
monograph,
monographId,
monographId
};
}
@@ -112,6 +116,7 @@ const PublishNoteSheet = ({
await monographData.execute();
Navigation.queueRoutesForUpdate();
eSendEvent(eMenuItemUpdate);
setPublishLoading(false);
}
requestInAppReview();
@@ -138,6 +143,7 @@ const PublishNoteSheet = ({
await db.monographs.unpublish(note.id);
monographData.execute();
Navigation.queueRoutesForUpdate();
eSendEvent(eMenuItemUpdate);
setPublishLoading(false);
}
} catch (e) {

View File

@@ -79,6 +79,7 @@ type SimpleTabViewProps = {
const createSceneMap = (
scenes: Record<string, React.ComponentType<any>>
): ((props: { route: SimpleRoute }) => React.ReactNode) => {
// eslint-disable-next-line react/display-name
return ({ route }: { route: SimpleRoute }) => {
const SceneComponent = scenes[route.key];
if (!SceneComponent) return null;
@@ -136,8 +137,8 @@ const SimpleTabView = ({
{loadedKeysRef.current.has(route.key)
? getSceneForRoute(route)
: navigationState.index === routeIndex
? getSceneForRoute(route)
: null}
? getSceneForRoute(route)
: null}
</View>
))}
</View>
@@ -432,12 +433,12 @@ const TabBar = (props: SimpleTabBarProps) => {
name="plus"
testID="sidebar-add-button"
size={AppFontSize.lg - 2}
top={10}
color={colors.primary.icon}
onPress={async () => {
if (props.navigationState.index === 1) {
const notebooksFeature = await isFeatureAvailable(
"notebooks"
);
const notebooksFeature =
await isFeatureAvailable("notebooks");
if (!notebooksFeature.isAllowed) {
PaywallSheet.present(notebooksFeature);
return;
@@ -485,6 +486,7 @@ const TabBar = (props: SimpleTabBarProps) => {
? "sort-ascending"
: "sort-descending"
}
top={10}
testID="sidebar-sort-button"
color={colors.primary.icon}
onPress={() => {
@@ -520,6 +522,7 @@ const TabBar = (props: SimpleTabBarProps) => {
width: 28,
height: 28
}}
top={10}
testID="sidebar-theme-button"
color={colors.primary.icon}
name={isDark ? "weather-night" : "weather-sunny"}

View File

@@ -24,18 +24,22 @@ import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { useTotalNotes } from "../../hooks/use-db-item";
import { db } from "../../common/database";
import { eSubscribeEvent } from "../../services/event-manager";
import {
eSubscribeEvent,
subscribeToItemUpdate
} from "../../services/event-manager";
import Navigation from "../../services/navigation";
import useNavigationStore, {
RouteParams
} from "../../stores/use-navigation-store";
import { eAfterSync } from "../../utils/events";
import { eAfterSync, eMenuItemUpdate } from "../../utils/events";
import { SideMenuItem } from "../../utils/menu-items";
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { Pressable } from "../ui/pressable";
import Paragraph from "../ui/typography/paragraph";
import { useSideBarDraggingStore } from "./dragging-store";
import { useRelationStore } from "../../stores/use-relation-store";
export function MenuItem({
item,
@@ -56,13 +60,19 @@ export function MenuItem({
const totalNotes = useTotalNotes(
item.dataType as "notebook" | "tag" | "color"
);
const update = useRelationStore((state) => state.updater);
const getTotalNotesRef = useRef(totalNotes.getTotalNotes);
getTotalNotesRef.current = totalNotes.getTotalNotes;
const menuItemCount = !item.data
? itemCount
: totalNotes.totalNotes(item.data.id);
useEffect(() => {
if (item.data) {
getTotalNotesRef.current([item.data?.id]);
}
}, [item.data, update]);
useEffect(() => {
const onSyncComplete = async () => {
try {
@@ -84,7 +94,9 @@ export function MenuItem({
setItemCount(await db.notes.archived.count());
break;
case "Trash":
setItemCount((await db.trash.all()).length);
{
setItemCount((await db.trash.all()).length);
}
break;
}
} else {
@@ -94,10 +106,20 @@ export function MenuItem({
/** Empty */
}
};
const event = eSubscribeEvent(eAfterSync, onSyncComplete);
const events = [eSubscribeEvent(eAfterSync, onSyncComplete)];
if (!item.data) {
events.push(eSubscribeEvent(eMenuItemUpdate, onSyncComplete));
}
if (item.data?.id) {
events.push(
subscribeToItemUpdate(item?.data?.id, item?.data?.type, onSyncComplete)
);
}
onSyncComplete();
return () => {
event?.unsubscribe();
events?.forEach((e) => e?.unsubscribe());
};
}, [item.data, item.id]);
@@ -151,8 +173,8 @@ export function MenuItem({
item.icon === "crown"
? colors.static.yellow
: isFocused
? colors.selected.icon
: colors.secondary.icon
? colors.selected.icon
: colors.secondary.icon
}
size={AppFontSize.md}
/>

View File

@@ -24,7 +24,8 @@ import { StoreApi, UseBoundStore } from "zustand";
import { useTotalNotes } from "../../hooks/use-db-item";
import {
eSubscribeEvent,
eUnSubscribeEvent
eUnSubscribeEvent,
ToastManager
} from "../../services/event-manager";
import { TreeItem } from "../../stores/create-notebook-tree-stores";
import { SelectionStore } from "../../stores/item-selection-store";
@@ -35,6 +36,7 @@ import AppIcon from "../ui/AppIcon";
import { IconButton } from "../ui/icon-button";
import { Pressable } from "../ui/pressable";
import Paragraph from "../ui/typography/paragraph";
import { useRelationStore } from "../../stores/use-relation-store";
export const NotebookItem = ({
index,
@@ -70,13 +72,14 @@ export const NotebookItem = ({
const notebook = item.notebook;
const isFocused = focused;
const { totalNotes, getTotalNotes } = useTotalNotes("notebook");
const updater = useRelationStore(state => state.updater);
const getTotalNotesRef = React.useRef(getTotalNotes);
getTotalNotesRef.current = getTotalNotes;
const { colors } = useThemeColors();
useEffect(() => {
getTotalNotesRef.current([item.notebook.id]);
}, [item.notebook]);
}, [item.notebook, updater]);
useEffect(() => {
const onNotebookUpdate = (id?: string) => {
@@ -101,7 +104,8 @@ export const NotebookItem = ({
? 15 * item.depth
: 15 * 5,
width: "100%",
marginTop: 2
marginTop: 2,
opacity: item.disabled ? 0.5 : 1
}}
>
<Pressable

View File

@@ -76,7 +76,7 @@ export const PinnedSection = React.memo(
onPress: onPress,
onLongPress: onLongPress
})) as SideMenuItem[],
[menuPins, onPress]
[menuPins, onLongPress, onPress]
);
const renderItem = React.useCallback(({ item }: { item: SideMenuItem }) => {

View File

@@ -40,6 +40,7 @@ import {
useSideMenuNotebookTreeStore
} from "./stores";
import { LegendList } from "@legendapp/list";
import { useRelationStore } from "../../stores/use-relation-store";
useSideMenuNotebookSelectionStore.setState({
multiSelect: true
});
@@ -52,6 +53,7 @@ export const SideMenuNotebooks = () => {
const [filteredNotebooks, setFilteredNotebooks] = React.useState(notebooks);
const searchTimer = React.useRef<NodeJS.Timeout>(undefined);
const lastQuery = React.useRef<string>(undefined);
const updater = useRelationStore(state => state.updater);
const loadRootNotebooks = React.useCallback(async () => {
if (!filteredNotebooks) return;
const _notebooks: Notebook[] = [];
@@ -66,9 +68,6 @@ export const SideMenuNotebooks = () => {
const updateNotebooks = React.useCallback(() => {
if (lastQuery.current) {
// useSideMenuNotebookTreeStore.setState({
// isSearching: true
// });
db.lookup
.notebooks(lastQuery.current)
.sorted(db.settings.getGroupOptions("notebooks"))
@@ -76,16 +75,13 @@ export const SideMenuNotebooks = () => {
setFilteredNotebooks(filtered);
});
} else {
// useSideMenuNotebookTreeStore.setState({
// isSearching: false
// });
setFilteredNotebooks(notebooks);
}
}, [notebooks]);
useEffect(() => {
updateNotebooks();
}, [updateNotebooks]);
}, [updateNotebooks,updater]);
useEffect(() => {
(async () => {

View File

@@ -38,6 +38,7 @@ import { SideMenuHeader } from "./side-menu-header";
import { SideMenuListEmpty } from "./side-menu-list-empty";
import { useSideMenuTagsSelectionStore } from "./stores";
import { LegendList, LegendListRenderItemProps } from "@legendapp/list";
import { useRelationStore } from "../../stores/use-relation-store";
const TagItem = (props: {
tags: VirtualizedGrouping<Tag>;
@@ -55,12 +56,13 @@ const TagItem = (props: {
const totalNotes = useTotalNotes("tag");
const totalNotesRef = React.useRef(totalNotes);
totalNotesRef.current = totalNotes;
const updater = useRelationStore(state => state.updater);
useEffect(() => {
if (item?.id) {
totalNotesRef.current?.getTotalNotes([item?.id]);
}
}, [item]);
}, [item, updater]);
return (
<View

View File

@@ -38,10 +38,10 @@ import {
import { getElevationStyle } from "../../utils/elevation";
import { eHideToast, eShowToast } from "../../utils/events";
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { DefaultAppStyles } from "../../utils/styles";
export const Toast = ({ context = "global" }) => {
const { colors, isDark } = useThemeColors();
@@ -119,11 +119,7 @@ export const Toast = ({ context = "global" }) => {
width: DDS.isTab ? dimensions.width / 2 : "100%",
alignItems: "center",
alignSelf: "center",
bottom:
Platform.OS === "android"
? Math.max(insets.bottom, 40)
: Math.max(insets.bottom, 40) +
(keyboard.keyboardShown ? keyboard.keyboardHeight : 0),
bottom: insets.bottom + 15,
position: "absolute",
zIndex: 999,
elevation: 15
@@ -158,22 +154,22 @@ export const Toast = ({ context = "global" }) => {
toastOptions.icon
? toastOptions.icon
: toastOptions.type === "success"
? "check"
: toastOptions.type === "info"
? "information"
: "close"
? "check"
: toastOptions.type === "info"
? "information"
: "close"
}
size={isFullToastMessage ? AppFontSize.xxxl : AppFontSize.xl}
color={
toastOptions?.icon
? toastOptions?.icon
: toastOptions.type === "error"
? colors.error.icon
: toastOptions.type === "info"
? isDark
? colors.static.white
: colors.static.black
: colors.success.icon
? colors.error.icon
: toastOptions.type === "info"
? isDark
? colors.static.white
: colors.static.black
: colors.success.icon
}
/>

View File

@@ -83,12 +83,13 @@ const SheetWrapper = ({
: 0
};
}, [
colors.primary.background,
colors.primary.border,
largeTablet,
smallTablet,
width,
insets.bottom
colors.primary.background,
colors.primary.border,
bottomInsets,
isGestureNavigationEnabled
]);
const _onOpen = () => {

View File

@@ -17,33 +17,14 @@ 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 { SubscriptionPlan } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { Linking, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import {
COMMUNITY_SVG,
LAUNCH_ROCKET,
SUPPORT_SVG,
WELCOME_SVG
} from "../../assets/images/assets";
import useRotator from "../../hooks/use-rotator";
import { eSendEvent } from "../../services/event-manager";
import { getContainerBorder } from "../../utils/colors";
import { getElevationStyle } from "../../utils/elevation";
import { eOpenAddNotebookDialog } from "../../utils/events";
import { defaultBorderRadius, AppFontSize } from "../../utils/size";
import { Button } from "../ui/button";
import Seperator from "../ui/seperator";
import { SvgView } from "../ui/svg";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { DefaultAppStyles } from "../../utils/styles";
import { WELCOME_SVG } from "../../assets/images/assets";
import { useUserStore } from "../../stores/use-user-store";
import { planToId, SubscriptionPlan } from "@notesnook/core";
import { planToDisplayName } from "../../utils/constants";
import AppIcon from "../ui/AppIcon";
import { SvgView } from "../ui/svg";
export type TStep = {
text?: string;

View File

@@ -33,7 +33,7 @@ import { useThemeColors } from "@notesnook/theme";
import { DisplayedNotification } from "@notifee/react-native";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { useEffect, useRef, useState } from "react";
import { InteractionManager, Platform, View } from "react-native";
import { InteractionManager, Platform } from "react-native";
import Share from "react-native-share";
import { DatabaseLogger, db } from "../common/database";
import { AttachmentDialog } from "../components/attachments";
@@ -72,8 +72,8 @@ import { useUserStore } from "../stores/use-user-store";
import { eCloseSheet, eUpdateNoteInEditor } from "../utils/events";
import { deleteItems } from "../utils/functions";
import { convertNoteToText } from "../utils/note-to-text";
import { sleep } from "../utils/time";
import { NotesnookModule } from "../utils/notesnook-module";
import { sleep } from "../utils/time";
import DatePickerComponent from "../components/date-picker";
@@ -196,7 +196,7 @@ export const useActions = ({
"expiringNotes"
]);
const [item, setItem] = useState(propItem);
const { colors, isDark } = useThemeColors();
const { colors } = useThemeColors();
const setMenuPins = useMenuStore((state) => state.setMenuPins);
const [isPinnedToMenu, setIsPinnedToMenu] = useState(
db.shortcuts.exists(item.id)
@@ -382,6 +382,16 @@ export const useActions = ({
}
const deleteItem = async () => {
if (isPublished) {
ToastManager.show({
heading: strings.notePublished(),
message: strings.unpublishToDelete(),
type: "error",
context: "local"
});
return;
}
close();
await sleep(300);
@@ -1237,7 +1247,8 @@ export const useActions = ({
: strings.moveToTrash(),
icon: "delete-outline",
type: "error",
onPress: deleteItem
onPress: deleteItem,
locked: isPublished
});
}
@@ -1275,7 +1286,10 @@ export const useActions = ({
(item as Note).headline || (item as Notebook).description || "",
(item as Color).colorCode
);
} catch (e) {}
} catch (e) {
/**
empty */
}
}
});
}

View File

@@ -352,7 +352,10 @@ async function checkForShareExtensionLaunchedInBackground() {
if (note) setTimeout(() => eSendEvent("loadingNote", note), 1);
MMKV.removeItem("shareExtensionOpened");
}
} catch (e) {}
} catch (e) {
/**
empty */
}
}
const onSuccessfulSubscription = async (

View File

@@ -135,7 +135,7 @@ export const useNoteLocked = (noteId: string | undefined) => {
//@ts-ignore
!globalThis["IS_SHARE_EXTENSION"]
) {
let unsub = useSettingStore.subscribe((state) => {
const unsub = useSettingStore.subscribe((state) => {
if (!state.isAppLoading) {
unsub();
db.vaults

View File

@@ -1,15 +1,28 @@
/*
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 { useAreFeaturesAvailable } from "@notesnook/common";
import { strings } from "@notesnook/intl";
import { useEffect } from "react";
import { db } from "../common/database";
import { presentDialog } from "../components/dialog/functions";
import { useDragState } from "../screens/settings/editor/state";
import { eSendEvent } from "../services/event-manager";
import Navigation from "../services/navigation";
import Notifications from "../services/notifications";
import SettingsService from "../services/settings";
import { useUserStore } from "../stores/use-user-store";
import { eCloseSimpleDialog } from "../utils/events";
export default function useFeatureManager() {
const features = useAreFeaturesAvailable([
@@ -24,11 +37,10 @@ export default function useFeatureManager() {
"disableTrashCleanup",
"fullOfflineMode"
]);
const user = useUserStore((state) => state.user);
const plan = useUserStore((state) => state.user?.subscription?.plan);
useEffect(() => {
if (!user || !features) return;
if (!useUserStore.getState().user || !features) return;
if (!features.createNoteFromNotificationDrawer.isAllowed) {
SettingsService.setProperty("notifNotes", false);
@@ -69,6 +81,7 @@ export default function useFeatureManager() {
db.settings.setDefaultTag(undefined);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [features, plan]);
return true;

View File

@@ -16,16 +16,17 @@ 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 { Plan, SubscriptionPlan, SubscriptionPlanId } from "@notesnook/core";
import { useEffect, useState } from "react";
import { Plan, SubscriptionPlan } from "@notesnook/core";
import React, { useEffect, useState } from "react";
import { useAsync } from "react-async-hook";
import { Platform } from "react-native";
import Config from "react-native-config";
import * as RNIap from "react-native-iap";
import { DatabaseLogger, db } from "../common/database";
import PremiumService from "../services/premium";
import SettingsService from "../services/settings";
import { useSettingStore } from "../stores/use-setting-store";
import { useUserStore } from "../stores/use-user-store";
import SettingsService from "../services/settings";
function numberWithCommas(x: string) {
const parts = x.toString().split(".");
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
@@ -155,9 +156,18 @@ const planIdToIndex = (planId: string) => {
return planIndex;
};
let WebPlanCache: Plan[];
const isGithubRelease = Config.GITHUB_RELEASE === "true";
const usePricingPlans = (options?: PricingPlansOptions) => {
const isGithubRelease = Config.GITHUB_RELEASE === "true";
const user = useUserStore((state) => state.user);
const regionalDiscount = useAsync(
() =>
db.pricing.sku(
Platform.OS === "android" ? "google" : "apple",
"yearly",
"pro"
),
[]
);
const [currentPlan, setCurrentPlan] = useState<string>(
options?.planId || pricingPlans[2].id
);
@@ -172,17 +182,20 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
const [userCanRequestTrial, setUserCanRequestTrial] = useState(false);
const [webPricingPlans, setWebPricingPlans] = useState<Plan[]>([]);
const getProduct = (planId: string, skuId: string) => {
if (isGithubRelease)
return webPricingPlans.find(
(plan) => planIdToIndex(planId) === plan.plan && skuId === plan.period
);
const getProduct = React.useCallback(
(planId: string, skuId: string) => {
if (isGithubRelease)
return webPricingPlans.find(
(plan) => planIdToIndex(planId) === plan.plan && skuId === plan.period
);
return (
plans.find((p) => p.id === planId)?.subscriptions?.[skuId] ||
plans.find((p) => p.id === planId)?.products?.[skuId]
);
};
return (
plans.find((p) => p.id === planId)?.subscriptions?.[skuId] ||
plans.find((p) => p.id === planId)?.products?.[skuId]
);
},
[plans, webPricingPlans]
);
const getProductAndroid = (planId: string, skuId: string) => {
if (isGithubRelease)
@@ -196,37 +209,45 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
return getProduct(planId, skuId) as RNIap.SubscriptionIOS;
};
const hasTrialOffer = (planId?: string, productId?: string) => {
if (!selectedProductSku && !productId) return false;
const hasTrialOffer = React.useCallback(
(planId?: string, productId?: string) => {
if (!selectedProductSku && !productId) return false;
if (productId?.includes("5year")) return false;
if (isGithubRelease) {
if (
user?.subscription?.trialsAvailed?.some(
(plan) => plan === planIdToIndex(planId || currentPlan)
)
) {
return false;
} else {
return true;
if (productId?.includes("5year")) return false;
if (isGithubRelease) {
if (
user?.subscription?.trialsAvailed?.some(
(plan) => plan === planIdToIndex(planId || currentPlan)
)
) {
return false;
} else {
return true;
}
}
}
return Platform.OS === "ios"
? (
getProduct(
planId || currentPlan,
productId || selectedProductSku
) as RNIap.SubscriptionIOS
)?.introductoryPricePaymentModeIOS === "FREETRIAL"
: (
getProduct(
planId || currentPlan,
productId || selectedProductSku
) as RNIap.SubscriptionAndroid
)?.subscriptionOfferDetails?.[0]?.pricingPhases?.pricingPhaseList
?.length > 1;
};
return Platform.OS === "ios"
? (
getProduct(
planId || currentPlan,
productId || selectedProductSku
) as RNIap.SubscriptionIOS
)?.introductoryPricePaymentModeIOS === "FREETRIAL"
: (
getProduct(
planId || currentPlan,
productId || selectedProductSku
) as RNIap.SubscriptionAndroid
)?.subscriptionOfferDetails?.[0]?.pricingPhases?.pricingPhaseList
?.length > 1;
},
[
currentPlan,
getProduct,
selectedProductSku,
user?.subscription?.trialsAvailed
]
);
// user && (!user.subscription || !user.subscription.expiry) ? true : false;
@@ -255,12 +276,15 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
const products = WebPlanCache || (await db.pricing.products());
WebPlanCache = products;
setWebPricingPlans(products);
} catch (e) {}
} catch (e) {
/**
empty */
}
}
setLoadingPlans(false);
};
loadPlans();
}, [options?.promoOffer, cancelPromo]);
}, [options?.promoOffer, cancelPromo, hasTrialOffer]);
function getLocalizedPrice(
product: RNIap.Subscription | RNIap.Product | Plan
@@ -276,8 +300,8 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
(product as Plan).period === "yearly"
? (product as Plan).price.gross
: (product as Plan).period === "5-year"
? (product as Plan).price.gross
: (product as Plan).price.gross
? (product as Plan).price.gross
: (product as Plan).price.gross
}`;
}
@@ -432,8 +456,8 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
return period.endsWith("W")
? "week"
: period.endsWith("M")
? "month"
: "year";
? "month"
: "year";
} else {
const unit = (product as RNIap.SubscriptionIOS)
?.subscriptionPeriodUnitIOS;
@@ -475,8 +499,8 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
type: phase.billingPeriod.endsWith("W")
? "week"
: phase.billingPeriod.endsWith("M")
? "month"
: "year"
? "month"
: "year"
};
} else {
const productIos = product as RNIap.SubscriptionIOS;
@@ -530,14 +554,15 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
const formattedPrice = numberWithCommas(monthlyPrice.toFixed(2));
return isAtLeft
? `${symbol} ${formattedPrice}`
: `${formattedPrice} ${symbol}`;
? `${symbol}${formattedPrice}`
: `${formattedPrice}${symbol}`;
};
const getDiscountValue = (p1: string, p2: string, splitToMonth?: boolean) => {
let price1 = Platform.OS === "ios" ? parseInt(p1) : parseInt(p1) / 1000000;
let price1 =
Platform.OS === "ios" ? parseFloat(p1) : parseFloat(p1) / 1000000;
const price2 =
Platform.OS === "ios" ? parseInt(p2) : parseInt(p2) / 1000000;
Platform.OS === "ios" ? parseFloat(p2) : parseFloat(p2) / 1000000;
price1 = splitToMonth ? price1 / 12 : price1;
@@ -587,7 +612,7 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
} else {
priceValue = price / 1000000;
}
const priceSymbol = localizedPrice.replace(/[\s\d,.]+/, "");
const priceSymbol = localizedPrice.replace(/[\d,.]+/, "");
return { priceValue, priceSymbol, localizedPrice };
};
@@ -607,8 +632,8 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
(product as Plan).period === "yearly"
? ((product as Plan).price.gross / 12).toFixed(2)
: (product as Plan).period === "5-year"
? ((product as Plan).price.gross / (12 * 5)).toFixed(2)
: (product as Plan).price.gross
? ((product as Plan).price.gross / (12 * 5)).toFixed(2)
: (product as Plan).price.gross
}`;
}
@@ -645,21 +670,6 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
);
};
async function getRegionalDiscount(plan: string, productId: string) {
if (productId !== "notesnook.pro.yearly") {
return;
}
try {
return await db.pricing.sku(
Platform.OS === "android" ? "google" : "apple",
"yearly",
plan as SubscriptionPlanId
);
} catch (e) {
console.log(e);
}
}
function isSubscribedToPlan(planId: string) {
if (!PremiumService.get()) return false;
return user?.subscription?.productId?.includes(planId);
@@ -730,7 +740,7 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
(plan) => plan.plan === planIndex && plan.period === period
);
},
getRegionalDiscount,
regionalDiscount: regionalDiscount.result,
isGithubRelease: isGithubRelease,
isSubscribed: () => user?.subscription?.plan !== SubscriptionPlan.FREE,
finish: () => options?.onBuy?.()

View File

@@ -139,7 +139,7 @@ export const FluidPanelsView = React.memo(
if (deviceMode === "smallTablet") {
fluidTabsRef.current?.openDrawer(false);
}
}, [deviceMode, dimensions.width, setFullscreen]);
}, [deviceMode, setFullscreen]);
const closeFullScreenEditor = useCallback(
(current: string) => {
@@ -158,7 +158,7 @@ export const FluidPanelsView = React.memo(
fluidTabsRef.current?.goToIndex(2, false);
}
},
[deviceMode, dimensions.width, setFullscreen]
[deviceMode, setFullscreen]
);
const toggleView = useCallback(
@@ -215,7 +215,7 @@ export const FluidPanelsView = React.memo(
}
}, 0);
},
[deviceMode, fullscreen, setDeviceModeState]
[fullscreen, setDeviceModeState]
);
const checkDeviceType = React.useCallback(
@@ -229,14 +229,14 @@ export const FluidPanelsView = React.memo(
: "mobile";
setDeviceMode(nextDeviceMode, size);
},
[orientation, setDeviceMode, setDimensions]
[orientation, setDeviceMode]
);
useEffect(() => {
if (orientation !== "UNKNOWN") {
checkDeviceType(dimensions);
}
}, [orientation, dimensions]);
}, [orientation, dimensions, checkDeviceType]);
const _onLayout = React.useCallback(
(event: LayoutChangeEvent) => {
@@ -251,13 +251,7 @@ export const FluidPanelsView = React.memo(
setOrientation(OrientationType["PORTRAIT"]);
}
},
[
checkDeviceType,
deviceMode,
dimensions.width,
orientation,
setDeviceMode
]
[setDimensions]
);
const PANE_OFFSET = useMemo(

View File

@@ -16,7 +16,6 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";

View File

@@ -400,17 +400,45 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
recurringMode === RecurringModes.Year
? null
: recurringMode === RecurringModes.Week
? (weekFormat === "Mon" ? WeekDaysMon : WeekDays).map(
(item) => (
? (weekFormat === "Mon" ? WeekDaysMon : WeekDays).map(
(item) => (
<Button
key={strings.weekDayNamesShort[
item as keyof typeof strings.weekDayNamesShort
]()}
title={strings.weekDayNamesShort[
item as keyof typeof strings.weekDayNamesShort
]()}
type={
selectedDays.indexOf(item) > -1
? "selected"
: "plain"
}
fontSize={AppFontSize.xs}
style={{
height: 40,
borderRadius: 100,
marginRight: 10
}}
onPress={() => {
setSelectedDays((days) => {
if (days.indexOf(item) > -1) {
days.splice(days.indexOf(item), 1);
return [...days];
}
days.push(item);
return [...days];
});
}}
/>
)
)
: MonthDays.map((item, index) => (
<Button
key={strings.weekDayNamesShort[
item as keyof typeof strings.weekDayNamesShort
]()}
title={strings.weekDayNamesShort[
item as keyof typeof strings.weekDayNamesShort
]()}
key={index + "monthday"}
title={index + 1 + ""}
type={
selectedDays.indexOf(item) > -1
selectedDays.indexOf(index + 1) > -1
? "selected"
: "plain"
}
@@ -422,44 +450,16 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
}}
onPress={() => {
setSelectedDays((days) => {
if (days.indexOf(item) > -1) {
days.splice(days.indexOf(item), 1);
if (days.indexOf(index + 1) > -1) {
days.splice(days.indexOf(index + 1), 1);
return [...days];
}
days.push(item);
days.push(index + 1);
return [...days];
});
}}
/>
)
)
: MonthDays.map((item, index) => (
<Button
key={index + "monthday"}
title={index + 1 + ""}
type={
selectedDays.indexOf(index + 1) > -1
? "selected"
: "plain"
}
fontSize={AppFontSize.xs}
style={{
height: 40,
borderRadius: 100,
marginRight: 10
}}
onPress={() => {
setSelectedDays((days) => {
if (days.indexOf(index + 1) > -1) {
days.splice(days.indexOf(index + 1), 1);
return [...days];
}
days.push(index + 1);
return [...days];
});
}}
/>
))}
))}
</ScrollView>
</View>
) : null}
@@ -542,24 +542,24 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
dayjs(date).format("hh:mm A")
)
: recurringMode === RecurringModes.Year
? strings.reminderRepeatStrings.year(
dayjs(date).format("dddd, MMMM D, h:mm A")
)
: selectedDays.length === 7 &&
recurringMode === RecurringModes.Week
? strings.reminderRepeatStrings.week.daily(
dayjs(date).format("hh:mm A")
)
: selectedDays.length === 0
? strings.reminderRepeatStrings[
recurringMode as "week" | "month"
].selectDays()
: strings.reminderRepeatStrings.repeats(
repeatFrequency,
recurringMode as string,
getSelectedDaysText(selectedDays),
dayjs(date).format("hh:mm A")
)}
? strings.reminderRepeatStrings.year(
dayjs(date).format("dddd, MMMM D, h:mm A")
)
: selectedDays.length === 7 &&
recurringMode === RecurringModes.Week
? strings.reminderRepeatStrings.week.daily(
dayjs(date).format("hh:mm A")
)
: selectedDays.length === 0
? strings.reminderRepeatStrings[
recurringMode as "week" | "month"
].selectDays()
: strings.reminderRepeatStrings.repeats(
repeatFrequency,
recurringMode as string,
getSelectedDaysText(selectedDays),
dayjs(date).format("hh:mm A")
)}
</Paragraph>
</>
</View>
@@ -587,8 +587,8 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
mode === "Silent"
? "minus-circle"
: mode === "Vibrate"
? "vibrate"
: "volume-high"
? "vibrate"
: "volume-high"
}
fontSize={AppFontSize.xs}
height={35}
@@ -636,6 +636,7 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
<Heading size={AppFontSize.md}>{strings.referencedIn()}</Heading>
{referencedNotes.value.map((item) => (
<Pressable
key={item.id}
style={{
justifyContent: "space-between",
flexDirection: "row",

View File

@@ -19,7 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import Sodium from "@ammarahmed/react-native-sodium";
import { isFeatureAvailable } from "@notesnook/common";
import { isImage } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import {
DocumentPickerOptions,
@@ -34,6 +33,7 @@ import { Image, openCamera, openPicker } from "react-native-image-crop-picker";
import { DatabaseLogger, db } from "../../../common/database";
import filesystem from "../../../common/filesystem";
import { compressToFile } from "../../../common/filesystem/compress";
import { santizeUri } from "../../../common/filesystem/utils";
import AttachImage from "../../../components/dialogs/attach-image-dialog";
import { ToastManager } from "../../../services/event-manager";
import PremiumService from "../../../services/premium";
@@ -41,7 +41,6 @@ import { useSettingStore } from "../../../stores/use-setting-store";
import { useUserStore } from "../../../stores/use-user-store";
import { useTabStore } from "./use-tab-store";
import { editorController, editorState } from "./utils";
import { santizeUri } from "../../../common/filesystem/utils";
type PickerOptions = {
noteId?: string;
@@ -103,7 +102,7 @@ const file = async (fileOptions: PickerOptions) => {
return;
}
let uri = santizeUri(fileCopyUri.localUri);
const uri = santizeUri(fileCopyUri.localUri);
const hash = await Sodium.hashFile({
uri: uri,
type: "url"

View File

@@ -19,7 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import type { ToolbarGroupDefinition } from "@notesnook/editor";
import { useEditor } from "./use-editor";
import { FeatureId, FeatureResult } from "@notesnook/common";
import { DayFormat } from "@notesnook/core";
export type useEditorType = ReturnType<typeof useEditor>;

View File

@@ -207,7 +207,7 @@ export const useEditorEvents = (
return () => {
subscriptions.forEach((subscription) => subscription.remove());
};
}, [editor.commands, editor.postMessage]);
}, [editor, editor.commands, editor.postMessage]);
useEffect(() => {
if (loading) return;
if (typeof defaultFontFamily === "object") {
@@ -764,7 +764,7 @@ export const useEditorEvents = (
let filePath: string;
let fileName: string;
if (Platform.OS === "android") {
let file = await ScopedStorage.createDocument(
const file = await ScopedStorage.createDocument(
"table.csv",
"text/csv",
csv,

View File

@@ -38,15 +38,14 @@ import { strings } from "@notesnook/intl";
import { useThemeEngineStore } from "@notesnook/theme";
import { Mutex } from "async-mutex";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Linking } from "react-native";
import WebView from "react-native-webview";
import { DatabaseLogger, db } from "../../../common/database";
import useGlobalSafeAreaInsets from "../../../hooks/use-global-safe-area-insets";
import { DDS } from "../../../services/device-detection";
import {
ToastManager,
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent
eSubscribeEvent
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { NotePreviewWidget } from "../../../services/note-preview-widget";
@@ -70,7 +69,6 @@ import { SessionHistory } from "./session-history";
import { EditorState, SavePayload } from "./types";
import { TabSessionItem, syncTabs, useTabStore } from "./use-tab-store";
import { defaultState, isContentInvalid, isEditorLoaded, post } from "./utils";
import { Linking } from "react-native";
const loadNoteMutex = new Mutex();

View File

@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { parseInternalLink } from "@notesnook/core";
import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events";
import { createRef, MutableRefObject, RefObject } from "react";
import { TextInput } from "react-native";
import WebView from "react-native-webview";
@@ -28,10 +29,8 @@ import {
eUnSubscribeEvent
} from "../../../services/event-manager";
import { eOnLoadNote } from "../../../utils/events";
import { NotesnookModule } from "../../../utils/notesnook-module";
import { AppState, EditorState, useEditorType } from "./types";
import { EditorState, useEditorType } from "./types";
import { useTabStore } from "./use-tab-store";
import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events";
export const textInput = createRef<TextInput>();
export const editorController =

View File

@@ -45,7 +45,10 @@ import { AddNotebookSheet } from "../../components/sheets/add-notebook";
import { Notice } from "../../components/ui/notice";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import { isFeatureAvailable } from "@notesnook/common";
import { ToastManager } from "../../services/event-manager";
import {
sendItemUpdateEvent,
ToastManager
} from "../../services/event-manager";
import PaywallSheet from "../../components/sheets/paywall";
const {
@@ -175,6 +178,7 @@ const LinkNotebooks = (props: NavigationProps<"LinkNotebooks">) => {
updateNotebook(item.id);
}
}
sendItemUpdateEvent(id, "notebook");
}
Navigation.queueRoutesForUpdate();
@@ -237,9 +241,8 @@ const LinkNotebooks = (props: NavigationProps<"LinkNotebooks">) => {
button={{
icon: "plus",
onPress: async () => {
const notebooksFeature = await isFeatureAvailable(
"notebooks"
);
const notebooksFeature =
await isFeatureAvailable("notebooks");
if (!notebooksFeature.isAllowed) {
ToastManager.show({
message: notebooksFeature.error,
@@ -388,8 +391,8 @@ const NotebookItemWrapper = React.memo(
!selected
? "selected"
: !state.initialState[item.notebook.id]
? undefined
: "deselected"
? undefined
: "deselected"
);
}}
canDisableSelectionMode={false}

View File

@@ -33,7 +33,10 @@ import { Pressable } from "../../components/ui/pressable";
import Heading from "../../components/ui/typography/heading";
import Paragraph from "../../components/ui/typography/paragraph";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import { ToastManager } from "../../services/event-manager";
import {
sendItemUpdateEvent,
ToastManager
} from "../../services/event-manager";
import Navigation, { NavigationProps } from "../../services/navigation";
import {
ItemSelection,
@@ -80,7 +83,10 @@ const useTagItemSelection = createItemSelectionStore(true);
const ManageTags = (props: NavigationProps<"ManageTags">) => {
const { colors } = useThemeColors();
const ids = props.route.params.ids || [];
const ids = React.useMemo(
() => props.route.params.ids || [],
[props.route.params.ids]
);
const [tags, setTags] = useState<Tag[]>();
const [query, setQuery] = useState<string>();
const inputRef = useRef<TextInput>(null);
@@ -121,7 +127,7 @@ const ManageTags = (props: NavigationProps<"ManageTags">) => {
tags.splice(0, 0, ...noteTags);
setTags(tags);
},
[]
[ids]
);
const refreshTags = useCallback(() => {
@@ -135,7 +141,7 @@ const ManageTags = (props: NavigationProps<"ManageTags">) => {
.sorted(db.settings.getGroupOptions("tags"))
.then(sortAndSetTags);
}
}, [query]);
}, [query, sortAndSetTags]);
useEffect(() => {
refreshTags();
@@ -199,6 +205,7 @@ const ManageTags = (props: NavigationProps<"ManageTags">) => {
}
useRelationStore.getState().update();
sendItemUpdateEvent(id, "tag");
useTagStore.getState().refresh();
setQuery(undefined);
} catch (e) {
@@ -242,6 +249,7 @@ const ManageTags = (props: NavigationProps<"ManageTags">) => {
console.error(e);
}
}
sendItemUpdateEvent(id, "tag");
useTagStore.getState().refresh();
useRelationStore.getState().update();
refreshTags();
@@ -417,15 +425,15 @@ const TagItem = ({
selection === "selected"
? "check-circle-outline"
: selection === "intermediate"
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
name={
selection === "selected"
? "check-circle-outline"
: selection === "intermediate"
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
)}

View File

@@ -46,6 +46,7 @@ import {
import {
checkParentSelected,
findRootNotebookId,
getAllNotebookChildren,
getParentNotebookId
} from "../../utils/notebooks";
import { DefaultAppStyles } from "../../utils/styles";
@@ -124,17 +125,25 @@ export const MoveNotebook = (props: NavigationProps<"MoveNotebook">) => {
useEffect(() => {
async function filterNotebooks() {
const excludedItems: string[] = [];
const disabledItems: string[] = [];
for (const notebook of selectedNotebooks) {
const children = await getAllNotebookChildren(notebook.id, []);
excludedItems.push(...children);
const parent = await getParentNotebookId(notebook.id);
if (!excludedItems.includes(parent)) {
excludedItems.push(parent);
if (!disabledItems.includes(parent)) {
disabledItems.push(parent);
}
excludedItems.push(notebook.id);
}
const filtered = tree.filter(
(treeItem) => !excludedItems.includes(treeItem.notebook.id)
// Exclude and disable items as needed
const filtered = tree.filter(item => !excludedItems.includes(item.notebook.id)).map(
(treeItem) => {
return {
...treeItem,
disabled: disabledItems.includes(treeItem.notebook.id)
}
}
);
return filtered;
}
filterNotebooks().then((filtered) => {
@@ -149,6 +158,13 @@ export const MoveNotebook = (props: NavigationProps<"MoveNotebook">) => {
index={index}
item={item}
onPress={async () => {
if (item.disabled) {
ToastManager.show({
type: "info",
"message": "You cannot move the selected notebook(s) here"
})
return;
}
const selectedNotebook = item.notebook;
presentDialog({
title: strings.moveNotebooks(selectedNotebooks.length),
@@ -239,6 +255,7 @@ export const MoveNotebook = (props: NavigationProps<"MoveNotebook">) => {
updateNotebooks();
}, 300);
}}
testID="move-notebook-search"
button={{
icon: "plus",
onPress: async () => {

View File

@@ -39,6 +39,7 @@ import { createItemSelectionStore } from "../../stores/item-selection-store";
import { updateNotebook } from "../../utils/notebooks";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { sendItemUpdateEvent } from "../../services/event-manager";
const useItemSelectionStore = createItemSelectionStore(true);
export const MoveNotes = (props: NavigationProps<"MoveNotes">) => {
@@ -198,6 +199,7 @@ export const MoveNotes = (props: NavigationProps<"MoveNotes">) => {
);
updateNotebook(currentNotebook.id);
sendItemUpdateEvent(currentNotebook.id, "notebook");
Navigation.queueRoutesForUpdate();
Navigation.goBack();
}}
@@ -281,8 +283,8 @@ const SelectableNoteItem = React.memo(
selected
? colors.selected.icon
: exists && !selected
? colors.static.red
: colors.primary.icon
? colors.static.red
: colors.primary.icon
}
/>
</>

View File

@@ -202,7 +202,7 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
) : undefined
}
placeholder={{
title: notebook?.title!,
title: notebook?.title || "",
paragraph: strings.notesEmpty(),
button: strings.addFirstNote(),
action: openEditor,

View File

@@ -17,23 +17,22 @@ 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 { isFeatureAvailable } from "@notesnook/common";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { StyleSheet, View } from "react-native";
import { DraxProvider, DraxScrollView } from "react-native-drax";
import Animated, { FadeInDown, FadeOutDown } from "react-native-reanimated";
import PaywallSheet from "../../../components/sheets/paywall";
import { Button } from "../../../components/ui/button";
import { Notice } from "../../../components/ui/notice";
import Paragraph from "../../../components/ui/typography/paragraph";
import PremiumService from "../../../services/premium";
import { useThemeColors } from "@notesnook/theme";
import { ToastManager } from "../../../services/event-manager";
import { AppFontSize } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import { Group } from "./group";
import { DragState, useDragState } from "./state";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../../utils/styles";
import { isFeatureAvailable } from "@notesnook/common";
import { ToastManager } from "../../../services/event-manager";
import PaywallSheet from "../../../components/sheets/paywall";
export const ConfigureToolbar = () => {
const data = useDragState((state) => state.data);
const preset = useDragState((state) => state.preset);
@@ -41,7 +40,7 @@ export const ConfigureToolbar = () => {
const renderGroups = () => {
return data?.map((item, index) => (
<Group key={`group-${index}`} item={item} index={index} />
<Group key={`group-${index}-${item.length}`} item={item} index={index} />
));
};
@@ -131,6 +130,7 @@ export const ConfigureToolbar = () => {
style={{
flex: 1
}}
key={preset}
scrollEventThrottle={13}
showsVerticalScrollIndicator={false}
>

View File

@@ -31,7 +31,7 @@ import { renderTool } from "./common";
import { DraggableItem, useDragState } from "./state";
import ToolSheet from "./tool-sheet";
import { useIsFeatureAvailable } from "@notesnook/common";
import { isFeatureAvailable, useIsFeatureAvailable } from "@notesnook/common";
import type { ToolId } from "@notesnook/editor";
import { strings } from "@notesnook/intl";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
@@ -64,78 +64,87 @@ export const Group = ({
const { colors } = useThemeColors();
const featureAvailable = useIsFeatureAvailable("customToolbarPreset");
const onDrop = (data: DraxDragWithReceiverEventData) => {
if (!featureAvailable?.isAllowed) {
ToastManager.show({
type: "info",
message: featureAvailable?.error
});
return;
}
const isDroppedAbove = data.receiver.receiveOffsetRatio.y < 0.5;
const dragged = data.dragged.payload;
const reciever = data.receiver.payload;
const _data = useDragState.getState().data.slice();
if (dragged.type === "group") {
const fromIndex = dragged.index;
const toIndex = isDroppedAbove
? Math.max(0, reciever.index)
: reciever.index + 1;
_data.splice(
toIndex > fromIndex ? toIndex - 1 : toIndex,
0,
_data.splice(fromIndex, 1)[0]
);
}
// Always insert sub group at the end of the group.
if (dragged.type === "subgroup") {
const fromIndex = dragged.index;
const insertAt = _data[reciever.index] as string[];
const insertFrom = _data[dragged.groupIndex] as string[];
if (typeof insertAt[insertAt.length - 1] !== "string") {
setRecieving(false);
return data.dragAbsolutePosition;
const onDrop = React.useCallback(
(data: DraxDragWithReceiverEventData) => {
if (!featureAvailable?.isAllowed) {
ToastManager.show({
type: "info",
message: featureAvailable?.error || strings.featureNotAvailable()
});
return;
}
insertAt.push(insertFrom.splice(fromIndex, 1)[0]);
}
const isDroppedAbove = data.receiver.receiveOffsetRatio.y < 0.5;
const dragged = data.dragged.payload;
const reciever = data.receiver.payload;
const _data = useDragState.getState().data.slice();
if (dragged.type === "tool") {
const insertFrom =
typeof dragged.parentIndex === "number"
? (_data[dragged.parentIndex][dragged.groupIndex] as string[])
: (_data[dragged.groupIndex] as string[]);
_data[groupIndex].push(insertFrom.splice(dragged.index, 1)[0] as ToolId);
}
if (dragged.type === "group") {
const fromIndex = dragged.index;
const toIndex = isDroppedAbove
? Math.max(0, reciever.index)
: reciever.index + 1;
setData(_data);
setRecieving(false);
return data.dragAbsolutePosition;
};
_data.splice(
toIndex > fromIndex ? toIndex - 1 : toIndex,
0,
_data.splice(fromIndex, 1)[0]
);
}
const onRecieveData = (data: DraxDragWithReceiverEventData) => {
setRecieving(true);
if (data.dragged.payload.type !== "group")
return setRecievePosition("below");
if (data.receiver.receiveOffsetRatio.y < 0.5) {
setRecievePosition("above");
} else {
setRecievePosition("below");
}
};
// Always insert sub group at the end of the group.
if (dragged.type === "subgroup") {
const fromIndex = dragged.index;
const insertAt = _data[reciever.index] as string[];
const insertFrom = _data[dragged.groupIndex] as string[];
if (typeof insertAt[insertAt.length - 1] !== "string") {
setRecieving(false);
return data.dragAbsolutePosition;
}
insertAt.push(insertFrom.splice(fromIndex, 1)[0]);
}
if (dragged.type === "tool") {
const insertFrom =
typeof dragged.parentIndex === "number"
? (_data[dragged.parentIndex][dragged.groupIndex] as string[])
: (_data[dragged.groupIndex] as string[]);
_data[groupIndex].push(
insertFrom.splice(dragged.index, 1)[0] as ToolId
);
}
setData(_data);
setRecieving(false);
return data.dragAbsolutePosition;
},
[featureAvailable]
);
const onRecieveData = React.useCallback(
(data: DraxDragWithReceiverEventData) => {
setRecieving(true);
if (data.dragged.payload.type !== "group")
return setRecievePosition("below");
if (data.receiver.receiveOffsetRatio.y < 0.5) {
setRecievePosition("above");
} else {
setRecievePosition("below");
}
},
[]
);
const buttons = [
{
name: "minus",
onPress: () => {
if (!featureAvailable?.isAllowed) {
onPress: async () => {
const feature = await isFeatureAvailable("customToolbarPreset");
if (!feature.isAllowed) {
ToastManager.show({
type: "info",
message: featureAvailable?.error
message: feature?.error
});
return;
}
@@ -157,11 +166,12 @@ export const Group = ({
},
{
name: "plus",
onPress: () => {
if (!featureAvailable?.isAllowed) {
onPress: async () => {
const feature = await isFeatureAvailable("customToolbarPreset");
if (!feature.isAllowed) {
ToastManager.show({
type: "info",
message: featureAvailable?.error
message: feature?.error
});
return;
}

View File

@@ -44,8 +44,8 @@ export default function ToolSheet({
fwdRef: RefObject<ActionSheetRef>;
}) {
const { colors } = useThemeColors();
const data = useDragState((state) => state.data);
const ungrouped = getUngroupedTools(data) as ToolId[];
const [data] = useDragState((state) => [state.data]);
const ungrouped = getUngroupedTools( data) as ToolId[];
const renderTool = React.useCallback(
(item: ToolId) => {
@@ -114,7 +114,7 @@ export default function ToolSheet({
return (
<View
style={{
maxHeight: "100%",
maxHeight: 400,
padding: DefaultAppStyles.GAP
}}
>

View File

@@ -33,7 +33,7 @@ import { DraggableItem, useDragState } from "./state";
import ToolSheet from "./tool-sheet";
import { findToolById, getToolIcon } from "./toolbar-definition";
import { useIsFeatureAvailable } from "@notesnook/common";
import { isFeatureAvailable, useIsFeatureAvailable } from "@notesnook/common";
import type { ToolId } from "@notesnook/editor";
import { strings } from "@notesnook/intl";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
@@ -78,11 +78,12 @@ export const Tool = ({
? [
{
name: "minus",
onPress: () => {
if (!featureAvailable?.isAllowed) {
onPress: async () => {
const feature = await isFeatureAvailable("customToolbarPreset");
if (!feature.isAllowed) {
ToastManager.show({
type: "info",
message: featureAvailable?.error
message: feature?.error
});
return;
}
@@ -102,11 +103,12 @@ export const Tool = ({
},
{
name: "plus",
onPress: () => {
if (!featureAvailable?.isAllowed) {
onPress: async () => {
const feature = await isFeatureAvailable("customToolbarPreset");
if (!feature.isAllowed) {
ToastManager.show({
type: "info",
message: featureAvailable?.error
message: feature?.error
});
return;
}
@@ -122,11 +124,12 @@ export const Tool = ({
: [
{
name: "minus",
onPress: () => {
if (!featureAvailable?.isAllowed) {
onPress: async () => {
const feature = await isFeatureAvailable("customToolbarPreset");
if (!feature.isAllowed) {
ToastManager.show({
type: "info",
message: featureAvailable?.error
message: feature?.error
});
return;
}
@@ -151,7 +154,7 @@ export const Tool = ({
if (parentIndex === undefined && !isSubgroup) {
btns.unshift({
name: "unfold-less-horizontal",
onPress: () => {
onPress: async () => {
if (groupIndex === undefined) return;
const _data = useDragState.getState().data.slice();
const hasSubGroup = Array.isArray(
@@ -267,6 +270,7 @@ export const Tool = ({
style={{
paddingLeft: 30
}}
key={`subgroup-${item.length}-${groupIndex}-${index}-${parentIndex}`}
>
{renderGroup({ index, item, parentIndex: groupIndex })}
</View>
@@ -290,63 +294,69 @@ export const Tool = ({
]
);
const onDrop = (data: DraxDragWithReceiverEventData) => {
if (!featureAvailable?.isAllowed) {
ToastManager.show({
type: "info",
message: featureAvailable?.error
});
return;
}
const isDroppedAbove = data.receiver.receiveOffsetRatio.y < 0.5;
const dragged = data.dragged.payload;
const reciever = data.receiver.payload;
const _data = useDragState.getState().data?.slice();
if (!_data) return;
const isFromSubgroup = typeof dragged?.parentIndex === "number";
const isDroppedAtSubgroup = typeof reciever?.parentIndex === "number";
if (dragged.type === "tool") {
const fromIndex = dragged.index;
const toIndex = isDroppedAbove
? Math.max(0, reciever.index)
: reciever.index + 1;
const insertAt = isDroppedAtSubgroup
? (_data[reciever.parentIndex][reciever.groupIndex] as string[])
: (_data[reciever.groupIndex] as string[]);
const insertFrom = isFromSubgroup
? (_data[dragged.parentIndex][dragged.groupIndex] as string[])
: (_data[dragged.groupIndex] as string[]);
insertAt.splice(
toIndex > fromIndex ? toIndex - 1 : toIndex,
0,
insertFrom.splice(fromIndex, 1)[0]
);
// Remove the group or subgroup if it is empty.
if (insertFrom.length === 0) {
isFromSubgroup
? _data[dragged.parentIndex].splice(
_data[dragged.parentIndex].length - 1,
1
)
: _data.splice(dragged.groupIndex, 1);
const onDrop = React.useCallback(
(data: DraxDragWithReceiverEventData) => {
if (!featureAvailable?.isAllowed) {
ToastManager.show({
type: "info",
message: featureAvailable?.error || strings.featureNotAvailable()
});
return;
}
}
setData(_data);
setRecieving(false);
return data.dragAbsolutePosition;
};
const isDroppedAbove = data.receiver.receiveOffsetRatio.y < 0.5;
const dragged = data.dragged.payload;
const reciever = data.receiver.payload;
const _data = useDragState.getState().data?.slice();
if (!_data) return;
const isFromSubgroup = typeof dragged?.parentIndex === "number";
const isDroppedAtSubgroup = typeof reciever?.parentIndex === "number";
const onRecieveData = (data: DraxDragWithReceiverEventData) => {
setRecieving(true);
if (data.receiver.receiveOffsetRatio.y < 0.5) {
setRecievePosition("above");
} else {
setRecievePosition("below");
}
};
if (dragged.type === "tool") {
const fromIndex = dragged.index;
const toIndex = isDroppedAbove
? Math.max(0, reciever.index)
: reciever.index + 1;
const insertAt = isDroppedAtSubgroup
? (_data[reciever.parentIndex][reciever.groupIndex] as string[])
: (_data[reciever.groupIndex] as string[]);
const insertFrom = isFromSubgroup
? (_data[dragged.parentIndex][dragged.groupIndex] as string[])
: (_data[dragged.groupIndex] as string[]);
insertAt.splice(
toIndex > fromIndex ? toIndex - 1 : toIndex,
0,
insertFrom.splice(fromIndex, 1)[0]
);
// Remove the group or subgroup if it is empty.
if (insertFrom.length === 0) {
isFromSubgroup
? _data[dragged.parentIndex].splice(
_data[dragged.parentIndex].length - 1,
1
)
: _data.splice(dragged.groupIndex, 1);
}
}
setData(_data);
setRecieving(false);
return data.dragAbsolutePosition;
},
[featureAvailable]
);
const onRecieveData = React.useCallback(
(data: DraxDragWithReceiverEventData) => {
setRecieving(true);
if (data.receiver.receiveOffsetRatio.y < 0.5) {
setRecievePosition("above");
} else {
setRecievePosition("below");
}
},
[]
);
return (
<Animated.View layout={Layout}>

View File

@@ -25,18 +25,18 @@ import {
} from "@notesnook/editor/dist/cjs/toolbar/tool-definitions";
import { ToolId } from "@notesnook/editor";
export const tools = getAllTools() as any;
export const tools = () => getAllTools();
export const presets: { [name: string]: ToolbarGroupDefinition[] } = {
default: getDefaultPresets().default as any,
minimal: getDefaultPresets().minimal as any,
custom: []
};
export function findToolById(id: keyof typeof tools): {
export function findToolById(id: keyof ReturnType<typeof tools> ): {
title: string;
icon: string;
} {
return tools[id];
return tools()[id];
}
export function getToolIcon(id: ToolId, color: string) {
@@ -51,13 +51,14 @@ export function getToolIcon(id: ToolId, color: string) {
export function getUngroupedTools(
toolDefinition: (string | string[])[][]
): string[] {
const keys = Object.keys(tools);
): string[] {
const allTools = tools();
const keys = Object.keys(allTools);
const ungrouped = [];
const toolString = JSON.stringify(toolDefinition);
for (const key of keys) {
if (tools[key as ToolId].conditional) continue;
if ((allTools[key as ToolId] as any).conditional) continue;
if (!toolString.includes(key)) ungrouped.push(key);
}

View File

@@ -21,7 +21,7 @@ import { NativeStackScreenProps } from "@react-navigation/native-stack";
import React from "react";
import { View } from "react-native";
import { KeyboardAwareFlatList } from "react-native-keyboard-aware-scroll-view";
import Animated, { FadeInDown } from "react-native-reanimated";
import Animated from "react-native-reanimated";
import DelayLayout from "../../components/delay-layout";
import { Header } from "../../components/header";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";

View File

@@ -1,3 +1,21 @@
/*
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 { strings } from "@notesnook/intl";
import { presentDialog } from "../../components/dialog/functions";
import { DatabaseLogger, db } from "../../common/database";

View File

@@ -1,3 +1,22 @@
/*
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 React from "react";
import { CirclePartner, SubscriptionStatus } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
@@ -77,7 +96,7 @@ export const NotesnookCircle = () => {
) : null}
{partners.result?.map((item) => (
<Partner item={item} available={!isFree && !isOnTrial} />
<Partner key={item.id} item={item} available={!isFree && !isOnTrial} />
))}
</ScrollView>
);

View File

@@ -25,12 +25,10 @@ import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { Dialog } from "../../../components/dialog";
import { Pressable } from "../../../components/ui/pressable";
import Paragraph from "../../../components/ui/typography/paragraph";
import PremiumService from "../../../services/premium";
import { getColorLinearShade } from "../../../utils/colors";
import { defaultBorderRadius, AppFontSize } from "../../../utils/size";
import { sleep } from "../../../utils/time";
import { verifyUser } from "../functions";
import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import { verifyUser } from "../functions";
interface PickerOptions<T> {
getValue: () => T;

View File

@@ -24,13 +24,7 @@ import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import { keepLocalCopy, pick } from "@react-native-documents/picker";
import React, { useEffect, useState } from "react";
import {
ActivityIndicator,
FlatList,
Platform,
ScrollView,
View
} from "react-native";
import { ActivityIndicator, Platform, ScrollView, View } from "react-native";
import RNFetchBlob, { ReactNativeBlobUtilStat } from "react-native-blob-util";
import * as ScopedStorage from "react-native-scoped-storage";
import { unzip } from "react-native-zip-archive";

View File

@@ -46,8 +46,10 @@ import { RouteParams, SettingSection } from "./types";
const _SectionItem = ({ item }: { item: SettingSection }) => {
const { colors } = useThemeColors();
// eslint-disable-next-line react-hooks/exhaustive-deps
const isFeatureAvailable = item.featureId
? useIsFeatureAvailable(item.featureId)
? // eslint-disable-next-line react-hooks/rules-of-hooks
useIsFeatureAvailable(item.featureId)
: ({
isAllowed: true
} as FeatureResult);

View File

@@ -36,7 +36,7 @@ import { strings } from "@notesnook/intl";
const onPressFloatingButton = () => {
presentDialog({
title: strings.clearTrash(),
paragraph: strings.clearTrashConfirm(),
paragraph: strings.clearTrashDesc(),
positiveText: strings.clear(),
negativeText: strings.cancel(),
positivePress: async () => {
@@ -58,8 +58,8 @@ const PLACEHOLDER_DATA = (trashCleanupInterval = 7) => ({
trashCleanupInterval === -1
? strings.noTrashCleanupInterval()
: trashCleanupInterval === 1
? strings.trashCleanupIntervalTextDaily()
: strings.trashCleanupIntervalTextDays(trashCleanupInterval),
? strings.trashCleanupIntervalTextDaily()
: strings.trashCleanupIntervalTextDays(trashCleanupInterval),
loading: strings.loadingTrash()
});
@@ -107,6 +107,7 @@ export const Trash = ({ navigation, route }: NavigationProps<"Trash">) => {
{trash && trash?.placeholders?.length !== 0 ? (
<FloatingButton
onPress={onPressFloatingButton}
testID="trash-clear"
alwaysVisible={true}
/>
) : null}

View File

@@ -17,7 +17,13 @@ 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 { EventHandler, EventManager, Note, NoteContent } from "@notesnook/core";
import {
EventHandler,
EventManager,
ItemType,
Note,
NoteContent
} from "@notesnook/core";
import Clipboard from "@react-native-clipboard/clipboard";
import { RefObject } from "react";
import { ActionSheetRef } from "react-native-actions-sheet";
@@ -84,6 +90,18 @@ export const eSubscribeEvent = <T = unknown>(
return eventManager.subscribe(eventName, action);
};
export function sendItemUpdateEvent(id: string, type: ItemType) {
eSendEvent(`${type}:${id}`);
}
export function subscribeToItemUpdate(
id: string,
type: ItemType,
callback: EventHandler
) {
return eSubscribeEvent(`${type}:${id}`, callback);
}
export const eUnSubscribeEvent = <T = unknown>(
eventName: string,
action: EventHandler

View File

@@ -16,7 +16,7 @@ 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 React from "react";
import { strings } from "@notesnook/intl";
import { Platform } from "react-native";
import { AuthMode } from "../components/auth/common";
@@ -146,7 +146,7 @@ const updateAvailableMessage = (
icon: "update",
type: "normal",
id: "app-update"
} as Message);
}) as Message;
export function setUpdateAvailableMessage(
version: GithubVersionInfo | CheckVersionResponse

View File

@@ -33,7 +33,7 @@ let subs: RNIap.Subscription[] = [];
let products: RNIap.Product[] = [];
async function setPremiumStatus() {
let userstore = useUserStore.getState();
const userstore = useUserStore.getState();
try {
const user = await db.user.getUser();
if (!user) {
@@ -99,7 +99,7 @@ const showVerifyEmailDialog = () => {
paragraph: strings.emailConfirmationLinkSent(),
action: async () => {
try {
let lastVerificationEmailTime =
const lastVerificationEmailTime =
SettingsService.get().lastVerificationEmailTime;
if (
lastVerificationEmailTime &&
@@ -148,7 +148,7 @@ const subscriptions = {
*/
get: () => {
if (Platform.OS === "android") return;
let _subscriptions = MMKV.getString("subscriptionsIOS");
const _subscriptions = MMKV.getString("subscriptionsIOS");
if (!_subscriptions) return [];
return JSON.parse(_subscriptions) as (
| RNIap.SubscriptionPurchase
@@ -167,7 +167,7 @@ const subscriptions = {
const _subscriptions = subscriptions.get();
if (!_subscriptions) return;
let index = _subscriptions.findIndex(
const index = _subscriptions.findIndex(
(s) => s.transactionId === subscription.transactionId
);
if (index === -1) {
@@ -182,7 +182,7 @@ const subscriptions = {
const _subscriptions = subscriptions.get();
if (!_subscriptions) return;
let index = _subscriptions.findIndex(
const index = _subscriptions.findIndex(
(s) => s.transactionId === transactionId
);
if (index !== -1) {
@@ -201,9 +201,9 @@ const subscriptions = {
if (subscription.transactionReceipt) {
if (Platform.OS === "ios") {
let user = await db.user.getUser();
const user = await db.user.getUser();
if (!user) return;
let requestData = {
const requestData = {
method: "POST",
body: JSON.stringify({
receipt_data: subscription.transactionReceipt,
@@ -216,14 +216,14 @@ const subscriptions = {
};
try {
let result = await fetch(
const result = await fetch(
__DEV__
? "https://payments.streetwriters.co/apple/verify"
: "https://payments.streetwriters.co/apple/verify",
requestData
);
let text = await result.text();
const text = await result.text();
if (!result.ok) {
if (text === "Receipt already expired.") {
@@ -243,7 +243,7 @@ const subscriptions = {
_subscription?: RNIap.SubscriptionPurchase | RNIap.ProductPurchase
) => {
if (Platform.OS === "android") return;
let _subscriptions = subscriptions.get();
const _subscriptions = subscriptions.get();
if (!_subscriptions) return;
let subscription = null;
if (_subscription) {

View File

@@ -22,7 +22,6 @@ import { db } from "../common/database";
import { DatabaseLogger } from "../common/database/index";
import { initAfterSync } from "../stores/index";
import { SyncStatus, useUserStore } from "../stores/use-user-store";
import BackgroundSync from "./background-sync";
import { ToastManager } from "./event-manager";
import SettingsService from "./settings";

View File

@@ -196,7 +196,7 @@ export const Editor = ({
}
};
}
}, []);
}, [editor.ref, editorRef]);
return (
<View

View File

@@ -78,7 +78,7 @@ const getLinkPreview = (url: string) => {
};
async function sanitizeHtml(site: string, title?: string) {
try {
let html = await fetchHandle.current?.processUrl(site);
const html = await fetchHandle.current?.processUrl(site);
return (
html +
"<hr/>" +
@@ -112,7 +112,7 @@ type DefaultNote = {
};
};
let defaultNote: DefaultNote = {
const defaultNote: DefaultNote = {
title: "",
id: undefined,
content: {
@@ -174,7 +174,8 @@ const ShareView = () => {
const [compress, setCompress] = useState(true);
globalThis["IS_SHARE_EXTENSION"] = true;
const onKeyboardDidShow = (event: KeyboardEvent) => {
let height = Dimensions.get("screen").height - event.endCoordinates.screenY;
const height =
Dimensions.get("screen").height - event.endCoordinates.screenY;
if (height < 100) {
setKeyboardHeight(0);
keyboardHeightRef.current = 0;
@@ -190,11 +191,11 @@ const ShareView = () => {
};
useEffect(() => {
let keyboardDidShow = Keyboard.addListener(
const keyboardDidShow = Keyboard.addListener(
"keyboardDidShow",
onKeyboardDidShow
);
let keyboardDidHide = Keyboard.addListener(
const keyboardDidHide = Keyboard.addListener(
"keyboardDidHide",
onKeyboardDidHide
);
@@ -211,10 +212,10 @@ const ShareView = () => {
}, [fullQualityImages]);
const showLinkPreview = async (note: DefaultNote, link: string) => {
let _note = note;
const _note = note;
_note.content.data = makeHtmlFromUrl(link);
try {
let preview = await getLinkPreview(link);
const preview = await getLinkPreview(link);
_note.title = preview.title;
} catch (e) {
console.log(e);
@@ -260,7 +261,7 @@ const ShareView = () => {
}
let note = { ...defaultNote };
for (let item of data) {
for (const item of data) {
if (item.type === "text") {
setRawData(item);
if (isURL(item.value)) {
@@ -357,7 +358,7 @@ const ShareView = () => {
return;
}
let rawContent = note.contentId
const rawContent = note.contentId
? await db.content.get(note.contentId)
: null;
noteData = {
@@ -406,7 +407,7 @@ const ShareView = () => {
if (value === 2) {
setLoadingPage(true);
setTimeout(async () => {
let html = await sanitizeHtml(rawData?.value || "", note.title);
const html = await sanitizeHtml(rawData?.value || "", note.title);
noteContent.current = html || "";
setLoadingPage(false);
onLoad();
@@ -417,11 +418,11 @@ const ShareView = () => {
}, 300);
} else {
setLoadingPage(false);
let html = !rawData.value
const html = !rawData.value
? ""
: isURL(rawData?.value)
? makeHtmlFromUrl(rawData?.value)
: makeHtmlFromPlainText(rawData?.value);
? makeHtmlFromUrl(rawData?.value)
: makeHtmlFromPlainText(rawData?.value);
setNote((note) => {
note.content.data = html;
noteContent.current = html;

View File

@@ -43,7 +43,7 @@ const StorageKeys = {
appSettings: "appSettings"
};
let appSettingsJson = MMKV.getString(StorageKeys.appSettings);
const appSettingsJson = MMKV.getString(StorageKeys.appSettings);
let appSettings: SettingStore["settings"] | null = null;
if (appSettingsJson) {
appSettings = JSON.parse(appSettingsJson) as SettingStore["settings"];
@@ -86,9 +86,9 @@ export const useShareStore = create<ShareStore>((set) => ({
set({ appendNote: noteId });
},
restore: () => {
let appendNote = MMKV.getString(StorageKeys.appendNote);
let selectedNotebooks = MMKV.getString(StorageKeys.selectedNotebooks);
let selectedTags = MMKV.getString(StorageKeys.selectedTag);
const appendNote = MMKV.getString(StorageKeys.appendNote);
const selectedNotebooks = MMKV.getString(StorageKeys.selectedNotebooks);
const selectedTags = MMKV.getString(StorageKeys.selectedTag);
set({
appendNote: appendNote,
selectedNotebooks: selectedNotebooks ? JSON.parse(selectedNotebooks) : [],

View File

@@ -22,7 +22,8 @@ import create, { StoreApi, UseBoundStore } from "zustand";
import { resolveItems } from "@notesnook/common";
import { useSettingStore } from "./use-setting-store";
import { DatabaseLogger } from "../common/database";
import { ToastManager } from "../services/event-manager";
import { eSendEvent, ToastManager } from "../services/event-manager";
import { eMenuItemUpdate } from "../utils/events";
export interface DBCollectionStore<Type extends Item> {
items: VirtualizedGrouping<Type> | undefined;
@@ -67,6 +68,7 @@ export default function createDBCollectionStore<Type extends Item>({
items,
loading: false
});
eSendEvent(eMenuItemUpdate);
} catch (e) {
DatabaseLogger.error(e as Error, "useDBCollectionStore.refresh", {
useDBCollectionStore: "refresh"

View File

@@ -28,6 +28,7 @@ export type TreeItem = {
notebook: Notebook;
depth: number;
hasChildren: boolean;
disabled?: boolean;
};
function removeTreeItem(tree: TreeItem[], id: string) {

View File

@@ -37,7 +37,7 @@ import { useUserStore } from "./use-user-store";
async function syncShortcuts(result: ShortcutInfo[]) {
try {
for (let shortcut of result) {
for (const shortcut of result) {
switch (shortcut.type) {
case "note":
{

View File

@@ -176,3 +176,4 @@ export const eOnEnterEditor = "621";
export const eOnExitEditor = "622";
export const eEditorReset = "623";
export const eAfterSync = "624";
export const eMenuItemUpdate = "625";

View File

@@ -34,6 +34,7 @@ import { useRelationStore } from "../stores/use-relation-store";
import { useTagStore } from "../stores/use-tag-store";
import { eUpdateNoteInEditor } from "./events";
import { unlockVault } from "./unlock-vault";
import { useTrashStore } from "../stores/use-trash-store";
export const valueLimiter = (value: number, min: number, max: number) => {
return value < min ? min : value > max ? max : value;
@@ -210,7 +211,9 @@ export const deleteItems = async (
if (type === "notebook") {
useNotebookStore.getState().refresh();
}
useTrashStore.getState().refresh();
useMenuStore.getState().setMenuPins();
useRelationStore.getState().update();
};
export const openLinkInBrowser = async (link: string) => {

View File

@@ -61,6 +61,25 @@ export async function checkParentSelected(
}
}
export async function getAllNotebookChildren(id: string, childrenIds: string[] = []) {
const relation = await db.relations
.from(
{
id,
type: "notebook"
},
"notebook"
)
.get()
childrenIds.push(...relation.map(rel => rel.toId))
for (let rel of relation) {
await getAllNotebookChildren(rel.toId, childrenIds)
}
return childrenIds
}
export async function getParentNotebookId(id: string) {
const relation = await db.relations
.to(

View File

@@ -38,6 +38,7 @@ export async function unlockVault({
if (unlockPromise) {
return unlockPromise;
}
// eslint-disable-next-line no-async-promise-executor
unlockPromise = new Promise(async (resolve) => {
const result = await (async () => {
if (db.vault.unlocked && !requirePassword) return true;

Some files were not shown because too many files have changed in this diff Show More