Compare commits

...

94 Commits

Author SHA1 Message Date
Ammar Ahmed
f545216dc3 ci: fix artifacts location 2024-11-13 09:31:37 +05:00
Ammar Ahmed
989de21e44 ci: generate debug.keystore 2024-11-13 09:26:12 +05:00
Ammar Ahmed
2c051f9d21 ci: disable gradle caching 2024-11-13 02:18:16 +05:00
Ammar Ahmed
4845d40c0b ci: push 2024-11-13 01:05:52 +05:00
Ammar Ahmed
b4615fa9aa ci: enable build 2024-11-12 22:51:53 +05:00
Ammar Ahmed
f24ff63c06 ci: disable more steps 2024-11-12 22:42:39 +05:00
Ammar Ahmed
7518e6badc ci: skip build 2024-11-12 22:33:21 +05:00
Ammar Ahmed
0fcbdbcdc3 ci: fix ci
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-11-12 21:57:07 +05:00
Ammar Ahmed
999856311f ci: free disk space before build 2024-11-12 20:04:26 +05:00
Ammar Ahmed
98f4e08c62 ci: fix 2024-11-12 18:50:05 +05:00
Ammar Ahmed
f3140cbd8f ci: fix 2024-11-12 18:00:00 +05:00
Ammar Ahmed
029cadedc4 ci: run command from root
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-11-12 16:46:19 +05:00
Ammar Ahmed
6cdeb48e56 ci: do not clean after build
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-11-12 16:05:41 +05:00
Ammar Ahmed
2c8abc4d00 ci: fix working dir path
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-11-12 15:11:16 +05:00
Ammar Ahmed
04820f6500 ci: update workflow
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-11-12 14:15:42 +05:00
Ammar Ahmed
fc6a843a1a ci: fix action 2024-11-12 14:14:05 +05:00
Ammar Ahmed
129dad06c2 ci: format 2024-11-12 14:12:45 +05:00
Ammar Ahmed
063b387807 ci: install detox-cli 2024-11-12 14:11:07 +05:00
Ammar Ahmed
e0058fe30b ci: change script working dir 2024-11-12 13:48:11 +05:00
Ammar Ahmed
a3c297636e ci: clean runner after build
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-11-12 13:10:09 +05:00
Ammar Ahmed
afc534806d ci: clean gh action space 2024-11-12 11:08:43 +05:00
Ammar Ahmed
4456cc2184 mobile: fix intl imports 2024-11-12 10:08:26 +05:00
Ammar Ahmed
2610db90d2 mobile: fix ts errors 2024-11-12 09:54:17 +05:00
Ammar Ahmed
f16c9804ee ci: fix build 2024-11-11 22:48:01 +05:00
Ammar Ahmed
7a3a1aa9c2 ci: update 2024-11-11 19:53:01 +05:00
Ammar Ahmed
57d0904bee mobile: add e2e test workflow 2024-11-11 19:25:56 +05:00
Ammar Ahmed
5ca6f98c6c mobile: fix e2e tests 2024-11-11 18:46:08 +05:00
Abdullah Atta
dbe32e8af6 web: fix more failing tests 2024-11-08 17:06:38 +05:00
Abdullah Atta
e0758bf353 web: fix note & notebook tests 2024-11-08 17:06:38 +05:00
Abdullah Atta
3c1b5d1ee8 intl: update po files 2024-11-08 17:06:38 +05:00
Abdullah Atta
074008ac67 web: localize more missing strings 2024-11-08 17:06:38 +05:00
Ammar Ahmed
7c56a6beb3 mobile: cleanup and fix errors 2024-11-08 17:06:38 +05:00
Abdullah Atta
3e9d7a70d0 web: localize missing strings 2024-11-08 17:06:38 +05:00
Abdullah Atta
e2ca1a0df6 intl: fix type error in vite.config.mts due to duplicate rollup packages 2024-11-08 17:06:38 +05:00
Abdullah Atta
392456a0d9 global: fix many localization related errors and issues 2024-11-08 17:06:38 +05:00
Abdullah Atta
a5faa4acea web: fix checkout (and make it load faster) 2024-11-08 17:06:17 +05:00
Ammar Ahmed
4014f2a5b5 mobile: support monograph server url 2024-11-08 12:21:07 +05:00
Abdullah Atta
fb62839dad mobile: use mongraph host from core (instead of a static value) 2024-11-08 12:21:07 +05:00
Abdullah Atta
7bf8f1cfc1 web: use mongraph host from core (instead of a static value) 2024-11-08 12:21:07 +05:00
Abdullah Atta
b8a727eee8 monograph: add /api/version endpoint for self hosting support 2024-11-08 12:21:07 +05:00
Abdullah Atta
52e12de296 web: add support for using custom monograph server 2024-11-08 12:21:07 +05:00
Abdullah Atta
0bb2345ccb core: add host for monograph server 2024-11-08 12:21:07 +05:00
Abdullah Atta
78e957eed4 monograph: use public url everywhere instead of monogr.ph 2024-11-08 12:21:07 +05:00
Muhammad Ali
865efaa860 desktop: fix duplicate window controls showing on Linux desktop app (#6844) 2024-11-08 12:16:33 +05:00
01zulfi
2931fd0b3e web: fix copy link button copying link text instead of link (#6642) (#6845)
Signed-off-by: Zulfiqar Ali <85733202+01zulfi@users.noreply.github.com>
2024-11-08 12:13:50 +05:00
Ammar Ahmed
faae68928b mobile: fix localization 2024-11-07 12:23:36 +05:00
01zulfi
21ada0e2d2 web: fix image preview opening as PDF (#6822) (#6843)
Signed-off-by: Zulfiqar Ali <85733202+01zulfi@users.noreply.github.com>
2024-11-07 11:33:57 +05:00
Abdullah Atta
2337b8cd85 web: fix notebook tests 2024-11-06 16:14:39 +05:00
Abdullah Atta
52c8d0b86c desktop: do not use tsc for building desktop app 2024-11-06 16:00:44 +05:00
Abdullah Atta
ce98cea4e7 desktop: update lockfile 2024-11-06 15:42:27 +05:00
Abdullah Atta
b66a093cc2 web: fix missing imports in importer.tsx 2024-11-06 15:38:00 +05:00
Abdullah Atta
a3e5ae4cf5 desktop: use tsup for builds 2024-11-06 15:03:49 +05:00
Abdullah Atta
cddd08c32d desktop: use nodenext module resolution to fix could not resolve errors 2024-11-06 14:51:26 +05:00
Ammar Ahmed
5c3ad96cbe mobile: check item restored successfully 2024-11-06 14:22:30 +05:00
01zulfi
55b441d14e web: fix restore notebooks over limit for free users (#6812)
Signed-off-by: Zulfiqar Ali <85733202+01zulfi@users.noreply.github.com>
2024-11-06 14:22:30 +05:00
Ammar Ahmed
f740a211ce Fix selection scrolls to incorrect position on focus and press enter on mobile (#6838)
* mobile: fix keep-in-view overscrolling

* mobile: fix editor status flickers too much

* mobile: fix setting startingOffset

* mobile: set sticky state if not set already
2024-11-06 13:14:14 +05:00
Ammar Ahmed
3cf00cb0f5 mobile: allow user to cancel 2fa auth 2024-11-06 11:04:08 +05:00
Ammar Ahmed
5659576ce3 mobile: fix vault does not show error toast on incorrect password 2024-11-06 10:33:49 +05:00
Abdullah Atta
55842d44ea chore: set type to Feature for feature requests
Signed-off-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-11-06 08:39:53 +05:00
Abdullah Atta
43238692e4 chore: remove thecodrr as default issue assignee
Signed-off-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-11-06 08:39:02 +05:00
01zulfi
fa1f64f505 web: toc improvements (#4977) (#6818)
1. ToC open/close state is persisted
2. ToC is resizeable
3. ToC no longer covers note contents
4. Increase ToC indentations

Signed-off-by: Zulfiqar Ali <01zulfiqarali@gmail.com>
2024-11-05 16:21:40 +05:00
Abdullah Atta
25e3d56435 intl: restore vaultLocked 2024-11-05 15:43:00 +05:00
01zulfi
8722524c98 web: show vault unlock indication in status bar (#6830)
* web: add vault in status bar
* add vault lock functionality for user
* add new vault EVENTS

Signed-off-by: Zulfiqar Ali <85733202+01zulfi@users.noreply.github.com>
2024-11-05 15:35:02 +05:00
Abdullah Atta
c9b1b42461 desktop: fix Could not resolve "@notesnook/intl/dist/locales/$en.json" error 2024-11-05 13:07:31 +05:00
Ammar Ahmed
fa54f7e7f7 intl: fix localization 2024-11-05 10:14:00 +05:00
Ammar Ahmed
26d1de0620 intl: fix localization 2024-11-04 16:13:02 +05:00
01zulfi
b21548780a web: fix note title not focusing until clicked twice (#6767)
Signed-off-by: Zulfiqar Ali <85733202+01zulfi@users.noreply.github.com>
2024-11-04 11:39:08 +05:00
Ammar Ahmed
c8ba9266a7 intl: fix localization 2024-11-04 10:01:45 +05:00
Abdullah Atta
9e9ef15737 web: add quick checkout (#6811)
* web: add quick checkout

* web: allow changing checkout locale during testing
2024-11-02 12:44:50 +05:00
Muhammad Ali
4d7d2fbf31 web: add confirm password when changing app lock pin/password (#6808) 2024-11-02 12:14:39 +05:00
Abdullah Atta
b7ddae6950 core: fix invalid object-ids generated on desktop 2024-10-28 15:58:45 +05:00
Abdullah Atta
1d60f2132d docs: update analytics script 2024-10-28 08:27:09 +05:00
Abdullah Atta
e9e4a76ac2 ci: publish versioned monograph docker image 2024-10-26 15:47:24 +05:00
Abdullah Atta
4d3a2cc574 monograph: bump version to 1.1.0 2024-10-26 15:44:10 +05:00
Abdullah Atta
c516c8a0ba monograph: fix theme not loading in dev 2024-10-26 15:43:31 +05:00
Abdullah Atta
24d5a0746b monograph: cache storage keys for 5 minutes 2024-10-26 15:43:20 +05:00
Abdullah Atta
dee6c86d15 monograph: add table of contents on mobile 2024-10-26 15:43:03 +05:00
Abdullah Atta
33e64416f3 monograph: fix process is not defined 2024-10-26 15:42:42 +05:00
Abdullah Atta
a74637e9c0 monograph: load i18n for editor 2024-10-26 15:41:55 +05:00
Muhammad Ali
ccd1b6d69b editor: fix editing note link replaces link text with note name (#6768) 2024-10-25 09:27:53 +05:00
Muhammad Ali
3750dd38dc editor: fix some math glyps not rendering correctly (#6762) 2024-10-24 11:47:13 +05:00
Ammar Ahmed
1506cc257d intl: update locale 2024-10-19 14:05:18 +05:00
Ammar Ahmed
fe7c7889e8 mobile: fix file upload checks 2024-10-19 14:05:04 +05:00
Ammar Ahmed
3f54eee11d mobile: fix localization 2024-10-19 14:04:16 +05:00
Ammar Ahmed
11b5922c2c mobile: use pseudo-LOCALE in debug 2024-10-19 11:49:53 +05:00
Ammar Ahmed
71b85d2a4d intl: fix strings 2024-10-19 11:46:50 +05:00
Ammar Ahmed
289832d166 mobile: wrap script manager in try/catch 2024-10-19 11:08:53 +05:00
Ammar Ahmed
a88e612c35 editor: fix table popup ui on mobile 2024-10-17 13:44:16 +05:00
Ammar Ahmed
bda456362b mobile: fix cache dir does not exist error on bulkExist check 2024-10-17 12:02:00 +05:00
Abdullah Atta
f6f644c55d editor: fix queueMicrotask causing hang 2024-10-17 11:13:05 +05:00
Ammar Ahmed
08c92333d6 global: fix discord community url in github issue templates
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-10-17 11:11:01 +05:00
Muhammad Ali
4d72b77f29 web: fix right side panel of settings is not scrollable (#6716) 2024-10-17 11:08:46 +05:00
Ammar Ahmed
4f9d06737f mobile: fix repack not loading async chunks 2024-10-16 16:03:47 +05:00
Ammar Ahmed
bfc494a3d4 mobile: fix copy/share buttons get stuck sometimes with no error thrown (#6708)
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-10-16 16:03:08 +05:00
185 changed files with 9816 additions and 6575 deletions

View File

@@ -1,8 +1,6 @@
name: Bug Report
description: Are you facing a bug or a crash in Notesnook?
labels: ["Type: Bug", "Status: Pending"]
assignees:
- thecodrr
type: Bug
body:
- type: markdown
attributes:

View File

@@ -1,5 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Notesnook Discord Community
url: https://discord.gg/6mHHyncHJE
url: https://go.notesnook.com/discord
about: Reach out to us directly & discuss your issues, suggestions & other feedback!

View File

@@ -1,6 +1,6 @@
name: Feature request
description: Suggest a feature you are missing in Notesnook
labels: ["Type: Feature Request", "Status: Pending"]
type: Feature
body:
- type: markdown
attributes:

120
.github/workflows/android.e2e.yml vendored Normal file
View File

@@ -0,0 +1,120 @@
name: e2e-android
on: workflow_dispatch
jobs:
e2e-android:
runs-on: ubuntu-latest
env:
API_LEVEL: 34
ARCH: x86_64
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Free Disk Space
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: false
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install node modules
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=mobile
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
with:
gradle-version: wrapper
cache-read-only: false
- name: Use specific Java version for the builds
uses: joschi/setup-jdk@v2
with:
java-version: "17"
architecture: "x64"
- name: Install Detox CLI
run: npm install detox-cli --global
- name: Generate debug.keystore
working-directory: apps/mobile/native/android/app
run: |
keytool -genkey -v -keystore debug.keystore -storepass android -alias androiddebugkey -keypass android -keyalg RSA -keysize 2048 -validity 10000 -dname "C=US, O=Android, CN=Android Debug"
- name: Detox build
run: |
yarn build:android
- name: Free Disk Space
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: false
- name: Free Disk Space After Build
run: |
echo "Disk space before cleanup:"
df -h
sudo rm -rf /usr/local/lib/android/sdk/ndk
sudo apt-get clean
echo "Disk space after cleanup:"
df -h
- name: Get device name
id: device
run: |
AVD_NAME=$(node -p "require('./apps/mobile/native/.detoxrc.js').devices.emulator.device.avdName")
echo "AVD_NAME=$AVD_NAME" >> $GITHUB_OUTPUT
- name: AVD cache
uses: actions/cache@v4
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: ${{ steps.device.outputs.AVD_NAME }}
- name: create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ env.API_LEVEL }}
arch: ${{ env.ARCH }}
avd-name: ${{ steps.device.outputs.AVD_NAME }}
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: echo "Generated AVD snapshot for caching."
- name: Detox test
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ env.API_LEVEL }}
arch: ${{ env.ARCH }}
avd-name: ${{ steps.device.outputs.AVD_NAME }}
disable-animations: true
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
force-avd-creation: false
script: |
yarn test:android --headless --record-logs failing --record-videos failing --take-screenshots failing --artifacts-location apps/mobile/native/artifacts
- name: Upload artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: detox-artifacts
path: apps/mobile/native/artifacts
retention-days: 14

View File

@@ -32,6 +32,12 @@ jobs:
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=monograph
- name: Collect package metadata
id: package_metadata
working-directory: ./apps/monograph
run: |
echo ::set-output name=app_version::$(cat package.json | jq -r .version)
- name: Generate build
run: npx nx build @notesnook/monograph
@@ -72,7 +78,7 @@ jobs:
file: apps/monograph/Dockerfile
push: true
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
tags: streetwriters/monograph:latest
tags: streetwriters/monograph:${{ steps.package_metadata.outputs.app_version }},streetwriters/monograph:latest
cache-from: streetwriters/monograph:latest
- name: Generate artifact attestation

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,20 @@
"appAppleId": "1544027013",
"private": true,
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"require": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
}
}
},
"homepage": "https://notesnook.com/",
"repository": "https://github.com/streetwriters/notesnook",
"license": "GPL-3.0-or-later",
@@ -44,7 +57,7 @@
"start": "node scripts/dev.mjs",
"staging": "node scripts/build.mjs --run",
"release": "node scripts/build.mjs",
"build": "tsc",
"build": "tsup-node src/index.ts",
"bundle": "esbuild electron=./src/main.ts ./src/preload.ts --external:electron --external:fsevents --external:better-sqlite3-multiple-ciphers --external:sodium-native --minify --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=false --define:RELEASE=true",
"bundle:mas": "esbuild electron=./src/main.ts ./src/preload.ts --minify --external:electron --external:fsevents --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=true --define:RELEASE=true",
"postinstall": "patch-package"

View File

@@ -63,7 +63,7 @@ if (args.variant === "mas") {
await exec(`yarn run bundle`);
}
await exec(`yarn tsc`);
await exec(`yarn run build`);
if (args.run) {
await exec(`yarn electron-builder --dir --x64`);

View File

@@ -57,7 +57,7 @@ async function onChange(first) {
}
await exec(`yarn run bundle`);
execAsync(`yarn`, [`tsc`]);
await exec(`yarn run build`);
if (await isBundleSame()) {
console.log("Bundle is same. Doing nothing.");

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/>.
*/
export * from "./constants";
export { PATHS } from "./constants";
export type { AppRouter } from "./api";
export { type UpdateInfo } from "builder-util-runtime";
export { type DesktopIntegration } from "./utils/config";

View File

@@ -41,8 +41,8 @@ import { i18n } from "@lingui/core";
const locale =
process.env.NODE_ENV === "development"
? import("@notesnook/intl/dist/locales/$pseudo-LOCALE.json")
: import("@notesnook/intl/dist/locales/$en.json");
? import("@notesnook/intl/locales/$pseudo-LOCALE.json")
: import("@notesnook/intl/locales/$en.json");
locale.then(({ default: locale }) => {
i18n.load({
en: locale.messages as unknown as Messages
@@ -101,7 +101,7 @@ async function createWindow() {
...(config.desktopSettings.nativeTitlebar
? {}
: {
titleBarStyle: "hidden",
titleBarStyle: process.platform === "win32" || process.platform === "darwin" ? "hidden" : "default",
frame: process.platform === "win32" || process.platform === "darwin",
titleBarOverlay: {
height: 37,

View File

@@ -19,22 +19,21 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
/* eslint-disable no-var */
import { ELECTRON_TRPC_CHANNEL } from "electron-trpc/main";
import { type RendererGlobalElectronTRPC } from "electron-trpc/src/types";
// import type { NNCrypto } from "@notesnook/crypto";
import { ipcRenderer } from "electron";
import { platform } from "os";
declare global {
var os: () => "mas" | ReturnType<typeof platform>;
var electronTRPC: RendererGlobalElectronTRPC;
var electronTRPC: any;
// var NativeNNCrypto: (new () => NNCrypto) | undefined;
}
process.once("loaded", async () => {
const electronTRPC: RendererGlobalElectronTRPC = {
sendMessage: (operation) =>
const electronTRPC = {
sendMessage: (operation: any) =>
ipcRenderer.send(ELECTRON_TRPC_CHANNEL, operation),
onMessage: (callback) =>
onMessage: (callback: any) =>
ipcRenderer.on(ELECTRON_TRPC_CHANNEL, (_event, args) => callback(args))
};
globalThis.electronTRPC = electronTRPC;

View File

@@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { NativeImage, nativeImage } from "electron";
import path from "path";
import { isDevelopment } from "./index";
// @ts-expect-error stupid icojs
import { ParsedImage, parseICO } from "icojs";
import { getSystemTheme } from "./theme";
import { readFile } from "fs/promises";

View File

@@ -2,7 +2,9 @@
"extends": "../../tsconfig",
"compilerOptions": {
"outDir": "./dist/",
"lib": ["ESNext"]
"lib": ["ESNext"],
"moduleResolution": "nodenext",
"module": "nodenext"
},
"include": ["src", "global.d.ts"]
}

View File

@@ -39,6 +39,7 @@ DerivedData
*.xcuserstate
*.hprof
**/.xcode.env.local
native/cache
# Android/IntelliJ
#
rn-build-deps/

View File

@@ -140,7 +140,7 @@ export async function downloadAttachments(attachments) {
useAttachmentStore.getState().setDownloading({
current: 0,
total: 1,
message: `${strings.savingZipFile()}... ${strings.pleaseWait()}`,
message: strings.savingZipFile(),
groupId
});
// If all goes well, zip the notesnook-attachments folder in cache.
@@ -150,9 +150,7 @@ export async function downloadAttachments(attachments) {
groupId,
current: progress,
total: 1,
message: `${strings.savingZipFile()} (${(progress * 100).toFixed(
1
)}%)... ${strings.pleaseWait()}`
message: strings.savingZipFile((progress * 100).toFixed(1))
});
});
await zip(zipSourceFolder, zipOutputFile);

View File

@@ -126,9 +126,13 @@ export async function clearFileStorage() {
}
export async function createCacheDir() {
if (!(await RNFetchBlob.fs.exists(cacheDir))) {
await RNFetchBlob.fs.mkdir(cacheDir);
DatabaseLogger.log("Cache directory created");
try {
if (!(await RNFetchBlob.fs.exists(cacheDir))) {
await RNFetchBlob.fs.mkdir(cacheDir);
DatabaseLogger.log("Cache directory created");
}
} catch (e) {
DatabaseLogger.error(e);
}
}
@@ -231,22 +235,30 @@ export async function exists(filename) {
}
export async function bulkExists(files) {
const cacheFiles = await RNFetchBlob.fs.ls(cacheDir);
let missingFiles = files.filter((file) => !cacheFiles.includes(file));
try {
await createCacheDir();
const cacheFiles = await RNFetchBlob.fs.ls(cacheDir);
let missingFiles = files.filter((file) => !cacheFiles.includes(file));
if (Platform.OS === "ios") {
const iosAppGroup =
Platform.OS === "ios"
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupFiles = await RNFetchBlob.fs.ls(iosAppGroup);
missingFiles = missingFiles.filter((file) => !appGroupFiles.includes(file));
if (Platform.OS === "ios") {
const iosAppGroup =
Platform.OS === "ios"
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupFiles = await RNFetchBlob.fs.ls(iosAppGroup);
missingFiles = missingFiles.filter(
(file) => !appGroupFiles.includes(file)
);
}
return missingFiles;
} catch (e) {
DatabaseLogger.error(e);
return [];
}
return missingFiles;
}
export async function getCacheSize() {
await createCacheDir();
const stat = await RNFetchBlob.fs.lstat(`file://` + cacheDir);
let total = 0;
console.log("Total files", stat.length);

View File

@@ -24,7 +24,7 @@ import { useAttachmentStore } from "../../stores/use-attachment-store";
import { IOS_APPGROUPID } from "../../utils/constants";
import { DatabaseLogger, db } from "../database";
import { createCacheDir } from "./io";
import { cacheDir, getUploadedFileSize } from "./utils";
import { cacheDir, checkUpload, getUploadedFileSize } from "./utils";
export async function uploadFile(filename, requestOptions, cancelToken) {
if (!requestOptions) return false;
@@ -33,24 +33,42 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
try {
const uploadedFileSize = await getUploadedFileSize(filename);
if (uploadedFileSize === -1) {
const error = `Uploaded file verification failed. (File hash: ${filename})`;
throw new Error(error);
let filePath = `${cacheDir}/${filename}`;
let exists = await RNFetchBlob.fs.exists(filePath);
// Check for file in appGroupPath if it doesn't exist in cacheDir
if (!exists && Platform.OS === "ios") {
const iosAppGroup =
Platform.OS === "ios"
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
filePath = appGroupPath;
exists = await RNFetchBlob.fs.exists(filePath);
}
if (uploadedFileSize !== 0) {
if (!exists) {
throw new Error(
`Trying to upload file at path ${filePath} that doest not exist.`
);
}
const fileSize = (await RNFetchBlob.fs.stat(filePath)).size;
let remoteFileSize = await getUploadedFileSize(filename);
if (remoteFileSize === -1) return false;
if (remoteFileSize > 0 && remoteFileSize === fileSize) {
DatabaseLogger.log(`File ${filename} is already uploaded.`);
return true;
}
let res = await fetch(url, {
let uploadUrlResponse = await fetch(url, {
method: "PUT",
headers
});
const uploadUrl = res.ok ? await res.text() : await res.json();
const uploadUrl = uploadUrlResponse.ok
? await uploadUrlResponse.text()
: await uploadUrlResponse.json();
if (typeof uploadUrl !== "string") {
throw new Error(
@@ -58,28 +76,9 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
);
}
let uploadFilePath = `${cacheDir}/${filename}`;
const iosAppGroup =
Platform.OS === "ios"
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
let exists = await RNFetchBlob.fs.exists(uploadFilePath);
if (!exists && Platform.OS === "ios") {
uploadFilePath = appGroupPath;
exists = await RNFetchBlob.fs.exists(uploadFilePath);
}
if (!exists) {
throw new Error(
`Trying to upload file at path ${uploadFilePath} that doest not exist.`
);
}
DatabaseLogger.info(`Starting upload: ${filename}`);
let request = RNFetchBlob.config({
let uploadRequest = RNFetchBlob.config({
IOSBackgroundTask: !globalThis["IS_SHARE_EXTENSION"]
})
.fetch(
@@ -88,7 +87,7 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
{
"content-type": ""
},
RNFetchBlob.wrap(uploadFilePath)
RNFetchBlob.wrap(filePath)
)
.uploadProgress((sent, total) => {
useAttachmentStore
@@ -101,30 +100,27 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
cancelToken.cancel = () => {
useAttachmentStore.getState().remove(filename);
request.cancel();
uploadRequest.cancel();
};
let response = await request;
let status = response.info().status;
let text = await response.text();
let result = status >= 200 && status < 300 && text.length === 0;
let uploadResponse = await uploadRequest;
let status = uploadResponse.info().status;
let uploaded = status >= 200 && status < 300;
useAttachmentStore.getState().remove(filename);
if (result) {
DatabaseLogger.info(
`File upload status: ${filename}, ${status}, ${text}`
);
let attachment = await db.attachments.attachment(filename);
if (!attachment) return result;
} else {
const fileInfo = await RNFetchBlob.fs.stat(uploadFilePath);
if (!uploaded) {
const fileInfo = await RNFetchBlob.fs.stat(filePath);
throw new Error(
`${status}, ${text}, name: ${fileInfo.filename}, length: ${
`${status}, name: ${fileInfo.filename}, length: ${
fileInfo.size
}, info: ${JSON.stringify(response.info())}`
}, info: ${JSON.stringify(uploadResponse.info())}`
);
}
return result;
const attachment = await db.attachments.attachment(filename);
await checkUpload(filename, requestOptions.chunkSize, attachment.size);
DatabaseLogger.info(`File upload status: ${filename}, ${status}`);
return uploaded;
} catch (e) {
useAttachmentStore.getState().remove(filename);
ToastManager.error(e, "File upload failed");

View File

@@ -123,3 +123,18 @@ export async function getUploadedFileSize(hash) {
return -1;
}
}
export async function checkUpload(filename, chunkSize, expectedSize) {
const size = await getUploadedFileSize(filename);
const totalChunks = Math.ceil(size / chunkSize);
const decryptedLength = size - totalChunks * ABYTES;
const error =
size === 0
? `File size is 0.`
: size === -1
? `File verification check failed.`
: expectedSize !== decryptedLength
? `File size mismatch. Expected ${size} bytes but got ${decryptedLength} bytes.`
: undefined;
if (error) throw new Error(error);
}

View File

@@ -235,7 +235,7 @@ export const AttachmentDialog = ({
for (let i = 0; i < filteredAttachments.placeholders.length; i++) {
if (!getState().isWorking) {
ToastManager.show({
message: "Attachment recheck cancelled",
message: strings.attachmentRecheckCancelled(),
type: "info",
context: isSheet ? "local" : "global"
});
@@ -322,7 +322,7 @@ export const AttachmentDialog = ({
) : null}
{!isSheet ? (
<Header
title="Manage attachments"
title={strings.manageAttachments()}
renderedInRoute="SettingsGroup"
canGoBack
headerRightButtons={[
@@ -330,23 +330,23 @@ export const AttachmentDialog = ({
onPress() {
onCheck();
},
title: "Recheck all"
title: strings.recheckAll()
},
{
onPress() {
if (!attachments) return;
presentDialog({
title: `Download ${attachments.placeholders.length} attachments`,
paragraph:
"Are you sure you want to download all attachments?",
positiveText: "Download",
title: strings.doActions.download.attachment(
attachments.placeholders.length
),
positiveText: strings.network.download(),
positivePress: async () => {
downloadAttachments(await attachments.ids());
},
negativeText: "Cancel"
negativeText: strings.cancel()
});
},
title: "Download all"
title: strings.downloadAllAttachments()
}
]}
/>
@@ -388,15 +388,14 @@ export const AttachmentDialog = ({
onPress={() => {
if (!attachments) return;
presentDialog({
title: `Download ${attachments.placeholders.length} attachments`,
paragraph:
"Are you sure you want to download all attachments?",
context: "local",
positiveText: "Download",
title: strings.doActions.download.attachment(
attachments.placeholders.length
),
positiveText: strings.network.download(),
positivePress: async () => {
downloadAttachments(await attachments.ids());
},
negativeText: "Cancel"
negativeText: strings.cancel()
});
}}
size={SIZE.lg}
@@ -457,14 +456,16 @@ export const AttachmentDialog = ({
<Paragraph>
{rechecker.isWorking
? note
? `Checking ${rechecker.filter.toLowerCase()} note attachments`
: `Checking ${rechecker.filter.toLowerCase()} attachments`
: "Attachments recheck complete"}
? strings.checkingNoteAttachments()
: strings.checkingAllAttachments()
: strings.attachmentRecheckComplete()}
</Paragraph>
<Paragraph>
{`${rechecker.isWorking ? "Please wait... " : ""}Passed: ${
{`${
rechecker.isWorking ? `${strings.pleaseWait()} ` : ""
}${strings.passed()}: ${
rechecker.passed
}, Failed: ${rechecker.failed}`}
}, ${strings.failed()}: ${rechecker.failed}`}
</Paragraph>
</View>
</View>

View File

@@ -17,8 +17,11 @@ 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, { useEffect, useRef, useState } from "react";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { View } from "react-native";
import { ScrollView } from "react-native-actions-sheet";
import { db } from "../../common/database/index";
import useTimer from "../../hooks/use-timer";
import {
@@ -26,21 +29,16 @@ import {
presentSheet,
ToastManager
} from "../../services/event-manager";
import { useThemeColors } from "@notesnook/theme";
import { eCloseSheet } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Input from "../ui/input";
import { Pressable } from "../ui/pressable";
import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { useCallback } from "react";
import { ScrollView } from "react-native-actions-sheet";
import { strings } from "@notesnook/intl";
const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
const { colors } = useThemeColors();
const code = useRef();
const [currentMethod, setCurrentMethod] = useState({
@@ -138,7 +136,8 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
<View
style={{
alignItems: "center",
paddingHorizontal: currentMethod.method ? 12 : 0
paddingHorizontal: currentMethod.method ? 12 : 0,
gap: 12
}}
>
<IconButton
@@ -167,8 +166,6 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
strings.select2faCodeHelpText()}
</Paragraph>
<Seperator />
{currentMethod.method === "sms" || currentMethod.method === "email" ? (
<Button
onPress={onSendCode}
@@ -187,8 +184,6 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
/>
) : null}
<Seperator />
{currentMethod.method ? (
<>
<Input
@@ -227,7 +222,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
minWidth: "50%"
}}
/>
<Seperator />
<Button
title={loading ? null : strings.next()}
type="accent"
@@ -235,8 +230,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
loading={loading}
onPress={onNext}
style={{
borderRadius: 100,
marginBottom: 10
borderRadius: 100
}}
/>
@@ -248,6 +242,13 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
onPress={onRequestSecondaryMethod}
height={30}
/>
<Button
title={strings.cancel()}
type="plain"
onPress={onCancel}
height={30}
/>
</>
) : (
<>
@@ -298,15 +299,16 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
);
};
TwoFactorVerification.present = (onMfaLogin, data, context) => {
TwoFactorVerification.present = (onMfaLogin, data, onCancel, context) => {
presentSheet({
component: () => (
<TwoFactorVerification onMfaLogin={onMfaLogin} mfaInfo={data} />
<TwoFactorVerification
onMfaLogin={onMfaLogin}
mfaInfo={data}
onCancel={onCancel}
/>
),
context: context || "two_factor_verify",
onClose: () => {
onMfaLogin();
},
disableClosing: true
});
};

View File

@@ -72,31 +72,39 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
const mfaInfo = await db.user.authenticateEmail(email.current);
console.log("email auth", mfaInfo);
if (mfaInfo) {
TwoFactorVerification.present(async (mfa, callback) => {
try {
const success = await db.user.authenticateMultiFactorCode(
mfa.code,
mfa.method
);
TwoFactorVerification.present(
async (mfa, callback) => {
try {
const success = await db.user.authenticateMultiFactorCode(
mfa.code,
mfa.method
);
if (success) {
setStep(LoginSteps.passwordAuth);
setLoading(false);
setTimeout(() => {
passwordInputRef.current?.focus();
}, 500);
callback && callback(true);
}
callback && callback(false);
} catch (e) {
callback && callback(false);
if (e.message === "invalid_grant") {
eSendEvent(eCloseSheet, "two_factor_verify");
setLoading(false);
setStep(LoginSteps.emailAuth);
if (success) {
setStep(LoginSteps.passwordAuth);
setLoading(false);
setTimeout(() => {
passwordInputRef.current?.focus();
}, 500);
callback && callback(true);
}
callback && callback(false);
} catch (e) {
callback && callback(false);
if (e.message === "invalid_grant") {
eSendEvent(eCloseSheet, "two_factor_verify");
setLoading(false);
setStep(LoginSteps.emailAuth);
}
}
},
mfaInfo,
() => {
eSendEvent(eCloseSheet, "two_factor_verify");
setLoading(false);
setStep(LoginSteps.emailAuth);
}
}, mfaInfo);
);
} else {
finishWithError(new Error(strings.unableToSend2faCode()));
}

View File

@@ -311,8 +311,10 @@ export class VaultDialog extends Component {
loading: true
});
try {
let verified = await db.user.verifyPassword(this.password);
if (!(await db.user.getUser())) verified = true;
let verified = true;
if (await db.user.getUser()) {
verified = await db.user.verifyPassword(this.password);
}
if (verified) {
let noteIds = [];
if (this.state.deleteAll) {
@@ -320,20 +322,21 @@ export class VaultDialog extends Component {
const relations = await db.relations.from(vault, "note").get();
noteIds = relations.map((item) => item.toId);
}
await db.vault.delete(this.state.deleteAll);
noteIds.forEach((id) => {
eSendEvent(
eUpdateNoteInEditor,
{
id: id,
deleted: true
},
true
);
});
if (this.state.deleteAll) {
noteIds.forEach((id) => {
eSendEvent(
eUpdateNoteInEditor,
{
id: id,
deleted: true
},
true
);
});
}
console.log("VAULT UPDATED EVENT");
eSendEvent("vaultUpdated");
this.setState({
loading: false
@@ -517,7 +520,6 @@ export class VaultDialog extends Component {
});
this.close();
} else {
eSendEvent("vaultUpdated");
ToastManager.show({
heading: strings.vaultCreated(),
type: "success",
@@ -525,6 +527,7 @@ export class VaultDialog extends Component {
});
this.close();
}
eSendEvent("vaultUpdated");
}
_permanantUnlock() {
@@ -540,6 +543,7 @@ export class VaultDialog extends Component {
this.close();
})
.catch((e) => {
console.log("Error", e);
this._takeErrorAction(e);
});
}
@@ -580,24 +584,17 @@ export class VaultDialog extends Component {
}
_takeErrorAction(e) {
if (
e.message === db.vault.ERRORS.wrongPassword ||
e.message === "FAILURE"
) {
this.setState({
wrongPassword: true,
visible: true
this.setState({
wrongPassword: true,
visible: true
});
setTimeout(() => {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
setTimeout(() => {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
}, 500);
return;
}
}, 500);
}
_revokeFingerprintAccess = async () => {
@@ -876,9 +873,9 @@ export class VaultDialog extends Component {
}
positiveTitle={
deleteVault
? strings.deleteVault()
? strings.delete()
: clearVault
? strings.clearVault()
? strings.clear()
: fingerprintAccess
? strings.enable()
: this.state.revokeFingerprintAccess

View File

@@ -33,7 +33,6 @@ import { eScrollEvent } from "../../utils/events";
import { LeftMenus } from "./left-menus";
import { RightMenus } from "./right-menus";
import { Title } from "./title";
import { useNavigation } from "@react-navigation/native";
type HeaderRightButton = {
title: string;

View File

@@ -17,30 +17,23 @@ 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 { useThemeColors } from "@notesnook/theme";
import React from "react";
import {
Linking,
ScrollView,
TouchableOpacity,
useWindowDimensions,
View
} from "react-native";
import { Linking, ScrollView, useWindowDimensions, View } from "react-native";
import { SwiperFlatList } from "react-native-swiper-flatlist";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { getElevationStyle } from "../../utils/elevation";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { eSendEvent } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import SettingsService from "../../services/settings";
import { useSettingStore } from "../../stores/use-setting-store";
import { getElevationStyle } from "../../utils/elevation";
import { eOpenLoginDialog } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { AuthMode } from "../auth";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import Navigation from "../../services/navigation";
import { eOpenLoginDialog } from "../../utils/events";
import { AuthMode } from "../auth";
import { eSendEvent } from "../../services/event-manager";
import { strings } from "@notesnook/intl";
const Intro = ({ navigation }) => {
const { colors } = useThemeColors();

View File

@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { GroupHeader, GroupOptions, ItemType } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
@@ -26,13 +27,11 @@ import { presentSheet } from "../../../services/event-manager";
import SettingsService from "../../../services/settings";
import { RouteName } from "../../../stores/use-navigation-store";
import { getContainerBorder } from "../../../utils/colors";
import { GROUP } from "../../../utils/constants";
import { SIZE } from "../../../utils/size";
import Sort from "../../sheets/sort";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import Heading from "../../ui/typography/heading";
import { strings } from "@notesnook/intl";
type SectionHeaderProps = {
item: GroupHeader;
@@ -58,17 +57,14 @@ export const SectionHeader = React.memo<
}: SectionHeaderProps) {
const { colors } = useThemeColors();
const { fontScale } = useWindowDimensions();
let groupBy = Object.keys(GROUP).find(
(key) => GROUP[key as keyof typeof GROUP] === groupOptions.groupBy
);
const groupBy =
strings.groupByStrings[
groupOptions.groupBy as keyof typeof strings.groupByStrings
]?.();
const isCompactModeEnabled = useIsCompactModeEnabled(
dataType as "note" | "notebook"
);
groupBy = !groupBy
? "Default"
: groupBy.slice(0, 1).toUpperCase() + groupBy.slice(1, groupBy.length);
return (
<View
style={{
@@ -118,12 +114,10 @@ export const SectionHeader = React.memo<
<>
<Button
onPress={() => {
console.log("Opening Sort sheet", screen, dataType);
presentSheet({
component: <Sort screen={screen} type={dataType} />
});
}}
tooltipText="Change sorting of items in list"
title={groupBy}
icon={
groupOptions.sortDirection === "asc"
@@ -157,11 +151,6 @@ export const SectionHeader = React.memo<
screen !== "Notes"
}
testID="icon-compact-mode"
tooltipText={
isCompactModeEnabled
? "Switch to normal mode"
: "Switch to compact mode"
}
color={colors.secondary.icon}
name={isCompactModeEnabled ? "view-list" : "view-list-outline"}
onPress={() => {

View File

@@ -41,7 +41,7 @@ export const openNotebook = (item: Notebook | BaseTrashItem<Notebook>) => {
positiveText: strings.restore(),
negativeText: strings.delete(),
positivePress: async () => {
await db.trash.restore(item.id);
if ((await db.trash.restore(item.id)) === false) return;
Navigation.queueRoutesForUpdate();
useSelectionStore.getState().setSelectionMode(undefined);
ToastManager.show({

View File

@@ -34,12 +34,13 @@ import {
Reminder,
Tag,
TrashItem,
VirtualizedGrouping
VirtualizedGrouping,
getSortValue
} from "@notesnook/core";
import { getSortValue } from "@notesnook/core";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { View } from "react-native";
import { db } from "../../common/database";
import { useIsCompactModeEnabled } from "../../hooks/use-is-compact-mode-enabled";
import { eSendEvent } from "../../services/event-manager";
import { RouteName } from "../../stores/use-navigation-store";
import { eOpenJumpToDialog } from "../../utils/events";
@@ -48,7 +49,6 @@ import { NoteWrapper } from "../list-items/note/wrapper";
import { NotebookWrapper } from "../list-items/notebook/wrapper";
import ReminderItem from "../list-items/reminder";
import TagItem from "../list-items/tag";
import { useIsCompactModeEnabled } from "../../hooks/use-is-compact-mode-enabled";
type ListItemWrapperProps<TItem = Item> = {
group?: GroupingKey;

View File

@@ -48,7 +48,7 @@ export default function NotePreview({ session, content, note }) {
async function restore() {
if (note && note.type === "trash") {
await db.trash.restore(note.id);
if ((await db.trash.restore(note.id)) === false) return;
Navigation.queueRoutesForUpdate();
useSelectionStore.getState().setSelectionMode(false);
ToastManager.show({

View File

@@ -90,7 +90,7 @@ export const SelectionHeader = React.memo(
const restoreItem = async () => {
if (!selectedItemsList.length) return;
await db.trash.restore(...selectedItemsList);
if ((await db.trash.restore(...selectedItemsList)) === false) return;
Navigation.queueRoutesForUpdate();
clearSelection();
@@ -102,9 +102,8 @@ export const SelectionHeader = React.memo(
const deleteItem = async () => {
presentDialog({
title: strings.deleteItems(type as string, selectedItemsList.length),
paragraph: strings.deleteItemsConfirmation(
type as string,
title: strings.doActions.delete[type](selectedItemsList.length),
paragraph: strings.actionConfirmations.delete[type](
selectedItemsList.length
),
positiveText: strings.delete(),

View File

@@ -81,7 +81,7 @@ export const AddNotebookSheet = ({
id: notebook?.id
});
if (parentNotebook) {
if (parentNotebook && id) {
await db.relations.add(parentNotebook, {
type: "notebook",
id: id
@@ -94,7 +94,8 @@ export const AddNotebookSheet = ({
useNotebookStore.getState().refresh();
const parent =
parentNotebook?.id || (await getParentNotebookId(notebook?.id || id));
parentNotebook?.id ||
(await getParentNotebookId(notebook?.id || (id as string)));
eSendEvent(eOnNotebookUpdated, parent);
if (notebook) {
@@ -104,7 +105,7 @@ export const AddNotebookSheet = ({
});
}
if (!notebook && showMoveNotesOnComplete) {
if (!notebook && showMoveNotesOnComplete && id) {
MoveNotes.present(await db.notebooks.notebook(id));
} else {
close?.(true);

View File

@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Note } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, {
RefObject,
@@ -49,13 +50,11 @@ import DialogHeader from "../../dialog/dialog-header";
import SheetProvider from "../../sheet-provider";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import Input from "../../ui/input";
import Paragraph from "../../ui/typography/paragraph";
import { AddNotebookSheet } from "../add-notebook";
import { NotebookItem } from "./notebook-item";
import { useNotebookItemSelectionStore } from "./store";
import { AddNotebookSheet } from "../add-notebook";
import Input from "../../ui/input";
import { presentDialog } from "../../dialog/functions";
import { strings } from "@notesnook/intl";
async function updateInitialSelectionState(items: string[]) {
const relations = await db.relations

View File

@@ -171,7 +171,7 @@ const ExportNotesSheet = ({
>
<DialogHeader
icon="export"
title={`${strings.export()} ${strings.notes(ids.length)}`}
title={strings.exportNotes(ids.length)}
/>
</View>

View File

@@ -43,7 +43,7 @@ const NewFeature = ({
version
}: {
features: FeatureType[];
version?: string | null;
version?: string;
}) => {
const { colors } = useThemeColors();
@@ -57,7 +57,7 @@ const NewFeature = ({
}}
>
<Heading color={colors.secondary.heading} size={SIZE.md}>
{!version ? `${strings.newVersion()} ` : `v${version} `}Highlights 🎉
{strings.newVersionHighlights(version)}
</Heading>
<Seperator />
@@ -122,7 +122,7 @@ NewFeature.present = () => {
component: (
<NewFeature
features={features}
version={SettingsService.getProperty("version")}
version={SettingsService.getProperty("version") || undefined}
/>
),
disableClosing: true

View File

@@ -206,7 +206,7 @@ export const NotebookSheet = () => {
backgroundInteractionEnabled
gestureEnabled
>
<View
{/* <View
style={{
position: "absolute",
right: 24 + normalize(50),
@@ -214,17 +214,11 @@ export const NotebookSheet = () => {
}}
>
<Pressable
testID={notesnook.buttons.add}
testID="add-notebook-button"
type="secondary"
onPress={() => {
if (!notebook) return;
AddNotebookSheet.present(
undefined,
notebook,
undefined,
undefined,
false
);
}}
style={{
borderRadius: 100
@@ -245,7 +239,7 @@ export const NotebookSheet = () => {
/>
</View>
</Pressable>
</View>
</View> */}
<View
style={{
@@ -404,6 +398,27 @@ export const NotebookSheet = () => {
height: 40 * fontScale
}}
/>
<IconButton
testID="add-notebook-button"
name="notebook-plus"
onPress={() => {
if (!notebook) return;
AddNotebookSheet.present(
undefined,
notebook,
undefined,
undefined,
false
);
}}
color={colors.primary.icon}
size={22}
style={{
width: 40 * fontScale,
height: 40 * fontScale
}}
/>
</>
)}
</View>

View File

@@ -36,7 +36,7 @@ import Seperator from "../../ui/seperator";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { requestInAppReview } from "../../../services/app-review";
import { Note } from "@notesnook/core";
import { hosts, Note } from "@notesnook/core";
import { strings } from "@notesnook/intl";
const PublishNoteSheet = ({
@@ -54,7 +54,7 @@ const PublishNoteSheet = ({
const [note, setNote] = useState<Note | undefined>(item);
const [publishing, setPublishing] = useState(false);
const publishUrl =
note && `https://monogr.ph/${db.monographs.monograph(note?.id)}`;
note && `${hosts.MONOGRAPH_HOST}/${db.monographs.monograph(note?.id)}`;
const isPublished = note && db.monographs.isPublished(note?.id);
const pwdInput = useRef(null);
const passwordValue = useRef<string>();
@@ -121,8 +121,8 @@ const PublishNoteSheet = ({
}}
>
<DialogHeader
title={`${strings.publish()} ${strings.note()}`}
paragraph={strings.publishDesc()}
title={strings.publishNote()}
paragraph={strings.publishNoteDesc()}
/>
{publishing ? (

View File

@@ -16,8 +16,8 @@ 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 { VirtualizedGrouping } from "@notesnook/core";
import { Item, ItemReference } from "@notesnook/core";
import { Item, ItemReference, VirtualizedGrouping } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { RefObject, useEffect, useState } from "react";
import { View } from "react-native";
@@ -37,8 +37,6 @@ import SheetProvider from "../../sheet-provider";
import { Button } from "../../ui/button";
import { PressableProps } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph";
import { isStringLiteralOrJsxExpression } from "typescript";
import { strings } from "@notesnook/intl";
type RelationsListProps = {
actionSheetRef: RefObject<ActionSheetRef>;
@@ -125,7 +123,9 @@ export const RelationsList = ({
// width="100%"
type="inverted"
icon="plus"
title={strings.addItem(referenceType)}
title={strings.addItem(
referenceType as "notebook" | "tag" | "reminder" | "note"
)}
/>
</View>
) : (

View File

@@ -279,9 +279,9 @@ export default function ReminderSheet({
{Object.keys(ReminderModes).map((mode) => (
<Button
key={mode}
title={strings.reminderModes[
mode as keyof typeof strings.reminderModes
]()}
title={strings.reminderModes(
ReminderModes[mode as keyof typeof ReminderModes] as string
)}
style={{
marginRight: 12,
borderRadius: 100,
@@ -337,9 +337,9 @@ export default function ReminderSheet({
{Object.keys(RecurringModes).map((mode) => (
<Button
key={mode}
title={strings.recurringModes[
mode as keyof typeof strings.recurringModes
]()}
title={strings.recurringModes(
RecurringModes[mode as keyof typeof RecurringModes]
)}
style={{
marginRight: 6,
borderRadius: 100

View File

@@ -70,7 +70,6 @@ interface InputProps extends TextInputProps {
};
buttons?: React.ReactNode;
onBlurInput?: () => void;
onPress?: () => void;
height?: number;
fontSize?: number;
onFocusInput?: () => void;

View File

@@ -17,6 +17,9 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Notebook } 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";
@@ -26,26 +29,22 @@ import {
SUPPORT_SVG,
WELCOME_SVG
} from "../../assets/images/assets";
import { ThemeStore } from "../../stores/use-theme-store";
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 { SIZE } from "../../utils/size";
import useRotator from "../../hooks/use-rotator";
import { AccentColorPicker } from "../../screens/settings/appearance";
import { Button } from "../ui/button";
import { SvgView } from "../ui/svg";
import { PinItem } from "../side-menu/pinned-section";
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 { useThemeColors } from "@notesnook/theme";
import { getContainerBorder } from "../../utils/colors";
import { strings } from "@notesnook/intl";
export type TStep = {
text?: string;
walkthroughItem: (colors: ThemeStore["colors"]) => React.ReactNode;
walkthroughItem: (colors: any) => React.ReactNode;
title?: string;
button?: {
type: "next" | "done";
@@ -62,18 +61,18 @@ const NotebookWelcome = () => {
const { colors } = useThemeColors();
const data = useRotator([
{
title: "Work and office",
description: "Everything related to my job",
title: strings.workAndOffice(),
description: strings.workAndOfficeDesc(),
count: 2
},
{
title: "School work",
description: "I don't like doing this but I have to.",
title: strings.schoolWork(),
description: strings.schoolWorkDesc(),
count: 5
},
{
title: "Recipes",
description: "I love cooking and collecting recipes",
title: strings.recipes(),
description: strings.recipesDesc(),
count: 10
}
]);
@@ -122,17 +121,17 @@ const notebooks: { id: string; steps: TStep[] } = {
id: "notebooks",
steps: [
{
title: "Notebooks",
text: "Boost your productivity with Notebooks and organize your notes.",
title: strings.notebooks(),
text: strings.boostProductivityNotebook(),
walkthroughItem: () => <NotebookWelcome />,
button: {
type: "next",
title: "Next"
title: strings.next()
}
},
{
title: "Notebook > Notes",
text: "Every Notebook can have notes and sub notebooks.",
title: strings.notebookNotes(),
text: strings.notebookNotesDesc(),
walkthroughItem: (colors: any) => (
<View
style={{
@@ -154,9 +153,9 @@ const notebooks: { id: string; steps: TStep[] } = {
}}
>
<Heading size={SIZE.md} color={colors.primary.heading}>
Work and office
{strings.workAndOffice()}
</Heading>
<Paragraph>Everything related to my job in one place.</Paragraph>
<Paragraph>{strings.workAndOfficeDesc()}</Paragraph>
<Paragraph
style={{
@@ -165,7 +164,7 @@ const notebooks: { id: string; steps: TStep[] } = {
size={SIZE.xs}
color={colors.secondary.paragraph}
>
Notebook - 2 notes
{strings.notes(2)}
</Paragraph>
</View>
<View
@@ -184,7 +183,7 @@ const notebooks: { id: string; steps: TStep[] } = {
size={SIZE.sm}
name="bookmark"
/>{" "}
Tasks
{strings.tasks()}
</Paragraph>
</View>
<View
@@ -200,7 +199,7 @@ const notebooks: { id: string; steps: TStep[] } = {
>
<Paragraph size={SIZE.xs}>
<Icon color={colors.primary.icon} size={SIZE.sm} name="note" />{" "}
February 2022 Week 2
{strings.taskAValue()}
</Paragraph>
</View>
<View
@@ -216,7 +215,7 @@ const notebooks: { id: string; steps: TStep[] } = {
>
<Paragraph size={SIZE.xs}>
<Icon color={colors.primary.icon} size={SIZE.sm} name="note" />{" "}
February 2022 Week 1
{strings.taskBValue()}
</Paragraph>
</View>
<View
@@ -235,19 +234,19 @@ const notebooks: { id: string; steps: TStep[] } = {
size={SIZE.sm}
name="bookmark"
/>{" "}
Meetings
{strings.meetings()}
</Paragraph>
</View>
</View>
),
button: {
type: "next",
title: "Next"
title: strings.next()
}
},
{
title: "Easy access",
text: "You can create shortcuts of frequently accessed notebooks in the side menu",
title: strings.easyAccess(),
text: strings.easyAccessDesc(),
walkthroughItem: () => (
<View
style={{
@@ -256,29 +255,31 @@ const notebooks: { id: string; steps: TStep[] } = {
}}
>
<PinItem
index={0}
placeholder={true}
item={{
title: "Tasks",
type: "topic"
}}
isPlaceholder={true}
item={
{
title: strings.tasks(),
type: "notebook"
} as Notebook
}
onPress={() => {}}
/>
<PinItem
index={1}
placeholder={true}
item={{
title: "Work and office",
type: "notebook"
}}
isPlaceholder={true}
item={
{
title: strings.workAndOffice(),
type: "notebook"
} as Notebook
}
onPress={() => {}}
/>
</View>
),
button: {
type: "done",
title: "Add your first notebook",
title: strings.addFirstNotebook(),
action: () => {
eSendEvent(eOpenAddNotebookDialog);
}
@@ -287,67 +288,32 @@ const notebooks: { id: string; steps: TStep[] } = {
]
};
const ChooseTheme = () => {
return (
<View
style={{
alignItems: "center",
marginTop: 20
}}
>
<Heading>Make yourself at home</Heading>
<Paragraph
style={{
textAlign: "center",
alignSelf: "center",
maxWidth: "80%"
}}
size={SIZE.md}
>
Pick a theme of your choice
</Paragraph>
<Seperator />
<AccentColorPicker />
<Seperator />
</View>
);
};
const trialstarted: { id: string; steps: TStep[] } = {
id: "trialstarted",
steps: [
{
title: "Your trial is activated",
text: "You can use all premium features for free for the next 14 days",
title: strings.trialStarted(),
text: strings.trialStartedDesc(),
walkthroughItem: (colors) => (
<SvgView src={LAUNCH_ROCKET(colors.primary.paragraph)} />
),
button: {
type: "next",
title: "Next"
}
},
{
walkthroughItem: () => <ChooseTheme />,
button: {
type: "next",
title: "Next"
title: strings.next()
}
},
{
title: "Join the cause",
text: "Meet other privacy-minded people and talk to us directly about your concerns, issues and suggestions.",
title: strings.joinTheCause(),
text: strings.meetPrivacyMinded(),
walkthroughItem: (colors) => (
<SvgView src={COMMUNITY_SVG(colors.primary.paragraph)} />
),
button: {
type: "done",
title: "Continue"
title: strings.continue()
},
actionButton: {
text: "Join Discord Community",
text: strings.joinDiscord(),
action: () => {
Linking.openURL("https://discord.gg/zQBK97EE22").catch(console.log);
}
@@ -360,14 +326,14 @@ const emailconfirmed: { id: string; steps: TStep[] } = {
id: "emailconfirmed",
steps: [
{
title: "Email confirmed",
text: "Your email was confirmed successfully. Thank you for choosing end-to-end encrypted note taking.",
title: strings.emailConfirmed(),
text: strings.emailNotConfirmedDesc(),
walkthroughItem: (colors) => (
<SvgView src={WELCOME_SVG(colors.primary.paragraph)} />
),
button: {
type: "done",
title: "Continue"
title: strings.continue()
}
}
]
@@ -382,15 +348,14 @@ const Support = () => {
}}
>
<SvgView src={SUPPORT_SVG()} />
<Heading>Get Priority Support</Heading>
<Heading>{strings.prioritySupport()}</Heading>
<Paragraph
style={{
textAlign: "center"
}}
size={SIZE.md}
>
You can reach out to us via multiple channels if you face an issue or
want to just talk.
{strings.weAreAlwaysListening()}
</Paragraph>
<Seperator />
@@ -405,7 +370,7 @@ const Support = () => {
}}
icon="discord"
type="secondary"
title="Join our community on Discord"
title={strings.joinDiscord()}
/>
<Button
@@ -419,7 +384,7 @@ const Support = () => {
}}
icon="telegram"
type="secondary"
title="Join our Telegram group"
title={strings.joinTelegram()}
/>
<Button
style={{
@@ -429,7 +394,7 @@ const Support = () => {
}}
icon="bug"
type="secondary"
title="Submit an issue from Settings"
title={strings.reportAnIssue()}
/>
<Button
style={{
@@ -439,7 +404,7 @@ const Support = () => {
}}
icon="mail"
type="secondary"
title="Email us at support@streetwriters.co"
title={strings.emailSupport()}
/>
</View>
);
@@ -449,21 +414,21 @@ const prouser: { id: string; steps: TStep[] } = {
id: "prouser",
steps: [
{
title: "Welcome to Notesnook Pro",
text: "Thank you for reaffirming our idea that privacy comes first",
title: strings.welcomeToNotesnookPro(),
text: strings.thankYouPrivacy(),
walkthroughItem: (colors) => (
<SvgView src={LAUNCH_ROCKET(colors.primary.paragraph)} />
),
button: {
type: "next",
title: "Next"
title: strings.next()
}
},
{
walkthroughItem: () => <Support />,
button: {
type: "done",
title: "Continue"
title: strings.continue()
}
}
]

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/>.
*/
/* eslint-disable no-inner-declarations */
import { VAULT_ERRORS } from "@notesnook/core";
import {
Color,
ItemReference,
@@ -25,22 +24,27 @@ import {
Notebook,
Reminder,
Tag,
TrashItem
TrashItem,
VAULT_ERRORS,
createInternalLink
} from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { DisplayedNotification } from "@notifee/react-native";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { InteractionManager, Platform } from "react-native";
import Share from "react-native-share";
import { db } from "../common/database";
import { DatabaseLogger, db } from "../common/database";
import { AttachmentDialog } from "../components/attachments";
import { presentDialog } from "../components/dialog/functions";
import NoteHistory from "../components/note-history";
import { AddNotebookSheet } from "../components/sheets/add-notebook";
import MoveNoteSheet from "../components/sheets/add-to";
import ExportNotesSheet from "../components/sheets/export-notes";
import { MoveNotebookSheet } from "../components/sheets/move-notebook";
import { MoveNotes } from "../components/sheets/move-notes/movenote";
import PublishNoteSheet from "../components/sheets/publish-note";
import { ReferencesList } from "../components/sheets/references";
import { RelationsList } from "../components/sheets/relations-list/index";
import ReminderSheet from "../components/sheets/reminder";
import { useSideBarDraggingStore } from "../components/side-menu/dragging-store";
@@ -60,15 +64,10 @@ import { useRelationStore } from "../stores/use-relation-store";
import { useSelectionStore } from "../stores/use-selection-store";
import { useTagStore } from "../stores/use-tag-store";
import { useUserStore } from "../stores/use-user-store";
import Errors from "../utils/errors";
import { eOpenLoginDialog, eUpdateNoteInEditor } from "../utils/events";
import { deleteItems } from "../utils/functions";
import { convertNoteToText } from "../utils/note-to-text";
import { sleep } from "../utils/time";
import { ReferencesList } from "../components/sheets/references";
import { createInternalLink } from "@notesnook/core";
import { MoveNotebookSheet } from "../components/sheets/move-notebook";
import { strings } from "@notesnook/intl";
export const useActions = ({
close,
@@ -141,7 +140,7 @@ export const useActions = ({
async function restoreTrashItem() {
if (!checkItemSynced()) return;
close();
await db.trash.restore(item.id);
if ((await db.trash.restore(item.id)) === false) return;
Navigation.queueRoutesForUpdate();
const type = item.type === "trash" ? item.itemType : item.type;
ToastManager.show({
@@ -255,8 +254,8 @@ export const useActions = ({
item.type === "color"
) {
presentDialog({
title: strings.deleteItem(item.type),
paragraph: strings.deleteItemConfirmation(item.type),
title: strings.doActions.delete[item.type](1),
paragraph: strings.actionConfirmations.delete[item.type](1),
positivePress: async () => {
if (item.type === "reminder") {
await db.reminders.remove(item.id);
@@ -304,8 +303,8 @@ export const useActions = ({
close();
await sleep(300);
presentDialog({
title: strings.delete(),
paragraph: strings.deleteItemsConfirmation(item.itemType, 1),
title: strings.doActions.delete[item.itemType](1),
paragraph: strings.actionConfirmations.delete[item.itemType](1),
positiveText: strings.delete(),
negativeText: strings.cancel(),
positivePress: async () => {
@@ -314,7 +313,10 @@ export const useActions = ({
Navigation.queueRoutesForUpdate();
useSelectionStore.getState().setSelectionMode(undefined);
ToastManager.show({
heading: strings.itemDeleted(1, item.itemType),
heading:
strings.actions.deleted[
item.itemType as keyof typeof strings.actions.deleted
](1),
type: "success",
context: "local"
});
@@ -650,35 +652,40 @@ export const useActions = ({
}
async function shareNote() {
if (item.type !== "note") return;
if (processingId.current === "shareNote") {
ToastManager.show({
heading: strings.pleaseWait() + "...",
context: "local"
});
return;
}
if (!checkItemSynced()) return;
if (locked) {
close();
await sleep(300);
openVault({
item: item,
novault: true,
locked: true,
share: true,
title: strings.shareNote()
});
} else {
processingId.current = "shareNote";
const convertedText = await convertNoteToText(item);
try {
if (item.type !== "note") return;
if (processingId.current === "shareNote") {
ToastManager.show({
heading: strings.pleaseWait() + "...",
context: "local"
});
return;
}
if (!checkItemSynced()) return;
if (locked) {
close();
await sleep(300);
openVault({
item: item,
novault: true,
locked: true,
share: true,
title: strings.shareNote()
});
} else {
processingId.current = "shareNote";
const convertedText = await convertNoteToText(item);
processingId.current = undefined;
Share.open({
title: strings.shareNote(),
failOnCancel: false,
message: convertedText || ""
});
}
} catch (e) {
ToastManager.error(e as Error);
DatabaseLogger.error(e);
processingId.current = undefined;
Share.open({
title: strings.shareNote(),
failOnCancel: false,
message: convertedText || ""
});
}
}
@@ -761,7 +768,8 @@ export const useActions = ({
});
}
} catch (e) {
console.error(e);
processingId.current = undefined;
DatabaseLogger.error(e);
ToastManager.error(e as Error);
}
}
@@ -769,7 +777,7 @@ export const useActions = ({
actions.push(
{
id: "favorite",
title: item.favorite ? "Unfav" : "Fav",
title: !item.favorite ? strings.favorite() : strings.unfavorite(),
icon: item.favorite ? "star-off" : "star-outline",
func: addToFavorites,
close: false,
@@ -949,8 +957,12 @@ export const useActions = ({
id: "trash",
title:
item.type !== "notebook" && item.type !== "note"
? "Delete " + item.type
: "Move to trash",
? strings.doActions.delete[
item.type === "trash"
? item.itemType
: (item.type as keyof typeof strings.doActions.delete)
](1)
: strings.moveToTrash(),
icon: "delete-outline",
type: "error",
func: deleteItem

View File

@@ -18,11 +18,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useCallback, useEffect } from "react";
import { db } from "../common/database";
import BiometricService from "../services/biometrics";
import { eSubscribeEvent, eUnSubscribeEvent } from "../services/event-manager";
import { db } from "../common/database";
const VaultStatusCache = {
const VaultStatusDefaults = {
exists: false,
biometryEnrolled: false,
isBiometryAvailable: false
@@ -35,18 +35,12 @@ export type VaultStatusType = {
};
export const useVaultStatus = () => {
const [vaultStatus, setVaultStatus] = React.useState(VaultStatusCache);
const [vaultStatus, setVaultStatus] = React.useState(VaultStatusDefaults);
const checkVaultStatus = useCallback(() => {
db.vault?.exists().then(async (exists) => {
const available = await BiometricService.isBiometryAvailable();
const fingerprint = await BiometricService.hasInternetCredentials();
if (
VaultStatusCache.exists === exists &&
VaultStatusCache.biometryEnrolled === fingerprint &&
VaultStatusCache.isBiometryAvailable === available
)
return;
setVaultStatus({
exists: exists,
biometryEnrolled: fingerprint,

View File

@@ -146,15 +146,16 @@ const Editor = React.memo(
onRenderProcessGone={onError}
nestedScrollEnabled
onError={onError}
injectedJavaScriptBeforeContentLoaded={`
globalThis.LINGUI_LOCALE = "${i18n.locale}";
globalThis.LINGUI_LOCALE_DATA = ${JSON.stringify({
[i18n.locale]: i18n.messages
})};
globalThis.__DEV__ = ${__DEV__}
globalThis.readonly=${readonly};
globalThis.noToolbar=${noToolbar};
globalThis.noHeader=${noHeader};
injectedJavaScript={`
globalThis.__DEV__ = ${__DEV__}
globalThis.readonly=${readonly};
globalThis.noToolbar=${noToolbar};
globalThis.noHeader=${noHeader};
globalThis.LINGUI_LOCALE = "${i18n.locale}";
globalThis.LINGUI_LOCALE_DATA = ${JSON.stringify({
[i18n.locale]: i18n.messages
})};
globalThis.loadApp();
`}
useSharedProcessPool={false}
javaScriptEnabled={true}

View File

@@ -19,19 +19,20 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { getFormattedDate } from "@notesnook/common";
import {
EVENTS,
isEncryptedContent,
NoteContent,
ContentItem,
ContentType,
DeletedItem,
EVENTS,
ItemReference,
Note,
NoteContent,
TrashItem,
UnencryptedContentItem,
isDeleted,
isEncryptedContent,
isTrashItem
} from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeEngineStore } from "@notesnook/theme";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import WebView from "react-native-webview";
@@ -57,6 +58,7 @@ import {
eUpdateNoteInEditor
} from "../../../utils/events";
import { tabBarRef } from "../../../utils/global-refs";
import { sleep } from "../../../utils/time";
import { unlockVault } from "../../../utils/unlock-vault";
import { onNoteCreated } from "../../notes/common";
import Commands from "./commands";
@@ -73,8 +75,6 @@ import {
isEditorLoaded,
post
} from "./utils";
import { sleep } from "../../../utils/time";
import { strings } from "@notesnook/intl";
type NoteWithContent = Note & {
content?: NoteContent<false>;
@@ -282,7 +282,7 @@ export const useEditor = (
if (saved) return;
commands.setStatus(
getFormattedDate(note ? note.dateEdited : Date.now(), "date-time"),
"Saving",
strings.saving(),
tabId
);
}, 50);
@@ -365,7 +365,7 @@ export const useEditor = (
note = (await db.notes?.note(id)) as Note;
await commands.setStatus(
getFormattedDate(note.dateEdited, "date-time"),
"Saved",
strings.saved(),
tabId
);
@@ -574,7 +574,7 @@ export const useEditor = (
await commands.setStatus(
getFormattedDate(item.dateEdited, "date-time"),
"Saved",
strings.saved(),
tabId
);
@@ -695,7 +695,7 @@ export const useEditor = (
if (currentNotes.current[noteId]?.dateEdited !== note.dateEdited) {
commands.setStatus(
getFormattedDate(note.dateEdited, "date-time"),
"Saved",
strings.saved(),
tabId as number
);
}

View File

@@ -76,7 +76,7 @@ export const Search = ({ route, navigation }: NavigationProps<"Search">) => {
await results.item(0);
setResults(results);
if (results.placeholders?.length === 0) {
setSearchStatus(`${strings.noResultsFound(query)}`);
setSearchStatus(strings.noResultsFound(query));
} else {
setSearchStatus(undefined);
}
@@ -125,10 +125,8 @@ export const Search = ({ route, navigation }: NavigationProps<"Search">) => {
loading={loading}
placeholder={{
title: route.name,
paragraph:
searchStatus ||
`${strings.typeAKeywordToSearchIn(route.params?.title)}`,
loading: `${strings.searchingFor(currentQuery.current as string)}...`
paragraph: searchStatus || strings.searchInRoute(route.params?.title),
loading: strings.searchingFor(currentQuery.current as string)
}}
/>
</>

View File

@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { formatBytes, getFormattedDate } from "@notesnook/common";
import { LegacyBackupFile } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import { ActivityIndicator, FlatList, Platform, View } from "react-native";
@@ -46,7 +47,6 @@ import SettingsService from "../../../services/settings";
import { refreshAllStores } from "../../../stores/create-db-collection-store";
import { useUserStore } from "../../../stores/use-user-store";
import { SIZE } from "../../../utils/size";
import { strings } from "@notesnook/intl";
type PasswordOrKey = { password?: string; encryptionKey?: string };
@@ -153,7 +153,7 @@ const restoreBackup = async (options: {
if (path === ".nnbackup" || path === "attachments") continue;
updateProgress({
progress: `${strings.restoringBackup()}... (${count++}/${
progress: `${strings.restoringBackup()} (${count++}/${
extractedBackupFiles.length
})`
});
@@ -242,7 +242,7 @@ const restoreBackup = async (options: {
await db.transaction(async () => {
updateProgress({
progress: strings.restoringBackup() + "..."
progress: strings.restoringBackup()
});
await db.backup.import(backup, {
encryptionKey,
@@ -533,12 +533,11 @@ const BackupItem = ({
}}
onPress={() => {
presentDialog({
title: `Restore ${itemName}`,
paragraph: `Are you sure you want to restore this backup?`,
positiveText: "Restore",
negativeText: "Cancel",
title: `${strings.restore()} ${itemName}`,
paragraph: strings.restoreBackupConfirm(),
positiveText: strings.restore(),
negativeText: strings.cancel(),
positivePress: async () => {
console.log("file path", (item as ScopedStorage.FileType).uri);
restoreBackup({
uri:
Platform.OS === "android"

View File

@@ -32,7 +32,12 @@ import SettingsService from "../../services/settings";
import { HostId, HostIds } from "../../stores/use-setting-store";
import { useUserStore } from "../../stores/use-user-store";
export const ServerIds = ["notesnook-sync", "auth", "sse"] as const;
export const ServerIds = [
"notesnook-sync",
"auth",
"sse",
"monograph"
] as const;
export type ServerId = (typeof ServerIds)[number];
type Server = {
id: ServerId;
@@ -40,6 +45,7 @@ type Server = {
title: string;
example: string;
description: string;
versionEndpoint: string;
};
type VersionResponse = {
version: number;
@@ -52,21 +58,32 @@ const SERVERS: Server[] = [
host: "API_HOST",
title: strings.syncServer(),
example: "http://localhost:4326",
description: strings.syncServerDesc()
description: strings.syncServerDesc(),
versionEndpoint: "/version"
},
{
id: "auth",
host: "AUTH_HOST",
title: strings.authServer(),
example: "http://localhost:5326",
description: strings.authServerDesc()
description: strings.authServerDesc(),
versionEndpoint: "/version"
},
{
id: "sse",
host: "SSE_HOST",
title: strings.sseServer(),
example: "http://localhost:7326",
description: strings.sseServerDesc()
description: strings.sseServerDesc(),
versionEndpoint: "/version"
},
{
id: "monograph",
host: "MONOGRAPH_HOST",
title: strings.monographServer(),
example: "http://localhost:6326",
description: strings.monographServerDesc(),
versionEndpoint: "/api/version"
}
];
export function ServersConfiguration() {
@@ -135,28 +152,31 @@ export function ServersConfiguration() {
type="secondary"
width="100%"
onPress={async () => {
setError(undefined);
try {
for (const host of HostIds) {
const url = urls[host];
const server = SERVERS.find((s) => s.host === host)!;
if (!server) throw new Error(strings.serverNotFound(host));
if (!url) throw new Error(strings.allServerUrlsRequired());
const version = await fetch(`${url}/version`)
const version = await fetch(`${url}${server.versionEndpoint}`)
.then((r) => r.json() as Promise<VersionResponse>)
.catch(() => undefined);
if (!version)
throw new Error(
`${strings.couldNotConnectTo()} ${server.title}.`
`${strings.couldNotConnectTo(server.title)}`
);
if (version.id !== server.id)
throw new Error(
`${strings.incorrectServerUrl(url)} ${server.title}.`
`${strings.incorrectServerUrl(url, server.title)}.`
);
if (!isServerCompatible(version.version)) {
throw new Error(strings.serverVersionMismatch());
throw new Error(
strings.serverVersionMismatch(server.title, url)
);
}
}
setSuccess(true);
setError(undefined);
} catch (e) {
setError((e as Error).message);
}

View File

@@ -333,27 +333,25 @@ export const settingsGroups: SettingSection[] = [
},
{
id: "clear-cache",
name: "Clear cache",
name: strings.clearCache(),
icon: "delete",
modifer: async () => {
presentDialog({
title: "Clear cache",
paragraph: "Are you sure you want to clear the cache?",
positiveText: "Clear",
title: strings.clearCacheConfirm(),
paragraph: strings.clearCacheConfirmDesc(),
positiveText: strings.clear(),
positivePress: async () => {
filesystem.clearCache();
ToastManager.show({
heading: "Cache cleared",
message: "All cached attachments have been removed",
heading: strings.cacheCleared(),
message: strings.cacheClearedDesc(),
type: "success"
});
}
});
},
description(current) {
return `Clear all cached attachments. Current cache size: ${
current as number
}`;
return strings.clearCacheDesc(current as number);
},
useHook: () => {
const [cacheSize, setCacheSize] = React.useState(0);
@@ -380,7 +378,7 @@ export const settingsGroups: SettingSection[] = [
presentDialog({
title: strings.logout(),
paragraph: strings.logoutConfirmation(),
positiveText: "Logout",
positiveText: strings.logout(),
check: {
info: strings.backupDataBeforeLogout(),
defaultValue: true
@@ -444,7 +442,7 @@ export const settingsGroups: SettingSection[] = [
}
updateProgress({
progress: `${strings.loggingOut()}... ${strings.pleaseWait()}`
progress: strings.loggingOut()
});
await db.user?.logout();
@@ -804,8 +802,8 @@ export const settingsGroups: SettingSection[] = [
{
id: "servers",
type: "screen",
name: "Servers",
description: "Configure server URLs for Notesnook",
name: strings.servers(),
description: strings.serversConfigurationDesc(),
icon: "server",
component: "server-config"
}
@@ -897,7 +895,7 @@ export const settingsGroups: SettingSection[] = [
item: {},
clearVault: true,
novault: true,
title: strings.clearVault()
title: strings.clearVault() + "?"
});
}
},
@@ -912,7 +910,7 @@ export const settingsGroups: SettingSection[] = [
item: {},
deleteVault: true,
novault: true,
title: strings.deleteVault()
title: strings.deleteVault() + "?"
});
}
},

View File

@@ -17,8 +17,10 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { strings } from "@notesnook/intl";
import React from "react";
import { Linking, Platform, View } from "react-native";
import Config from "react-native-config";
import { Button } from "../../components/ui/button";
import { usePricing } from "../../hooks/use-pricing";
import {
@@ -28,14 +30,9 @@ import {
} from "../../services/event-manager";
import PremiumService from "../../services/premium";
import { useUserStore } from "../../stores/use-user-store";
import {
SUBSCRIPTION_PROVIDER,
SUBSCRIPTION_STATUS
} from "../../utils/constants";
import { SUBSCRIPTION_STATUS } from "../../utils/constants";
import { eOpenPremiumDialog } from "../../utils/events";
import { SIZE } from "../../utils/size";
import Config from "react-native-config";
import { strings } from "@notesnook/intl";
export const Subscription = () => {
const user = useUserStore((state) => state.user);
const monthlyPlan = usePricing("monthly");

View File

@@ -33,7 +33,7 @@ import {
} from "@notesnook/common";
import { Note } from "@notesnook/core";
import { FilteredSelector } from "@notesnook/core";
import { basename, dirname, join } from "pathe";
import { basename, dirname, join, extname } from "pathe";
import downloadAttachment from "../common/filesystem/download-attachment";
import { cacheDir } from "../common/filesystem/utils";
import { unlockVault } from "../utils/unlock-vault";
@@ -326,10 +326,18 @@ async function createFile(
await copyFileAsync("file://" + exportedFile, file.uri);
filePath = file.uri;
} else {
filePath = join(path, basename(noteItem.path));
const originalPath = join(path, basename(noteItem.path));
filePath = originalPath;
const ext = extname(originalPath);
let id = 1;
while (await RNFetchBlob.fs.exists(filePath)) {
filePath = originalPath.replace(`${ext}`, "") + "_" + id + ext;
id++;
}
console.log("path", filePath);
await RNFetchBlob.fs.mv(exportedFile, filePath);
}
console.log("file moved...");
return {
filePath: filePath,
fileDir: path,

View File

@@ -25,7 +25,12 @@ import { FileType } from "react-native-scoped-storage";
import create, { State } from "zustand";
import { ThemeDark, ThemeLight, ThemeDefinition } from "@notesnook/theme";
import { Reminder } from "@notesnook/core";
export const HostIds = ["API_HOST", "AUTH_HOST", "SSE_HOST"] as const;
export const HostIds = [
"API_HOST",
"AUTH_HOST",
"SSE_HOST",
"MONOGRAPH_HOST"
] as const;
export type HostId = (typeof HostIds)[number];
export type Settings = {

View File

@@ -33,11 +33,9 @@ import { strings } from "@notesnook/intl";
function confirmDeleteAllNotes(items, type, context) {
return new Promise((resolve) => {
presentDialog({
title: `Delete ${
items.length > 1 ? `${items.length} ${type}s` : `${type}`
}?`,
positiveText: "Delete",
negativeText: "Cancel",
title: strings.doActions.delete[type](items.length),
positiveText: strings.delete(),
negativeText: strings.cancel(),
positivePress: (_inputValue, value) => {
setTimeout(() => {
resolve({ delete: true, deleteNotes: value });
@@ -114,10 +112,10 @@ export const deleteItems = async (items, type, context) => {
}
} else if (type === "tag") {
presentDialog({
title: strings.deleteTags(ids.length),
title: strings.doActions.delete.tag(ids.length),
positiveText: strings.delete(),
negativeText: strings.cancel(),
paragraph: strings.deleteTagsConfirm(),
paragraph: strings.actionConfirmations.delete.tag(2),
positivePress: async () => {
await db.tags.remove(...ids);
useTagStore.getState().refresh();
@@ -130,12 +128,12 @@ export const deleteItems = async (items, type, context) => {
let deletedIds = [...ids];
if (type === "notebook" || type === "note") {
let message = strings.movedToTrash(type, ids.length);
let message = strings.actions.movedToTrash[type](ids.length);
ToastManager.show({
heading: message,
type: "success",
func: async () => {
await db.trash.restore(...deletedIds);
if ((await db.trash.restore(...deletedIds)) === false) return;
Navigation.queueRoutesForUpdate();
useMenuStore.getState().setMenuPins();
useMenuStore.getState().setColorNotes();

View File

@@ -34,7 +34,6 @@ export async function unlockVault({
paragraph: string;
}) {
if (unlockPromise) {
console.log("Unlocking.... waiting for unlock promise");
return unlockPromise;
}
unlockPromise = new Promise(async (resolve) => {
@@ -58,10 +57,10 @@ export async function unlockVault({
context: context,
input: true,
secureTextEntry: true,
positiveText: "Unlock",
positiveText: strings.unlock(),
title: title,
paragraph: paragraph,
inputPlaceholder: "Enter password",
inputPlaceholder: strings.enterPassword(),
positivePress: async (value) => {
const unlocked = await db.vault.unlock(value);
if (!unlocked) {

View File

@@ -0,0 +1,29 @@
import { execSync } from "child_process";
import { pathExists, ensureDir } from "fs-extra";
import { resolveConfig } from "detox/internals";
import { globalSetup } from "detox/runners/jest";
export default async function customGlobalSetup() {
const config = await resolveConfig();
if (config.device.type === "android.emulator") {
await downloadTestButlerAPK();
}
await globalSetup();
}
async function downloadTestButlerAPK() {
const version = "2.2.1";
const artifactUrl = `https://repo1.maven.org/maven2/com/linkedin/testbutler/test-butler-app/${version}/test-butler-app-${version}.apk`;
const filePath = `cache/test-butler-app.apk`;
await ensureDir("cache");
if (!(await pathExists(filePath))) {
console.log(`\nDownloading Test-Butler APK v${version}...`);
execSync(`curl -f -o ${filePath} ${artifactUrl}`);
}
}
module.exports = customGlobalSetup;

View File

@@ -1,20 +1,21 @@
/** @type {import('@jest/types').Config.InitialOptions} */
module.exports = {
rootDir: "..",
testMatch: ["<rootDir>/e2e/**/*.e2e.js"],
testTimeout: 120000,
maxWorkers: 1,
setupFilesAfterEnv: ["./e2e/setup.js"],
globalSetup: "detox/runners/jest/globalSetup",
testTimeout: 120000,
globalSetup: "./e2e/globalSetup.ts",
globalTeardown: "detox/runners/jest/globalTeardown",
reporters: ["detox/runners/jest/reporter"],
setupFilesAfterEnv: ["./e2e/setup.ts"],
testEnvironment: "detox/runners/jest/testEnvironment",
verbose: true,
reporters: ["detox/runners/jest/reporter"],
testRunner: "jest-circus/runner",
testMatch: ["<rootDir>/e2e/**/*.e2e.(js|ts)"],
transform: {
"^.+\\.(js|jsx|ts|tsx)$": [
"\\.tsx?$": "ts-jest",
"^.+\\.(js|jsx)$": [
"babel-jest",
{ configFile: "../native/babel.config.js" }
]
},
transformIgnorePatterns: ["<rootDir>/../node_modules/"]
verbose: true
};

View File

@@ -74,7 +74,7 @@ async function login() {
// }
describe("AUTH", () => {
it("Login", async () => {
it.skip("Login", async () => {
await prepare();
await openSideMenu();
await login();

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 { navigate, tapByText, prepare, sleep } from "./utils";
import { Tests } from "./utils";
describe("APP LAUNCH AND NAVIGATION", () => {
it("App should launch successfully & hide welcome screen", async () => {
await prepare();
await Tests.prepare();
});
it("Basic navigation should work", async () => {
await prepare();
await navigate("Notebooks");
await sleep(500);
await navigate("Favorites");
await navigate("Trash");
await navigate("Tags");
await navigate("Settings");
await navigate("Monographs");
await navigate("Reminders");
await navigate("Notes");
await Tests.prepare();
await Tests.navigate("Notebooks");
await Tests.navigate("Favorites");
await Tests.navigate("Trash");
await Tests.navigate("Tags");
await Tests.navigate("Settings");
await Tests.navigate("Monographs");
await Tests.navigate("Reminders");
await Tests.navigate("Notes");
});
});

View File

@@ -1,140 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { notesnook } from "../test.ids";
import {
navigate,
tapById,
visibleByText,
createNote,
prepare,
visibleById,
notVisibleById,
sleep,
exitEditor,
tapByText,
elementByText,
elementById
} from "./utils";
describe("NOTE TESTS", () => {
it("Create a note in editor", async () => {
await prepare();
await createNote();
});
it("Open and close a note", async () => {
await prepare();
await createNote();
await tapById(notesnook.ids.note.get(0));
await exitEditor();
});
it.only("Notes properties should show", async () => {
await prepare();
let note = await createNote();
await tapById(notesnook.listitem.menu);
await waitFor(elementByText("Created at:")).toBeVisible().withTimeout(500);
});
it("Favorite and unfavorite a note", async () => {
await prepare();
let note = await createNote();
await tapById(notesnook.listitem.menu);
await tapById("icon-favorite");
await visibleById("icon-star");
await navigate("Favorites");
await visibleByText(note.body);
await sleep(500);
await tapById(notesnook.listitem.menu);
await tapById("icon-favorite");
await expect(element(by.text(note.body))).not.toBeVisible();
await navigate("Notes");
});
it("Pin a note to top", async () => {
await prepare();
await createNote();
await tapById(notesnook.listitem.menu);
await tapById("icon-pin");
await visibleByText("Pinned");
await visibleById("icon-pinned");
await tapById(notesnook.listitem.menu);
await tapById("icon-pin");
expect(element(by.id("icon-pinned"))).not.toBeVisible();
});
it("Pin a note in notifications", async () => {
await prepare();
await createNote();
await tapById(notesnook.listitem.menu);
await tapById("icon-pin-to-notifications");
await visibleByText("Unpin from notifications");
await sleep(500);
await tapById("icon-pin-to-notifications");
await sleep(500);
await visibleByText("Pin to notifications");
});
it("Copy note", async () => {
await prepare();
await createNote();
await tapById(notesnook.listitem.menu);
await sleep(1000);
await waitFor(elementById("icon-copy")).toBeVisible().withTimeout(500);
await tapById("icon-copy");
});
it("Assign colors to a note", async () => {
await prepare();
let note = await createNote();
await tapById(notesnook.listitem.menu);
await sleep(1000);
await tapByText("Add color");
await sleep(500);
await elementById("color-title-input").typeText("Test color");
await tapByText("Add color");
await sleep(3000);
await visibleById("icon-check");
await tapById("icon-color-#efefef");
await notVisibleById("icon-check");
await tapById("icon-color-#efefef");
await device.pressBack();
await navigate("Test color");
await visibleByText(note.body);
});
it("Delete & restore a note", async () => {
await prepare();
await createNote();
await tapById(notesnook.listitem.menu);
await sleep(500);
await tapById("icon-trash");
await navigate("Trash");
await sleep(500);
await tapById(notesnook.listitem.menu);
await sleep(500);
await tapByText("Restore note");
await device.pressBack();
await sleep(500);
await visibleByText(
"Test note description that is very long and should not fit in text."
);
});
});

View File

@@ -0,0 +1,116 @@
/*
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 { notesnook } from "../test.ids";
import { Tests } from "./utils";
describe("NOTE TESTS", () => {
it("Create a note in editor", async () => {
await Tests.prepare();
await Tests.createNote();
});
it("Open and close a note", async () => {
await Tests.prepare();
await Tests.createNote();
await Tests.fromId(notesnook.ids.note.get(0)).waitAndTap();
await Tests.exitEditor();
});
it("Notes properties should show", async () => {
await Tests.prepare();
await Tests.createNote();
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.fromText("Created at").isVisible();
});
it("Favorite and unfavorite a note", async () => {
await Tests.prepare();
let note = await Tests.createNote();
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.fromId("icon-favorite").waitAndTap();
await Tests.fromId("icon-star").isVisible();
await Tests.navigate("Favorites");
await Tests.fromText(note.body).isVisible();
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.fromId("icon-favorite").waitAndTap();
await Tests.fromText(note.body).isNotVisible();
await Tests.navigate("Notes");
});
it("Pin a note to top", async () => {
await Tests.prepare();
await Tests.createNote();
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.fromId("icon-pin").waitAndTap();
await Tests.fromText("Pinned").isVisible();
await Tests.fromId("icon-pinned").isVisible();
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.fromId("icon-pin").waitAndTap();
await Tests.fromText("icon-pinned").isNotVisible();
});
it("Pin a note in notifications", async () => {
await Tests.prepare();
await Tests.createNote();
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.fromId("icon-pin-to-notifications").waitAndTap();
await Tests.fromText("Unpin from notifications").isVisible();
await Tests.fromId("icon-pin-to-notifications").waitAndTap();
await Tests.fromText("Pin to notifications").isVisible();
});
it("Copy note", async () => {
await Tests.prepare();
await Tests.createNote();
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.fromId("icon-copy").isVisible();
await Tests.fromId("icon-copy").waitAndTap();
});
it("Assign colors to a note", async () => {
await Tests.prepare();
let note = await Tests.createNote();
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.fromText("Add color").waitAndTap();
await Tests.fromId("color-title-input").element.typeText("Test color");
await Tests.fromText("Add color").waitAndTap();
await Tests.fromId("icon-check").isVisible();
await Tests.fromId("icon-color-#efefef").waitAndTap();
await Tests.fromId("icon-check").isNotVisible();
await Tests.fromId("icon-color-#efefef").waitAndTap();
await device.pressBack();
await Tests.navigate("Test color");
await Tests.fromText(note.body).isVisible();
});
it.only("Delete & restore a note", async () => {
await Tests.prepare();
await Tests.createNote();
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.fromId("icon-trash").waitAndTap();
await Tests.navigate("Trash");
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.fromText("Restore note").waitAndTap();
await device.pressBack();
await Tests.fromText(
"Test note description that is very long and should not fit in text."
).isVisible();
});
});

View File

@@ -1,309 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { notesnook } from "../test.ids";
import {
tapById,
elementById,
visibleByText,
tapByText,
createNote,
prepare,
notVisibleById,
navigate,
elementByText,
sleep,
notVisibleByText,
visibleById,
createNotebook
} from "./utils";
// async function addTopic(title = "Topic") {
// await tapById(notesnook.buttons.add);
// await elementById("input-title").typeText(title);
// await tapByText("Add");
// await sleep(500);
// }
describe("NOTEBOOKS", () => {
it("Create a notebook with title only", async () => {
await prepare();
await navigate("Notebooks");
await sleep(500);
await tapByText("Add your first notebook");
await createNotebook("Notebook 1", false, false);
await sleep(500);
await device.pressBack();
await sleep(500);
await visibleByText("Notebook 1");
});
it("Create a notebook title & description", async () => {
await prepare();
await navigate("Notebooks");
await sleep(500);
await tapByText("Add your first notebook");
await createNotebook("Notebook 1", true, false);
await sleep(500);
await device.pressBack();
await sleep(500);
await visibleByText("Notebook 1");
});
it("Create a notebook, move notes", async () => {
await prepare();
let note = await createNote();
await navigate("Notebooks");
await sleep(500);
await tapByText("Add your first notebook");
await createNotebook("Notebook 1", true, true);
await sleep(500);
await tapById("listitem.select");
await tapByText("Move selected notes");
await sleep(500);
await tapByText("Notebook 1");
await sleep(500);
await visibleByText(note.body);
});
it("Add a sub notebook to a notebook", async () => {
await prepare();
await navigate("Notebooks");
await sleep(500);
await tapByText("Add your first notebook");
await createNotebook("Notebook 1", true, false);
await sleep(500);
await device.pressBack();
await sleep(500);
await tapByText("Notebook 1");
await sleep(500);
await tapById("add-notebook-button");
await createNotebook("Sub notebook", true, true);
await sleep(500);
await device.pressBack();
await visibleByText("Sub notebook");
await tapById(notesnook.ids.notebook.menu);
await tapByText("Move to trash");
await sleep(300);
await tapByText("Delete");
await notVisibleByText("Sub notebook");
});
it("Remove a sub notebook from notebook", async () => {
await prepare();
await navigate("Notebooks");
await sleep(500);
await tapByText("Add your first notebook");
await createNotebook("Notebook 1", true, false);
await sleep(500);
await device.pressBack();
await sleep(500);
await tapByText("Notebook 1");
await sleep(500);
await tapById("add-notebook-button");
await createNotebook("Sub notebook", true, true);
await sleep(500);
await device.pressBack();
await visibleByText("Sub notebook");
});
it("Edit notebook", async () => {
await prepare();
await navigate("Notebooks");
await sleep(500);
await tapById(notesnook.buttons.add);
await createNotebook("Notebook 1", true);
await sleep(500);
await device.pressBack();
await tapById(notesnook.ids.notebook.menu);
await tapByText("Edit notebook");
await elementById(notesnook.ids.dialogs.notebook.inputs.title).typeText(
" (edited)"
);
await tapByText("Save");
await visibleByText("Notebook 1 (edited)");
});
it("Edit a sub notebook", async () => {
await prepare();
await navigate("Notebooks");
await sleep(500);
await tapById(notesnook.buttons.add);
await createNotebook("Notebook 1", true);
await sleep(500);
await device.pressBack();
await sleep(500);
await tapByText("Notebook 1");
await sleep(500);
await tapById("add-notebook-button");
await createNotebook("Sub notebook", true, true);
await device.pressBack();
await sleep(500);
await tapById(notesnook.ids.notebook.menu);
await tapByText("Edit notebook");
await sleep(500);
await elementById(notesnook.ids.dialogs.notebook.inputs.title).typeText(
" (edited)"
);
await tapByText("Save");
await sleep(500);
await visibleByText("Sub notebook (edited)");
});
it("Add a note to notebook", async () => {
await prepare();
await navigate("Notebooks");
await tapByText("Add your first notebook");
await sleep(500);
await createNotebook("Notebook 1", true, true);
await sleep(500);
await device.pressBack();
await sleep(500);
await tapByText("Notebook 1");
await createNote();
});
it("Remove note from Notebook", async () => {
await prepare();
await navigate("Notebooks");
await tapByText("Add your first notebook");
await createNotebook("Notebook 1", true, true);
await sleep(500);
await device.pressBack();
await sleep(500);
await tapByText("Notebook 1");
await sleep(500);
let note = await createNote();
await elementByText(note.body).longPress();
await tapById("select-minus");
await notVisibleById(note.title);
});
it("Add/Remove note to notebook from home", async () => {
await prepare();
await navigate("Notebooks");
await sleep(500);
await tapByText("Add your first notebook");
await createNotebook("Notebook 1", true, false);
await sleep(500);
await device.pressBack();
await sleep(500);
await navigate("Notes");
await createNote();
await tapById(notesnook.listitem.menu);
await tapById("icon-notebooks");
await sleep(500);
await tapByText("Notebook 1");
await tapByText("Save");
await sleep(300);
await visibleByText("Notebook 1");
});
it("Edit notebook title, description", async () => {
await prepare();
await navigate("Notebooks");
await sleep(500);
await tapByText("Add your first notebook");
await createNotebook();
await sleep(500);
await device.pressBack();
await sleep(500);
await visibleByText("Notebook 1");
await tapById(notesnook.ids.notebook.menu);
await tapByText("Edit notebook");
await sleep(500);
await elementById(notesnook.ids.dialogs.notebook.inputs.title).typeText(
" (Edited)"
);
await elementById(
notesnook.ids.dialogs.notebook.inputs.description
).clearText();
await elementById(
notesnook.ids.dialogs.notebook.inputs.description
).typeText("Description of Notebook 1 (Edited)");
await tapByText("Save");
await sleep(500);
await visibleByText("Notebook 1 (Edited)");
await visibleByText("Description of Notebook 1 (Edited)");
});
it.skip("Move notebook to trash", async () => {
await prepare();
await navigate("Notebooks");
await sleep(500);
await tapByText("Add your first notebook");
await createNotebook("Notebook 1", false, false);
await sleep(500);
await device.pressBack();
await sleep(500);
await visibleByText("Notebook 1");
await tapById(notesnook.ids.notebook.menu);
await tapByText("Move to trash");
await sleep(2000);
await tapByText("Delete");
await sleep(4000);
await navigate("Trash");
await visibleByText("Notebook 1");
});
it("Move notebook to trash with notes", async () => {
await prepare();
let note = await createNote();
await navigate("Notebooks");
await sleep(500);
await tapByText("Add your first notebook");
await createNotebook("Notebook 1", false, true);
await sleep(500);
await tapById("listitem.select");
await tapByText("Move selected notes");
await sleep(500);
await visibleByText("Notebook 1");
await tapById(notesnook.ids.notebook.menu);
await tapByText("Move to trash");
await sleep(2000);
await tapByText("Move all notes in this notebook to trash");
await sleep(500);
await tapByText("Delete");
await sleep(4000);
await navigate("Trash");
await visibleByText("Notebook 1");
await visibleByText(note.body);
});
it("Pin notebook to side menu", async () => {
await prepare();
await navigate("Notebooks");
await sleep(500);
await tapByText("Add your first notebook");
await createNotebook("Notebook 1", false, false);
await sleep(500);
await device.pressBack();
await sleep(500);
await visibleByText("Notebook 1");
await tapById(notesnook.ids.notebook.menu);
await tapByText("Add Shortcut");
let menu = elementById(notesnook.ids.default.header.buttons.left);
await menu.tap();
await visibleByText("Notebook 1");
});
});

View File

@@ -0,0 +1,233 @@
/*
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 { notesnook } from "../test.ids";
import { Tests } from "./utils";
describe("NOTEBOOKS", () => {
it("Create a notebook with title only", async () => {
await Tests.prepare();
await Tests.navigate("Notebooks");
await Tests.fromText("Add your first notebook").waitAndTap();
await Tests.createNotebook("Notebook 1", false);
await device.pressBack();
await Tests.fromText("Notebook 1").isVisible();
});
it("Create a notebook title & description", async () => {
await Tests.prepare();
await Tests.navigate("Notebooks");
await Tests.fromText("Add your first notebook").waitAndTap();
await Tests.createNotebook("Notebook 1", true);
await device.pressBack();
await Tests.fromText("Notebook 1").isVisible();
});
it("Create a notebook, move notes", async () => {
await Tests.prepare();
let note = await Tests.createNote();
await Tests.navigate("Notebooks");
await Tests.fromText("Add your first notebook").waitAndTap();
await Tests.createNotebook("Notebook 1", true);
await Tests.fromId("listitem.select").waitAndTap();
await Tests.fromText("Move selected notes").waitAndTap();
await Tests.fromText("Notebook 1").waitAndTap();
await Tests.fromText(note.body).isVisible();
});
it("Add a sub notebook to a notebook", async () => {
await Tests.prepare();
await Tests.navigate("Notebooks");
await Tests.fromText("Add your first notebook").waitAndTap();
await Tests.createNotebook("Notebook 1", true);
await device.pressBack();
await Tests.fromText("Notebook 1").waitAndTap();
await Tests.sleep(500);
await Tests.fromId("add-notebook-button").waitAndTap();
await Tests.createNotebook("Sub notebook", true);
await Tests.fromText("Sub notebook").isVisible();
await Tests.fromId(notesnook.ids.notebook.menu).waitAndTap();
await Tests.sleep(500);
await Tests.fromText("Move to trash").waitAndTap();
await Tests.fromText("Delete").waitAndTap();
await Tests.fromText("Sub notebook").isNotVisible();
});
it("Remove a sub notebook from notebook", async () => {
await Tests.prepare();
await Tests.navigate("Notebooks");
await Tests.fromText("Add your first notebook").waitAndTap();
await Tests.createNotebook("Notebook 1", true);
await device.pressBack();
await Tests.fromText("Notebook 1").waitAndTap();
await Tests.sleep(500);
await Tests.fromId("add-notebook-button").waitAndTap();
await Tests.createNotebook("Sub notebook", true);
await Tests.fromText("Sub notebook").isVisible();
});
it("Edit notebook", async () => {
await Tests.prepare();
await Tests.navigate("Notebooks");
await Tests.fromId(notesnook.buttons.add).waitAndTap();
await Tests.createNotebook("Notebook 1", true);
await device.pressBack();
await Tests.fromId(notesnook.ids.notebook.menu).waitAndTap();
await Tests.fromText("Edit notebook").waitAndTap();
await Tests.fromId(
notesnook.ids.dialogs.notebook.inputs.title
).element.typeText(" (edited)");
await Tests.fromText("Save").waitAndTap();
await Tests.fromText("Notebook 1 (edited)").isVisible();
});
it("Edit a sub notebook", async () => {
await Tests.prepare();
await Tests.navigate("Notebooks");
await Tests.fromId(notesnook.buttons.add).waitAndTap();
await Tests.createNotebook("Notebook 1", true);
await device.pressBack();
await Tests.fromText("Notebook 1").waitAndTap();
await Tests.sleep(500);
await Tests.fromId("add-notebook-button").waitAndTap();
await Tests.createNotebook("Sub notebook", true);
await Tests.fromId(notesnook.ids.notebook.menu).waitAndTap();
await Tests.sleep(500);
await Tests.fromText("Edit notebook").waitAndTap();
await Tests.fromId(
notesnook.ids.dialogs.notebook.inputs.title
).element.typeText(" (edited)");
await Tests.fromText("Save").waitAndTap();
await Tests.fromText("Sub notebook (edited)").isVisible();
});
it("Add a note to notebook", async () => {
await Tests.prepare();
await Tests.navigate("Notebooks");
await Tests.fromText("Add your first notebook").waitAndTap();
await Tests.createNotebook("Notebook 1", true);
await device.pressBack();
await Tests.fromText("Notebook 1").waitAndTap();
await Tests.createNote();
});
it.skip("Remove note from Notebook", async () => {
await Tests.prepare();
await Tests.navigate("Notebooks");
await Tests.fromText("Add your first notebook").waitAndTap();
await Tests.createNotebook("Notebook 1", true);
await device.pressBack();
await Tests.fromText("Notebook 1").waitAndTap();
await Tests.sleep(500);
let note = await Tests.createNote();
await Tests.fromText(note.body).element.longPress();
await Tests.fromId("select-minus").waitAndTap();
await Tests.fromId(note.title).isNotVisible();
});
it("Add/Remove note to notebook from home", async () => {
await Tests.prepare();
await Tests.navigate("Notebooks");
await Tests.fromText("Add your first notebook").waitAndTap();
await Tests.createNotebook("Notebook 1", true);
await device.pressBack();
await Tests.navigate("Notes");
await Tests.createNote();
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.sleep(500);
await Tests.fromId("icon-notebooks").waitAndTap();
await Tests.fromText("Notebook 1").waitAndTap();
await Tests.fromText("Save").waitAndTap();
await Tests.fromText("Notebook 1").isVisible();
});
it("Edit notebook title, description", async () => {
await Tests.prepare();
await Tests.navigate("Notebooks");
await Tests.fromText("Add your first notebook").waitAndTap();
await Tests.createNotebook();
await device.pressBack();
await Tests.fromText("Notebook 1").isVisible();
await Tests.fromId(notesnook.ids.notebook.menu).waitAndTap();
await Tests.sleep(500);
await Tests.fromText("Edit notebook").waitAndTap();
await Tests.fromId(
notesnook.ids.dialogs.notebook.inputs.title
).element.typeText(" (Edited)");
await Tests.fromId(
notesnook.ids.dialogs.notebook.inputs.description
).element.clearText();
await Tests.fromId(
notesnook.ids.dialogs.notebook.inputs.description
).element.typeText("Description of Notebook 1 (Edited)");
await Tests.fromText("Save").waitAndTap();
await Tests.fromText("Notebook 1 (Edited)").isVisible();
await Tests.fromText("Description of Notebook 1 (Edited)").isVisible();
});
it.skip("Move notebook to trash", async () => {
await Tests.prepare();
await Tests.navigate("Notebooks");
await Tests.fromText("Add your first notebook").waitAndTap();
await Tests.createNotebook("Notebook 1", false);
await device.pressBack();
await Tests.fromText("Notebook 1").isVisible();
await Tests.fromId(notesnook.ids.notebook.menu).waitAndTap();
await Tests.sleep(500);
await Tests.fromText("Move to trash").waitAndTap();
await Tests.fromText("Delete").waitAndTap();
await Tests.navigate("Trash");
await Tests.fromText("Notebook 1").isVisible();
});
it("Move notebook to trash with notes", async () => {
await Tests.prepare();
let note = await Tests.createNote();
await Tests.navigate("Notebooks");
await Tests.fromText("Add your first notebook").waitAndTap();
await Tests.createNotebook("Notebook 1", false);
await Tests.fromId("listitem.select").waitAndTap();
await Tests.fromText("Move selected notes").waitAndTap();
await Tests.fromText("Notebook 1").isVisible();
await Tests.fromId(notesnook.ids.notebook.menu).waitAndTap();
await Tests.sleep(500);
await Tests.fromText("Move to trash").waitAndTap();
await Tests.fromText(
"Move all notes in this notebook to trash"
).waitAndTap();
await Tests.fromText("Delete").waitAndTap();
await Tests.navigate("Trash");
await Tests.fromText("Notebook 1").isVisible();
await Tests.fromText(note.body).isVisible();
});
it.skip("Pin notebook to side menu", async () => {
await Tests.prepare();
await Tests.navigate("Notebooks");
await Tests.fromText("Add your first notebook").waitAndTap();
await Tests.createNotebook("Notebook 1", false);
await device.pressBack();
await Tests.fromText("Notebook 1").isVisible();
await Tests.fromId(notesnook.ids.notebook.menu).waitAndTap();
await Tests.sleep(500);
await Tests.fromText("Add Shortcut").waitAndTap();
await Tests.fromId(notesnook.ids.default.header.buttons.left).waitAndTap();
await Tests.fromText("Notebook 1").isVisible();
});
});

View File

@@ -17,29 +17,17 @@ 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 { notesnook } from "../test.ids";
import {
tapById,
visibleByText,
createNote,
prepare,
elementById,
sleep,
tapByText
} from "./utils";
import { Tests } from "./utils";
describe("Search", () => {
it("Search for a note", async () => {
await prepare();
let note = await createNote();
await tapById("icon-search");
await sleep(300);
await elementById("search-input").typeText("Test");
await sleep(1000);
await tapByText(note.body);
await sleep(1000);
await Tests.prepare();
let note = await Tests.createNote();
await Tests.fromId("icon-search").waitAndTap();
await Tests.fromId("search-input").element.typeText("Test");
await Tests.fromText(note.body).waitAndTap();
await device.pressBack();
await device.pressBack();
await visibleByText(note.body);
await Tests.fromText(note.body).isVisible();
});
});

View File

@@ -1,121 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { notesnook } from "../test.ids";
import {
tapById,
visibleByText,
createNote,
prepare,
tapByText,
notVisibleByText,
sleep,
navigate,
createNotebook
} from "./utils";
async function sortBy(sorting, elementText = "Default") {
await tapByText(elementText);
await tapByText(sorting);
await device.pressBack();
}
describe("Sort & filter", () => {
it("Sort by date-edited/date-created", async () => {
await prepare();
let webview = web(by.id(notesnook.editor.id));
await createNote("Note 1", "Note 1");
await sleep(300);
await createNote("Note 2", "Note 2");
await sleep(300);
await tapByText("Note 1");
await sleep(500);
await expect(webview.element(by.web.className("ProseMirror"))).toExist();
await webview.element(by.web.className("ProseMirror")).tap();
await webview
.element(by.web.className("ProseMirror"))
.typeText("Edited ", true);
await device.pressBack();
await device.pressBack();
await sortBy("Date created");
await tapById(notesnook.listitem.menu);
//await visibleByText("Note 2");
await device.pressBack();
await sortBy("Date edited");
await tapById(notesnook.listitem.menu);
//await visibleByText("Edited Note 1");
await device.pressBack();
});
it("Disable grouping", async () => {
await prepare();
await createNote("Note 1", "Note 1");
await sleep(300);
await sortBy("None");
await sleep(300);
await visibleByText("None");
});
it("Group by Abc", async () => {
await prepare();
await createNote("Note 1", "Note 1");
await sleep(300);
await sortBy("Abc");
await visibleByText("N");
});
it("Group by Year", async () => {
await prepare();
await createNote("Note 1", "Note 1");
await sleep(300);
await sortBy("Year");
await sleep(300);
await visibleByText("Year");
});
it("Group by Week", async () => {
await prepare();
await createNote("Note 1", "Note 1");
await sleep(300);
await sortBy("Week");
await sleep(300);
await visibleByText("Week");
});
it("Group by Month", async () => {
await prepare();
await createNote("Note 1", "Note 1");
await sleep(300);
await sortBy("Month");
await sleep(300);
await visibleByText("Month");
});
it("Compact mode", async () => {
await prepare();
await createNote("Note 1", "Note 1");
await sleep(300);
await tapById("icon-compact-mode");
await sleep(300);
await notVisibleByText("Note 1");
await tapById("icon-compact-mode");
await sleep(300);
await visibleByText("Note 1");
});
});

View File

@@ -0,0 +1,93 @@
/*
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 { notesnook } from "../test.ids";
import { Tests } from "./utils";
async function sortBy(sorting: string, elementText = "Default") {
await Tests.fromText(elementText).waitAndTap();
await Tests.fromText(sorting).waitAndTap();
await device.pressBack();
}
describe("Sort & filter", () => {
it("Sort by date-edited/date-created", async () => {
await Tests.prepare();
let webview = web();
await Tests.createNote("Note 1", "Note 1");
await Tests.createNote("Note 2", "Note 2");
await Tests.fromText("Note 1").waitAndTap();
await webview.element(by.web.className("ProseMirror")).tap();
await webview
.element(by.web.className("ProseMirror"))
.typeText("Edited ", true);
await device.pressBack();
await device.pressBack();
await sortBy("Date created");
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await device.pressBack();
await sortBy("Date edited");
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await device.pressBack();
});
it("Disable grouping", async () => {
await Tests.prepare();
await Tests.createNote("Note 1", "Note 1");
await sortBy("None");
await Tests.fromText("Default").isNotVisible();
});
it("Group by Abc", async () => {
await Tests.prepare();
await Tests.createNote("Note 1", "Note 1");
await sortBy("Abc");
await Tests.fromText("N").isVisible();
});
it("Group by Year", async () => {
await Tests.prepare();
await Tests.createNote("Note 1", "Note 1");
await sortBy("Year");
await Tests.fromText("Year").isVisible();
});
it("Group by Week", async () => {
await Tests.prepare();
await Tests.createNote("Note 1", "Note 1");
await sortBy("Week");
await Tests.fromText("Week").isVisible();
});
it("Group by Month", async () => {
await Tests.prepare();
await Tests.createNote("Note 1", "Note 1");
await sortBy("Month");
await Tests.fromText("Month").isVisible();
});
it("Compact mode", async () => {
await Tests.prepare();
await Tests.createNote("Note 1", "Note 1");
await Tests.fromId("icon-compact-mode").waitAndTap();
await Tests.fromText("Note 1").isNotVisible();
await Tests.fromId("icon-compact-mode").waitAndTap();
await Tests.fromText("Note 1").isVisible();
});
});

View File

@@ -1,103 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { notesnook } from "../test.ids";
import {
navigate,
tapById,
visibleByText,
createNote,
prepare,
elementById,
tapByText,
notVisibleByText,
sleep
} from "./utils";
describe("Tags", () => {
it("Tag a note", async () => {
await prepare();
let note = await createNote();
await tapById(notesnook.listitem.menu);
await tapByText("Add tags");
await elementById("tag-input").typeText("testtag");
await tapByText('Add "#testtag"');
await visibleByText("#testtag");
await device.pressBack();
await device.pressBack();
await navigate("Tags");
await tapByText("#testtag");
await visibleByText(note.body);
});
it("Untag a note", async () => {
await prepare();
await createNote();
await tapById(notesnook.listitem.menu);
await tapByText("Add tags");
await elementById("tag-input").typeText("testtag");
await tapByText('Add "#testtag"');
await visibleByText("#testtag");
await tapByText("#testtag");
await device.pressBack();
await device.pressBack();
await notVisibleByText("#testtag");
});
it("Creat shortcut of a tag", async () => {
await prepare();
await createNote();
await tapById(notesnook.listitem.menu);
await tapByText("Add tags");
await elementById("tag-input").typeText("testtag");
await tapByText('Add "#testtag"');
await visibleByText("#testtag");
await device.pressBack();
await device.pressBack();
await navigate("Tags");
await sleep(500);
await tapById(notesnook.ids.tag.menu);
await sleep(500);
await tapByText("Add Shortcut");
let menu = elementById(notesnook.ids.default.header.buttons.left);
await menu.tap();
await visibleByText("#testtag");
});
it("Rename a tag", async () => {
await prepare();
await createNote();
await tapById(notesnook.listitem.menu);
await tapByText("Add tags");
await elementById("tag-input").typeText("testtag");
await tapByText('Add "#testtag"');
await visibleByText("#testtag");
await device.pressBack();
await device.pressBack();
await navigate("Tags");
await tapById(notesnook.ids.tag.menu);
await sleep(500);
await tapByText("Rename tag");
await sleep(500);
await elementById("input-value").clearText();
await elementById("input-value").typeText("testtag_edited");
await tapByText("Save");
await visibleByText("#testtag_edited");
});
});

View File

@@ -0,0 +1,95 @@
/*
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 { notesnook } from "../test.ids";
import { Tests } from "./utils";
describe("Tags", () => {
it("Tag a note", async () => {
await Tests.prepare();
let note = await Tests.createNote();
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.sleep(500);
await Tests.fromText("Add tag").waitAndTap();
await Tests.fromId("tag-input").element.typeText("testtag");
await Tests.fromText('Add "#testtag"').waitAndTap();
await Tests.fromText("#testtag").isVisible();
await device.pressBack();
await device.pressBack();
await Tests.navigate("Tags");
await Tests.fromText("#testtag").waitAndTap();
await Tests.fromText(note.body).isVisible();
});
it("Untag a note", async () => {
await Tests.prepare();
await Tests.createNote();
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.sleep(500);
await Tests.fromText("Add tag").waitAndTap();
await Tests.fromId("tag-input").element.typeText("testtag");
await Tests.fromText('Add "#testtag"').waitAndTap();
await Tests.fromText("#testtag").isVisible();
await Tests.fromText("#testtag").waitAndTap();
await device.pressBack();
await device.pressBack();
await Tests.fromText("#testtag").isNotVisible();
});
it("Create shortcut of a tag", async () => {
await Tests.prepare();
await Tests.createNote();
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.sleep(500);
await Tests.fromText("Add tag").waitAndTap();
await Tests.fromId("tag-input").element.typeText("testtag");
await Tests.fromText('Add "#testtag"').waitAndTap();
await Tests.fromText("#testtag").isVisible();
await device.pressBack();
await device.pressBack();
await Tests.navigate("Tags");
await Tests.fromId(notesnook.ids.tag.menu).waitAndTap();
await Tests.sleep(500);
await Tests.fromText("Add shortcut").waitAndTap();
await Tests.fromId(notesnook.ids.default.header.buttons.left).waitAndTap();
await Tests.fromText("testtag").isVisible();
});
it("Rename a tag", async () => {
await Tests.prepare();
await Tests.createNote();
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.sleep(500);
await Tests.fromText("Add tag").waitAndTap();
await Tests.fromId("tag-input").element.typeText("testtag");
await Tests.fromText('Add "#testtag"').waitAndTap();
await Tests.fromText("#testtag").isVisible();
await device.pressBack();
await device.pressBack();
await Tests.navigate("Tags");
await Tests.fromId(notesnook.ids.tag.menu).waitAndTap();
await Tests.sleep(500);
await Tests.fromText("Rename").waitAndTap();
await Tests.sleep(100);
await Tests.fromId("input-value").element.clearText();
await Tests.fromId("input-value").element.typeText("testtag_edited");
await Tests.fromText("Save").waitAndTap();
await Tests.fromText("#testtag_edited").isVisible();
});
});

View File

@@ -1,166 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { notesnook } from "../test.ids";
import { readFileSync } from "fs";
import { expect as jestExpect } from "@jest/globals";
import { toMatchImageSnapshot } from "jest-image-snapshot";
import { device as _device } from "detox";
jestExpect.extend({ toMatchImageSnapshot });
const sleep = (duration) =>
new Promise((resolve) =>
setTimeout(() => {
resolve();
}, duration)
);
async function LaunchApp() {
await waitFor(element(by.id(notesnook.ids.default.root)))
.toBeVisible()
.withTimeout(500);
}
function elementById(id) {
return element(by.id(id)).atIndex(0);
}
function elementByText(text) {
return element(by.text(text)).atIndex(0);
}
async function tapById(id) {
await elementById(id).tap();
}
async function tapByText(text) {
await elementByText(text).tap();
}
async function visibleByText(text) {
await expect(elementByText(text)).toBeVisible();
}
async function visibleById(id) {
await expect(elementById(id)).toBeVisible();
}
async function notVisibleById(id) {
await expect(elementById(id)).not.toBeVisible();
}
async function notVisibleByText(text) {
await expect(elementByText(text)).not.toBeVisible();
}
async function exitEditor() {
await _device.pressBack();
await _device.pressBack();
}
async function createNote(_title, _body) {
let title = _title || "Test note description that ";
let body =
_body ||
"Test note description that is very long and should not fit in text.";
await tapById(notesnook.buttons.add);
let webview = web(by.id(notesnook.editor.id));
await expect(webview.element(by.web.className("ProseMirror"))).toExist();
await webview.element(by.web.className("ProseMirror")).tap();
await webview.element(by.web.className("ProseMirror")).typeText(body, true);
await exitEditor();
await waitFor(element(by.text(body)))
.toBeVisible()
.withTimeout(500);
return { title, body };
}
async function openSideMenu() {
let menu = elementById(notesnook.ids.default.header.buttons.left);
await menu.tap();
}
async function navigate(screen) {
let menu = elementById(notesnook.ids.default.header.buttons.left);
await waitFor(menu).toBeVisible().withTimeout(300);
await menu.tap();
await waitFor(elementByText(screen)).toBeVisible().withTimeout(300);
await elementByText(screen).tap();
}
const testvars = {
isFirstTest: true
};
async function prepare() {
await device.disableSynchronization();
if (testvars.isFirstTest) {
testvars.isFirstTest = false;
return await LaunchApp();
}
await device.reverseTcpPort(8081);
await device.uninstallApp();
await device.installApp();
await device.launchApp({ newInstance: true });
await LaunchApp();
}
async function matchSnapshot(element, name) {
let path = await element.takeScreenshot(name);
const bitmapBuffer = readFileSync(path);
jestExpect(bitmapBuffer).toMatchImageSnapshot({
failureThreshold: 200,
failureThresholdType: "pixel"
});
}
async function createNotebook(title = "Notebook 1", description = true) {
await elementById(notesnook.ids.dialogs.notebook.inputs.title).typeText(
title
);
if (description) {
await elementById(
notesnook.ids.dialogs.notebook.inputs.description
).typeText(`Description of ${title}`);
}
await tapByText("Add");
await sleep(300);
}
export {
matchSnapshot,
createNotebook,
prepare,
LaunchApp,
createNote,
navigate,
openSideMenu,
notVisibleById,
notVisibleByText,
visibleById,
visibleByText,
tapById,
tapByText,
elementByText,
elementById,
sleep,
exitEditor
};

View File

@@ -0,0 +1,149 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { expect as jestExpect } from "@jest/globals";
import { device as _device, expect } from "detox";
import { readFileSync } from "fs";
import { toMatchImageSnapshot } from "jest-image-snapshot";
import type { RouteName } from "../../app/stores/use-navigation-store";
import { notesnook } from "../test.ids";
jestExpect.extend({ toMatchImageSnapshot });
const testvars = {
isFirstTest: true
};
class Element {
element: Detox.NativeElement;
constructor(public type: "id" | "text", public value: string) {
if (type == "id") {
this.element = element(by.id(value)).atIndex(0);
} else {
this.element = element(by.text(value)).atIndex(0);
}
}
isVisible(timeout?: number) {
return waitFor(this.element)
.toBeVisible()
.withTimeout(timeout || 500);
}
isNotVisible(timeout?: number) {
return waitFor(this.element)
.not.toBeVisible()
.withTimeout(timeout || 500);
}
async waitAndTap(timeout?: number) {
await waitFor(this.element)
.toBeVisible()
.withTimeout(timeout || 500);
await this.element.tap();
}
tap(point?: Detox.Point2D): Promise<void> {
return this.element.tap(point);
}
static fromId(id: string) {
return new Element("id", id);
}
static fromText(text: string) {
return new Element("text", text);
}
}
const Tests = {
awaitLaunch: async () => {
await waitFor(element(by.id(notesnook.ids.default.root)))
.toBeVisible()
.withTimeout(500);
},
sleep: (duration: number) => {
return new Promise((resolve) =>
setTimeout(() => {
resolve(undefined);
}, duration)
);
},
fromId: Element.fromId,
fromText: Element.fromText,
async exitEditor() {
await _device.pressBack();
await _device.pressBack();
},
async createNote(_title?: string, _body?: string) {
let title = _title || "Test note description that ";
let body =
_body ||
"Test note description that is very long and should not fit in text.";
await Tests.fromId(notesnook.buttons.add).tap();
await expect(web().element(by.web.className("ProseMirror"))).toExist();
// await web().element(by.web.className("ProseMirror")).tap();
await web().element(by.web.className("ProseMirror")).typeText(body, true);
await Tests.exitEditor();
await Tests.fromText(body).isVisible();
return { title, body };
},
async navigate(screen: RouteName | ({} & string)) {
let menu = Tests.fromId(notesnook.ids.default.header.buttons.left);
await menu.waitAndTap();
await Tests.fromText(screen).waitAndTap();
},
async openSideMenu() {
await Tests.fromId(notesnook.ids.default.header.buttons.left).waitAndTap();
},
async prepare() {
await device.disableSynchronization();
if (testvars.isFirstTest) {
testvars.isFirstTest = false;
return await Tests.awaitLaunch();
}
await device.reverseTcpPort(8081);
await device.uninstallApp();
await device.installApp();
await device.launchApp({ newInstance: true });
await Tests.awaitLaunch();
},
async createNotebook(title = "Notebook 1", description = true) {
await Tests.sleep(1000);
const titleInput = Tests.fromId(
notesnook.ids.dialogs.notebook.inputs.title
);
await titleInput.isVisible();
await titleInput.element.typeText(title);
if (description) {
await Tests.fromId(
notesnook.ids.dialogs.notebook.inputs.description
).element.typeText(`Description of ${title}`);
}
await Tests.fromText("Add").waitAndTap();
},
async matchSnapshot(element: Element, name: string) {
let path = await element.element.takeScreenshot(name);
const bitmapBuffer = readFileSync(path);
(jestExpect(bitmapBuffer) as any).toMatchImageSnapshot({
failureThreshold: 200,
failureThresholdType: "pixel"
});
}
};
export { Element, Tests };

View File

@@ -1,145 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { notesnook } from "../test.ids";
import {
tapById,
elementById,
visibleByText,
tapByText,
createNote,
prepare,
visibleById,
matchSnapshot,
notVisibleById,
navigate
} from "./utils";
import { sleep } from "./utils";
async function lockNote() {
await tapById(notesnook.listitem.menu);
await tapById("icon-lock-unlock");
await sleep(1000);
await visibleByText("Lock");
await elementById(notesnook.ids.dialogs.vault.pwd).typeText("1234");
await elementById(notesnook.ids.dialogs.vault.pwdAlt).typeText("1234");
await tapByText("Lock");
await sleep(500);
await visibleById("note-locked-icon");
}
async function removeFromVault() {
await tapById(notesnook.listitem.menu);
await tapById("icon-lock-unlock");
await sleep(1000);
await elementById(notesnook.ids.dialogs.vault.pwd).typeText("1234");
await tapByText("Unlock");
await sleep(1000);
await notVisibleById("note-locked-icon");
}
async function openLockedNote(pwd) {
await tapById(notesnook.ids.note.get(1));
await sleep(1000);
await visibleByText("Open");
await elementById(notesnook.ids.dialogs.vault.pwd).typeText(pwd || "1234");
await tapByText("Open");
await sleep(3000);
await matchSnapshot(elementById("editor-wrapper"), "note-after-vault-unlock");
}
async function goToPrivacySecuritySettings() {
await navigate("Settings");
await tapByText("Vault");
await sleep(500);
}
describe("VAULT", () => {
it("Create vault from settings", async () => {
await prepare();
await goToPrivacySecuritySettings();
await tapByText("Create vault");
await elementById(notesnook.ids.dialogs.vault.pwd).typeText("1234");
await elementById(notesnook.ids.dialogs.vault.pwdAlt).typeText("1234");
await tapByText("Create");
await sleep(500);
await visibleByText("Clear vault");
});
it("Change vault password", async () => {
await prepare();
await createNote();
await lockNote();
await goToPrivacySecuritySettings();
await tapByText("Change vault password");
await elementById(notesnook.ids.dialogs.vault.pwd).typeText("1234");
await elementById(notesnook.ids.dialogs.vault.changePwd).typeText("2362");
await tapByText("Change");
await sleep(2000);
await device.pressBack();
await device.pressBack();
await device.pressBack();
await openLockedNote("2362");
});
it("Delete vault", async () => {
await prepare();
await createNote();
await lockNote();
await goToPrivacySecuritySettings();
await tapByText("Delete vault");
await elementById(notesnook.ids.dialogs.vault.pwd).typeText("1234");
await tapByText("Delete");
await sleep(500);
await visibleByText("Create vault");
await device.pressBack();
await device.pressBack();
await visibleById(notesnook.listitem.menu);
});
it("Delete vault with locked notes", async () => {
await prepare();
await createNote();
await lockNote();
await goToPrivacySecuritySettings();
await tapByText("Delete vault");
await elementById(notesnook.ids.dialogs.vault.pwd).typeText("1234");
await tapByText("Delete all notes");
await tapByText("Delete");
await sleep(500);
await visibleByText("Create vault");
await device.pressBack();
await device.pressBack();
await notVisibleById(notesnook.listitem.menu);
});
it("Add a note to vault", async () => {
await prepare();
await createNote();
await lockNote();
await openLockedNote();
});
it("Remove note from vault", async () => {
await prepare();
await createNote();
await lockNote();
await removeFromVault();
});
});

View File

@@ -0,0 +1,143 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { expect } from "detox";
import { notesnook } from "../test.ids";
import { Tests } from "./utils";
async function lockNote() {
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.sleep(500);
await Tests.fromId("icon-lock-unlock").waitAndTap();
await Tests.sleep(500);
await Tests.fromText("Lock").isVisible();
await Tests.fromId(notesnook.ids.dialogs.vault.pwd).element.typeText("1234");
await Tests.fromId(notesnook.ids.dialogs.vault.pwdAlt).element.typeText(
"1234"
);
await Tests.fromText("Lock").waitAndTap();
await Tests.fromId("note-locked-icon").isVisible();
}
async function removeFromVault() {
await Tests.fromId(notesnook.listitem.menu).waitAndTap();
await Tests.sleep(500);
await Tests.fromId("icon-lock-unlock").waitAndTap();
await Tests.sleep(500);
await Tests.fromId(notesnook.ids.dialogs.vault.pwd).element.typeText("1234");
await Tests.fromText("Unlock").waitAndTap();
await Tests.fromId("note-locked-icon").isNotVisible();
}
async function openLockedNote(pwd?: string) {
await Tests.fromId(notesnook.ids.note.get(0)).waitAndTap();
await Tests.sleep(500);
await web()
.element(by.web.name("password"))
.typeText(pwd || "1234", false);
await web().element(by.web.className("unlock-note")).tap();
await Tests.sleep(500);
await expect(web().element(by.web.className("unlock-note"))).not.toExist();
}
async function goToPrivacySecuritySettings() {
await Tests.navigate("Settings");
await Tests.fromText("Vault").waitAndTap();
}
describe("VAULT", () => {
it("Create vault from settings", async () => {
await Tests.prepare();
await goToPrivacySecuritySettings();
await Tests.fromText("Create vault").waitAndTap();
await Tests.fromId(notesnook.ids.dialogs.vault.pwd).element.typeText(
"1234"
);
await Tests.fromId(notesnook.ids.dialogs.vault.pwdAlt).element.typeText(
"1234"
);
await Tests.fromText("Create").waitAndTap();
await Tests.fromText("Clear vault").isVisible();
});
it("Change vault password", async () => {
await Tests.prepare();
await Tests.createNote();
await lockNote();
await goToPrivacySecuritySettings();
await Tests.fromText("Change vault password").waitAndTap();
await Tests.fromId(notesnook.ids.dialogs.vault.pwd).element.typeText(
"1234"
);
await Tests.fromId(notesnook.ids.dialogs.vault.changePwd).element.typeText(
"2362"
);
await Tests.fromText("Change").waitAndTap();
await device.pressBack();
await device.pressBack();
await Tests.sleep(500);
await openLockedNote("2362");
});
it("Delete vault", async () => {
await Tests.prepare();
await Tests.createNote();
await lockNote();
await goToPrivacySecuritySettings();
await Tests.fromText("Delete vault").waitAndTap();
await Tests.fromId(notesnook.ids.dialogs.vault.pwd).element.typeText(
"1234"
);
await Tests.fromText("Delete").waitAndTap();
await Tests.fromText("Create vault").isVisible();
await device.pressBack();
await device.pressBack();
await Tests.fromId(notesnook.listitem.menu).isVisible();
});
it("Delete vault with locked notes", async () => {
await Tests.prepare();
await Tests.createNote();
await lockNote();
await goToPrivacySecuritySettings();
await Tests.fromText("Delete vault").waitAndTap();
await Tests.fromId(notesnook.ids.dialogs.vault.pwd).element.typeText(
"1234"
);
await Tests.fromText("Delete notes in this vault").waitAndTap();
await Tests.fromText("Delete").waitAndTap();
await Tests.fromText("Create vault").isVisible();
await device.pressBack();
await device.pressBack();
await Tests.fromId(notesnook.listitem.menu).isNotVisible();
});
it("Add a note to vault", async () => {
await Tests.prepare();
await Tests.createNote();
await lockNote();
await openLockedNote();
});
it.only("Remove note from vault", async () => {
await Tests.prepare();
await Tests.createNote();
await lockNote();
await removeFromVault();
});
});

View File

@@ -52,7 +52,7 @@ module.exports = {
emulator: {
type: 'android.emulator',
device: {
avdName: 'Pixel_5_API_31'
avdName: 'Pixel_5_API_34'
}
}
},

View File

@@ -12,7 +12,7 @@ buildscript {
androidXCoreVersion = "1.7.0"
androidXCore = "1.7.0"
androidXBrowser = "1.0.0"
ndkVersion = "23.1.7779620"
ndkVersion = "23.2.8568313"
}
repositories {

View File

@@ -16,6 +16,7 @@ org.gradle.jvmargs=-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
org.gradle.configuration-cache=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK

View File

@@ -15,13 +15,50 @@ import { DOMParser } from './worker.js';
global.DOMParser = DOMParser;
import {setI18nGlobal } from "@notesnook/intl";
import { i18n } from "@lingui/core";
import { ScriptManager, Script } from '@callstack/repack/client';
import {
messages as $en
} from "@notesnook/intl/locales/$en.json";
} from "@notesnook/intl/dist/locales/$en.json";
import {
messages as $pseudo
} from "@notesnook/intl/dist/locales/$pseudo-LOCALE.json";
i18n.load({
en: $en,
en: __DEV__ ? $pseudo : $en
});
setI18nGlobal(i18n);
i18n.activate("en");
setI18nGlobal(i18n);
if (global.__DEV__) {
const err = console.error;
console.error = function (message) {
if (typeof message === 'string' && message.includes('VirtualizedLists should never be')) {
err.apply(console, ["VirtualizedLists should never be nested inside plain ScrollViews"]);
return;
}
err.apply(console, arguments);
};
}
try {
ScriptManager.shared.addResolver(async (scriptId) => {
// `scriptId` will be either 'student' or 'teacher'
// In dev mode, resolve script location to dev server.
if (__DEV__) {
return {
url: Script.getDevServerURL(scriptId),
cache: false,
};
}
return {
url: Script.getFileSystemURL(scriptId)
};
});
} catch(e) {
/** ignore error when running with metro bundler */
}

View File

@@ -10,8 +10,6 @@
},
"dependencies": {
"@ammarahmed/notifee-react-native": "7.4.7",
"@ammarahmed/react-native-share-extension": "^3.0.0",
"@ammarahmed/react-native-sodium": "1.5.4",
"@bam.tech/react-native-image-resizer": "3.0.5",
"@callstack/repack": "^4.1.1",
"@react-native-clipboard/clipboard": "^1.9.0",
@@ -73,7 +71,10 @@
"react-native-url-polyfill": "^2.0.0",
"react-native-screenguard": "^1.0.0",
"@formatjs/intl-locale": "4.0.0",
"@formatjs/intl-pluralrules": "5.2.14"
"@formatjs/intl-pluralrules": "5.2.14",
"detox": "^20.27.6",
"@ammarahmed/react-native-share-extension": "^2.6.0",
"@ammarahmed/react-native-sodium": "1.5.6"
},
"devDependencies": {
"@babel/core": "^7.20.0",
@@ -86,7 +87,6 @@
"@react-native/metro-config": "0.74.86",
"@tsconfig/react-native": "^3.0.2",
"@types/html-to-text": "^8.0.1",
"@types/jest": "^29.2.1",
"@types/metro-config": "^0.76.3",
"@types/react": "^18.2.6",
"@types/react-native": "^0.69.1",
@@ -98,19 +98,13 @@
"babel-loader": "^8.2.5",
"babel-plugin-module-resolver": "^4.1.0",
"babel-plugin-transform-remove-console": "6.9.4",
"detox": "^20.11.2",
"eslint": "^8.19.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-native": "^4.0.0",
"eslint-plugin-unused-imports": "^2.0.0",
"expect": "^29.6.2",
"jest": "^29.6.3",
"jest-circus": "^29.6.2",
"jest-image-snapshot": "^6.2.0",
"@react-native/babel-preset": "0.74.86",
"pixelmatch": "^5.3.0",
"prettier": "2.8.8",
"react-native-actions-shortcuts": "^1.0.1",
"react-native-bundle-visualizer": "^3.1.1",
@@ -118,9 +112,16 @@
"react-refresh": "0.14.0",
"react-test-renderer": "18.2.0",
"terser-webpack-plugin": "^5.3.5",
"ts-jest": "^29.1.1",
"webpack-cli": "^5.1.4",
"webpack": "^5.88.2",
"metro-react-native-babel-preset": "0.77.0"
"metro-react-native-babel-preset": "0.77.0",
"acorn-import-attributes": "1.9.5",
"expect": "^29.7.0",
"jest": "^29.7.0",
"jest-circus": "^29.7.0",
"jest-image-snapshot": "^6.4.0",
"ts-jest": "^29.2.5",
"pixelmatch": "^6.0.0",
"@types/jest": "^29.5.14"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
diff --git a/node_modules/react-native-actions-shortcuts/android/build.gradle b/node_modules/react-native-actions-shortcuts/android/build.gradle
index 19bd311..d15cdaf 100644
index 19bd311..bc37238 100644
--- a/node_modules/react-native-actions-shortcuts/android/build.gradle
+++ b/node_modules/react-native-actions-shortcuts/android/build.gradle
@@ -29,7 +29,7 @@ android {
@@ -7,7 +7,7 @@ index 19bd311..d15cdaf 100644
buildToolsVersion getExtOrDefault('buildToolsVersion')
defaultConfig {
- minSdkVersion 16
+ minSdkVersion 21
+ minSdkVersion getExtOrDefault('minSdkVersion')
targetSdkVersion getExtOrIntegerDefault('targetSdkVersion')
versionCode 1
versionName "1.0"

View File

@@ -0,0 +1,11 @@
diff --git a/node_modules/react-native-fingerprint-scanner/android/build.gradle b/node_modules/react-native-fingerprint-scanner/android/build.gradle
index eaf3829..6c392ba 100644
--- a/node_modules/react-native-fingerprint-scanner/android/build.gradle
+++ b/node_modules/react-native-fingerprint-scanner/android/build.gradle
@@ -49,5 +49,5 @@ dependencies {
// 1.2.3 is the minimum version compatible with androidx.
// See https://github.com/uccmawei/FingerprintIdentify/issues/74
// (translation https://translate.google.com/translate?sl=zh-CN&tl=en&u=https://github.com/uccmawei/FingerprintIdentify/issues/74)
- implementation "com.wei.android.lib:fingerprintidentify:${safeExtGet("fingerprintidentify", "1.2.6")}"
+ implementation "com.github.uccmawei:FingerprintIdentify:1.2.6"
}

View File

@@ -39,7 +39,7 @@ index 0ccfd23..7771e6f 100644
s.preserve_paths = 'LICENSE', 'package.json'
s.source_files = '**/*.{h,m}'
diff --git a/node_modules/react-native-tooltips/android/build.gradle b/node_modules/react-native-tooltips/android/build.gradle
index 978045f..401b458 100644
index 978045f..59c64f1 100644
--- a/node_modules/react-native-tooltips/android/build.gradle
+++ b/node_modules/react-native-tooltips/android/build.gradle
@@ -3,8 +3,9 @@ buildscript {
@@ -53,24 +53,36 @@ index 978045f..401b458 100644
}
dependencies {
@@ -15,12 +16,12 @@ buildscript {
@@ -14,18 +15,19 @@ buildscript {
apply plugin: 'com.android.library'
android {
-android {
- compileSdkVersion 29
- buildToolsVersion '29.0.2'
+ compileSdkVersion 31
+ buildToolsVersion '31.0.0'
+def safeExtGet(prop, fallback) {
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
+}
+
+android {
+ compileSdkVersion safeExtGet('compileSdkVersion', 29)
+ buildToolsVersion safeExtGet('buildToolsVersion', '29.0.2')
defaultConfig {
- minSdkVersion 16
- targetSdkVersion 29
+ minSdkVersion 21
+ targetSdkVersion 31
+ minSdkVersion safeExtGet('minSdkVersion', 21)
+ targetSdkVersion safeExtGet('targetSdkVersion', 29)
versionCode 1
versionName "1.0"
- versionName "1.0"
- }
- lintOptions {
- abortOnError false
+ versionName "0.1"
}
@@ -36,5 +37,5 @@ repositories {
}
@@ -36,5 +38,5 @@ repositories {
dependencies {
implementation 'com.facebook.react:react-native:+'
@@ -104,7 +116,7 @@ index fb96466..75a6fdb 100644
@interface TooltipDelegate : NSObject <SexyTooltipDelegate>
@@ -7,9 +9,15 @@ @interface TooltipDelegate : NSObject <SexyTooltipDelegate>
@@ -7,9 +9,15 @@
@end
@implementation TooltipDelegate
@@ -120,7 +132,7 @@ index fb96466..75a6fdb 100644
}
@end
@@ -17,77 +25,120 @@ - (void)tooltipDidDismiss:(SexyTooltip *)tooltip {
@@ -17,77 +25,120 @@
@implementation RNTooltips

View File

@@ -21,6 +21,7 @@ import { mdiTimerOutline } from "@mdi/js";
import { Icon } from "@notesnook/ui";
import { Flex, Image, Link, Text } from "@theme-ui/components";
import { SxProp } from "@theme-ui/core";
import { PUBLIC_URL } from "../utils/env";
export function MonographChat({ sx }: SxProp) {
return (
@@ -61,7 +62,7 @@ export function MonographChat({ sx }: SxProp) {
borderRadius: 10,
width: "100%"
}}
src="https://monogr.ph/api/og.png?title=Open+sourcing&description=VGhpcyBtb25vZ3JhcGggaXMgZW5jcnlwdGVkLiBFbnRlciBwYXNzd29yZCB0byB2aWV3IGNvbnRlbnRzLg%3D%3D&date=Saturday%2C+February+18%2C+2023"
src={`${PUBLIC_URL}/api/og.png?title=Open+sourcing&description=VGhpcyBtb25vZ3JhcGggaXMgZW5jcnlwdGVkLiBFbnRlciBwYXNzd29yZCB0byB2aWV3IGNvbnRlbnRzLg%3D%3D&date=Saturday%2C+February+18%2C+2023`}
/>
<Text
mt="12px"
@@ -75,10 +76,10 @@ export function MonographChat({ sx }: SxProp) {
color: "accent",
overflowWrap: "anywhere"
}}
href="https://monogr.ph/62db75572020209c36f9f9fb"
href={`${PUBLIC_URL}/62db75572020209c36f9f9fb`}
target="_blank"
>
https://monogr.ph/62db75572020209c36f9f9fb
{PUBLIC_URL}/62db75572020209c36f9f9fb
</Link>
</Text>
<Timer time={"5m ago"} sx={{ mb: 1, mr: 1, alignSelf: "end" }} />

View File

@@ -25,10 +25,23 @@ import {
useThemeColors,
useThemeEngineStore
} from "@notesnook/theme";
import { useLayoutEffect, useMemo, useRef, useState } from "react";
import { useLayoutEffect, useMemo, useRef } from "react";
import { Flex } from "@theme-ui/components";
import TipTap, { type TipTapProps } from "./tiptap";
import { ScopedThemeProvider } from "../theme-provider";
import { setI18nGlobal, Messages } from "@notesnook/intl";
import { i18n } from "@lingui/core";
const locale = import.meta.env.DEV
? import("@notesnook/intl/locales/$pseudo-LOCALE.json")
: import("@notesnook/intl/locales/$en.json");
locale.then(({ default: locale }) => {
i18n.load({
en: locale.messages as unknown as Messages
});
i18n.activate("en");
});
setI18nGlobal(i18n);
export type EditorType = typeof Editor;
@@ -37,11 +50,9 @@ export function Editor(props: Omit<TipTapProps, "editorContainer" | "theme">) {
const containerRef = useRef<HTMLDivElement>(null);
const { theme } = useThemeEngineStore();
const { colors, isDark } = useThemeColors("editor");
const [isLoaded, setIsLoaded] = useState(false);
useLayoutEffect(() => {
if (
!("window" in globalThis) ||
!containerRef.current ||
!editorContainerRef.current ||
editorContainerRef.current.parentElement === containerRef.current
@@ -50,11 +61,6 @@ export function Editor(props: Omit<TipTapProps, "editorContainer" | "theme">) {
containerRef.current.appendChild(editorContainerRef.current);
}, []);
useLayoutEffect(() => {
if (!("window" in globalThis)) return;
setIsLoaded(true);
}, []);
const editorTheme = useMemo(() => {
const editorTheme = ThemeFactory.construct({
colorScheme: isDark ? "dark" : "light",
@@ -82,25 +88,23 @@ export function Editor(props: Omit<TipTapProps, "editorContainer" | "theme">) {
}
}}
>
{isLoaded ? (
<TipTap
{...props}
editorContainer={() => {
if (editorContainerRef.current) return editorContainerRef.current;
const editorContainer = document.createElement("div");
editorContainer.classList.add("selectable", "editor-container");
editorContainer.style.flex = "1";
editorContainer.style.cursor = "text";
editorContainer.style.color =
theme.scopes.editor?.primary?.paragraph ||
theme.scopes.base.primary.paragraph;
editorContainerRef.current = editorContainer;
if (containerRef.current)
containerRef.current.appendChild(editorContainerRef.current);
return editorContainer;
}}
/>
) : null}
<TipTap
{...props}
editorContainer={() => {
if (editorContainerRef.current) return editorContainerRef.current;
const editorContainer = document.createElement("div");
editorContainer.classList.add("selectable", "editor-container");
editorContainer.style.flex = "1";
editorContainer.style.cursor = "text";
editorContainer.style.color =
theme.scopes.editor?.primary?.paragraph ||
theme.scopes.base.primary.paragraph;
editorContainerRef.current = editorContainer;
if (containerRef.current)
containerRef.current.appendChild(editorContainerRef.current);
return editorContainer;
}}
/>
</Flex>
</ScopedThemeProvider>
);

View File

@@ -35,7 +35,13 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { NNCrypto } from "../../utils/nncrypto.client";
import { hashNavigate } from "../../utils/use-hash-location";
import { Icon } from "@notesnook/ui";
import { mdiAlertCircleOutline, mdiArrowUp, mdiLockOutline } from "@mdi/js";
import {
mdiAlertCircleOutline,
mdiArrowUp,
mdiClose,
mdiListBoxOutline,
mdiLockOutline
} from "@mdi/js";
import { Footer } from "../footer";
import ReportDialog from "./report-modal";
@@ -80,7 +86,7 @@ function generateTableOfContents() {
currentHeading < headingLevel
? level + 1
: currentHeading > headingLevel
? level - 1
? headingLevel
: level;
currentHeading = headingLevel;
@@ -103,41 +109,10 @@ export const MonographPage = ({
}) => {
const [reportDialogVisible, setReportDialogVisible] = useState(false);
const [tableOfContents, setTableOfContents] = useState<TableOfContent[]>([]);
const [activeHeadings, setActiveHeadings] = useState<string[]>();
const [content, setContent] = useState(monograph.content);
const [showScrollToTop, setShowScrollToTop] = useState(false);
const [showTableOfContents, setShowTableOfContents] = useState(false);
const editorContainer = useRef<HTMLDivElement>(null);
useEffect(() => {
function onScroll() {
const scrollTop = window.scrollY;
const height = window.innerHeight || 0;
const viewportHeight = scrollTop + height - 30;
const documentOffset = editorContainer.current?.offsetTop || 0;
const active = tableOfContents.filter((t, i, array) => {
const next = array.at(i + 1);
const headingOffset =
documentOffset + t.node.offsetTop + t.node.clientHeight;
const lessThanNext = next
? scrollTop <=
next.node.offsetTop + documentOffset + next.node.clientHeight
: true;
const isInViewport =
headingOffset > scrollTop && headingOffset < viewportHeight;
const isActive = scrollTop >= headingOffset && lessThanNext;
return isInViewport || isActive;
});
setShowScrollToTop(scrollTop > 120);
setActiveHeadings(active.map((a) => a.id));
}
onScroll();
window.addEventListener("scroll", onScroll);
return () => {
window.removeEventListener("scroll", onScroll);
};
}, [tableOfContents]);
if (!content && monograph.encryptedContent)
return (
<ClientOnly fallback={<div />}>
@@ -158,68 +133,69 @@ export const MonographPage = ({
<Box
sx={{
bg: "background-secondary",
borderBottom: "1px solid var(--border)",
px: [10, "15%"],
pb: 20
py: 4,
top: 0,
zIndex: 999,
position: "sticky"
}}
>
<Button
sx={{ position: "absolute", top: 20, right: 20, p: 0 }}
onClick={() => setShowTableOfContents((s) => !s)}
>
<Icon
path={mdiListBoxOutline}
size={24}
color={showTableOfContents ? "var(--accent)" : "var(--icon)"}
/>
</Button>
<Box
sx={{
width: "100%",
textAlign: "center",
mt: 20,
mb: 100
px: "10%",
textAlign: "center"
}}
>
<Link
as="a"
variant="text.body"
href="/"
sx={{
fontSize: 18,
fontFamily: "monospace",
textDecoration: "none"
}}
>
<span style={{ color: "var(--accent)" }}>Mono</span>graph
</Link>
<ResponsiveTitle title={monograph.title} />
</Box>
</Box>
<Box
sx={{
bg: "background-secondary",
borderBottom: "1px solid var(--border)",
px: [10, 10, "15%"],
pb: 20,
pt: 100
}}
>
<Text as="h1" variant="heading" sx={{ fontSize: 32, fontWeight: 800 }}>
{monograph.title}
</Text>
<Flex sx={{ justifyContent: "space-between" }}>
<Flex sx={{ gap: 1 }}>
<Text
variant="subtitle"
sx={{
color: "paragraph-secondary"
}}
>
{formatDate(monograph.datePublished, {
type: "date-time",
dateFormat: "YYYY-MM-DD",
timeFormat: "24-hour"
})}
</Text>
{monograph.encryptedContent ? (
<Flex sx={{ gap: "2px" }}>
<Icon path={mdiLockOutline} size={14} color="var(--accent)" />
<Text
variant="subtitle"
sx={{
color: "accent"
}}
>
End-to-end encrypted
</Text>
</Flex>
) : null}
<Image
sx={{ display: "none" }}
src={`https://api.notesnook.com/monographs/${monograph.id}/view`}
/>
</Flex>
{monograph.encryptedContent ? null : (
<Flex sx={{ gap: 2 }}>
<Text
variant="subtitle"
sx={{
color: "paragraph-secondary"
}}
>
{formatDate(monograph.datePublished, {
type: "date-time",
dateFormat: "YYYY-MM-DD",
timeFormat: "24-hour"
})}
</Text>
{monograph.encryptedContent ? (
<Flex sx={{ gap: "2px" }}>
<Icon path={mdiLockOutline} size={14} color="var(--accent)" />
<Text
variant="subtitle"
sx={{
color: "accent"
}}
>
End-to-end encrypted
</Text>
</Flex>
) : (
<Button
title="Report"
onClick={() => {
@@ -236,12 +212,18 @@ export const MonographPage = ({
>
<Icon
path={mdiAlertCircleOutline}
color="var(--icon-error)"
color="var(--paragraph-secondary)"
size={16}
/>
<Text variant="error">Report</Text>
<Text variant="error" sx={{ color: "paragraph-secondary" }}>
Report
</Text>
</Button>
)}
<Image
sx={{ display: "none" }}
src={`https://api.notesnook.com/monographs/${monograph.id}/view`}
/>
</Flex>
</Box>
<Flex
@@ -250,14 +232,19 @@ export const MonographPage = ({
<Box
ref={editorContainer}
sx={{
ml: [10, "15%"],
ml: [10, 10, "15%"],
mr: [10, 20],
pr: [0, "max(50px, calc(20% - 300px))"],
pr: [0, 0, "max(50px, calc(20% - 300px))"],
pb: 50,
flex: 1,
borderRight: ["none", "1px solid var(--border)"]
borderRight: [
"none",
"none",
showTableOfContents ? "1px solid var(--border)" : "none"
]
}}
>
{showScrollToTop && <ScrollToTopButton />}
<ScrollToTopButton />
<ClientOnly
fallback={
<Box
@@ -283,69 +270,91 @@ export const MonographPage = ({
<Flex
sx={{
display: ["none", "flex"],
display: ["none", "none", "flex"],
flexDirection: "column",
flexShrink: 0,
mt: 16,
pr: 20,
pb: 50,
width: 300,
alignSelf: "start",
gap: 2,
position: "sticky",
top: 20
top: 80
}}
>
<Text
variant="subtitle"
sx={{
color: "heading-secondary",
mb: 2
}}
>
CONTENTS
</Text>
{tableOfContents.length === 0 ? (
{showTableOfContents ? (
<>
<Text variant="subBody">
This monograph has no table of contents.
<Text
variant="subtitle"
sx={{
color: "heading-secondary",
mb: 2
}}
>
CONTENTS
</Text>
<TableOfContents
container={editorContainer}
contents={tableOfContents}
/>
</>
) : null}
{tableOfContents.map((item) => (
<Button
variant="anchor"
key={item.id}
</Flex>
{showTableOfContents ? (
<Flex
sx={{
display: ["flex", "flex", "none"],
flexDirection: "column",
position: "fixed",
zIndex: 999,
bottom: 0,
right: 0,
borderTopLeftRadius: 15,
borderTopRightRadius: 15,
boxShadow: "0px 0px 15px 5px rgba(0,0,0,0.1)",
bg: "background-secondary",
px: 4,
gap: 2,
height: "60vh",
width: "100%",
overflowY: "auto",
border: "1px solid var(--theme-ui-colors-border)"
}}
>
<Flex
sx={{
textAlign: "left",
p: 0,
m: 0,
pl: item.level * 10,
textDecoration: "none"
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
mx: 0,
// px: "16px",
mt: 4
}}
onClick={() => {
const documentOffset = editorContainer.current?.offsetTop || 0;
const headingOffset =
documentOffset + item.node.offsetTop + item.node.clientHeight;
window.scrollTo({
top: headingOffset - 100,
behavior: "smooth"
});
setShowTableOfContents(false);
}}
>
<Text
variant="subtitle"
sx={{
color: activeHeadings?.includes(item.id)
? "accent"
: "paragraph-secondary",
fontWeight: 600
color: "heading-secondary",
mb: 2
}}
>
{item.title}
CONTENTS
</Text>
</Button>
))}
</Flex>
<Button>
<Icon path={mdiClose} size={24} color="var(--icon)" />
</Button>
</Flex>
<TableOfContents
container={editorContainer}
contents={tableOfContents}
onNavigate={() => setShowTableOfContents(false)}
/>
</Flex>
) : null}
</Flex>
<Footer subtitle="Published via Notesnook" />
{reportDialogVisible ? (
@@ -479,6 +488,20 @@ function MonographLockscreen({
}
function ScrollToTopButton() {
const [showScrollToTop, setShowScrollToTop] = useState(false);
useEffect(() => {
function onScroll() {
const scrollTop = window.scrollY;
setShowScrollToTop(scrollTop > 120);
}
onScroll();
window.addEventListener("scroll", onScroll);
return () => {
window.removeEventListener("scroll", onScroll);
};
}, []);
if (!showScrollToTop) return null;
return (
<Box
onClick={() => {
@@ -507,6 +530,139 @@ function ScrollToTopButton() {
);
}
function ResponsiveTitle({ title }: { title: string }) {
const [showTitle, setShowTitle] = useState(false);
useEffect(() => {
function onScroll() {
const scrollTop = window.scrollY;
setShowTitle(scrollTop > 120);
}
onScroll();
window.addEventListener("scroll", onScroll);
return () => {
window.removeEventListener("scroll", onScroll);
};
}, []);
return (
<>
<Link
as="a"
variant="text.body"
href="/"
sx={{
fontSize: 18,
fontFamily: "monospace",
textDecoration: "none",
display: "block",
whiteSpace: "nowrap",
overflow: "hidden"
}}
>
{showTitle ? (
title
) : (
<>
<span style={{ color: "var(--accent)" }}>Mono</span>graph
</>
)}
</Link>
</>
);
}
function TableOfContents({
contents,
container,
onNavigate,
showActive
}: {
contents: TableOfContent[];
container: React.RefObject<HTMLDivElement>;
onNavigate?: (id: string) => void;
showActive?: boolean;
}) {
const [activeHeadings, setActiveHeadings] = useState<string[]>();
useEffect(() => {
if (!showActive) return;
function onScroll() {
const scrollTop = window.scrollY;
const height = window.innerHeight || 0;
const viewportHeight = scrollTop + height - 30;
const documentOffset = container.current?.offsetTop || 0;
const active = contents.filter((t, i, array) => {
const next = array.at(i + 1);
const headingOffset =
documentOffset + t.node.offsetTop + t.node.clientHeight;
const lessThanNext = next
? scrollTop <=
next.node.offsetTop + documentOffset + next.node.clientHeight
: true;
const isInViewport =
headingOffset > scrollTop && headingOffset < viewportHeight;
const isActive = scrollTop >= headingOffset && lessThanNext;
return isInViewport || isActive;
});
setActiveHeadings(active.map((a) => a.id));
}
onScroll();
window.addEventListener("scroll", onScroll);
return () => {
window.removeEventListener("scroll", onScroll);
};
}, [contents, showActive]);
return (
<>
{contents.length === 0 ? (
<>
<Text variant="subBody">
This monograph has no table of contents.
</Text>
</>
) : null}
{contents.map((item) => (
<Button
variant="anchor"
key={item.id}
sx={{
textAlign: "left",
p: 0,
m: 0,
pl: item.level * 10,
textDecoration: "none",
flexShrink: 0
}}
onClick={() => {
const documentOffset = container.current?.offsetTop || 0;
const headingOffset =
documentOffset + item.node.offsetTop + item.node.clientHeight;
window.scrollTo({
top: headingOffset - 100,
behavior: "smooth"
});
onNavigate?.(item.id);
}}
>
<Text
variant="subtitle"
sx={{
color:
showActive && activeHeadings?.includes(item.id)
? "accent"
: "paragraph-secondary",
fontWeight: 600
}}
>
{item.title}
</Text>
</Button>
))}
</>
);
}
function encode(input: string) {
return toBase64Url(Buffer.from(input).toString("base64"));
}

View File

@@ -0,0 +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 { COMPATIBILITY_VERSION, INSTANCE_NAME } from "../utils/env";
export async function loader() {
return Response.json({
version: COMPATIBILITY_VERSION,
id: "monograph",
instance: INSTANCE_NAME
});
}

View File

@@ -17,6 +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/>.
*/
export const API_HOST = process.env.API_HOST || `https://api.notesnook.com`;
export const API_HOST =
import.meta.env.API_HOST ||
process.env.API_HOST ||
`https://api.notesnook.com`;
export const PUBLIC_URL =
process.env.PUBLIC_URL || `http://localhost:${process.env.PORT || 5017}`;
import.meta.env.PUBLIC_URL ||
process.env.PUBLIC_URL ||
`http://localhost:${import.meta.env.PORT || process.env.PORT || 5017}`;
export const COMPATIBILITY_VERSION = 1;
export const INSTANCE_NAME = process.env.INSTANCE_NAME || "default";

View File

@@ -17,8 +17,26 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const cache: Record<
string,
{
ttl: number;
value: any;
cachedAt: number;
}
> = {};
export async function read<T>(key: string, fallback: T) {
return (await provider).read<T>(key, fallback);
const cached = cache[key];
if (cached && cached.ttl > Date.now() - cached.cachedAt) {
return cached.value;
}
const value = (await provider).read<T>(key, fallback);
cache[key] = {
ttl: 5 * 60000,
value,
cachedAt: Date.now()
};
return value;
}
export async function write<T>(key: string, data: T) {

View File

@@ -1,20 +1,23 @@
{
"name": "@notesnook/monograph",
"version": "1.0.0",
"version": "1.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/monograph",
"version": "1.0.0",
"version": "1.1.0",
"dependencies": {
"@emotion/cache": "11.11.0",
"@emotion/react": "11.11.1",
"@emotion/server": "11.11.0",
"@lingui/core": "4.11.4",
"@lingui/react": "4.11.4",
"@mdi/js": "^7.4.47",
"@notesnook/core": "file:../../packages/core",
"@notesnook/crypto": "file:../../packages/crypto",
"@notesnook/editor": "file:../../packages/editor",
"@notesnook/intl": "file:../../packages/intl",
"@notesnook/theme": "file:../../packages/theme",
"@notesnook/ui": "file:../../packages/ui",
"@remix-run/node": "^2.12.1",
@@ -223,6 +226,29 @@
"zustand": ">=4"
}
},
"../../packages/intl": {
"name": "@notesnook/intl",
"version": "1.0.0",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"devDependencies": {
"@lingui/cli": "^4.11.4",
"@lingui/macro": "^4.11.4 ",
"@lingui/swc-plugin": "^4.0.10",
"@types/react": "^18.2.39",
"babel-plugin-macros": "^3.1.0",
"react": "18.2.0",
"typescript": "5.5.3",
"vite": "^5.4.8",
"vite-plugin-dts": "^4.2.3",
"vite-plugin-static-copy": "^2.0.0",
"vite-plugin-swc-transform": "^1.0.1"
},
"peerDependencies": {
"@lingui/macro": "*",
"react": ">=18"
}
},
"../../packages/theme": {
"name": "@notesnook/theme",
"version": "2.1.3",
@@ -1627,6 +1653,49 @@
"integrity": "sha512-Lg3PnLp0QXpxwLIAuuJboLeRaIhrgJjeuh797QADg3xz8wGLugQOS5DpsE8A6i6Adgzf+bacllkKZG3J0tGfDw==",
"dev": true
},
"node_modules/@lingui/core": {
"version": "4.11.4",
"resolved": "https://registry.npmjs.org/@lingui/core/-/core-4.11.4.tgz",
"integrity": "sha512-W0bBIFe44s//Qs+RQ+NMfzK5vAm9oEKyDddlN94Db6rzeUT/IJo7N+T75A6Bya8v/BrtF2G/W4b77eS3sd0utw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@lingui/message-utils": "4.11.4",
"unraw": "^3.0.0"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/@lingui/message-utils": {
"version": "4.11.4",
"resolved": "https://registry.npmjs.org/@lingui/message-utils/-/message-utils-4.11.4.tgz",
"integrity": "sha512-ZTCDhGbj5EN+P9Ajcj0Gq9uDP3HZTRW6/kT09WkiFgL4NayYLksPvgBk29sIglsS6M+Y6Iw2BrUK403SZjZKgw==",
"license": "MIT",
"dependencies": {
"@messageformat/parser": "^5.0.0",
"js-sha256": "^0.10.1"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/@lingui/react": {
"version": "4.11.4",
"resolved": "https://registry.npmjs.org/@lingui/react/-/react-4.11.4.tgz",
"integrity": "sha512-f7re4HhjI6CLBV1CY/PcI3VYP5zS4rtfU33speWnfkymsxGIXQv4ol3BqrgPLGhypMl2nKcL5nfL+LewrLIW8g==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@lingui/core": "4.11.4"
},
"engines": {
"node": ">=16.0.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@mdi/js": {
"version": "7.4.47",
"resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz",
@@ -1663,6 +1732,15 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/@messageformat/parser": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@messageformat/parser/-/parser-5.1.0.tgz",
"integrity": "sha512-jKlkls3Gewgw6qMjKZ9SFfHUpdzEVdovKFtW1qRhJ3WI4FW5R/NnGDqr8SDGz+krWDO3ki94boMmQvGke1HwUQ==",
"license": "MIT",
"dependencies": {
"moo": "^0.5.1"
}
},
"node_modules/@notesnook/core": {
"resolved": "../../packages/core",
"link": true
@@ -1675,6 +1753,10 @@
"resolved": "../../packages/editor",
"link": true
},
"node_modules/@notesnook/intl": {
"resolved": "../../packages/intl",
"link": true
},
"node_modules/@notesnook/theme": {
"resolved": "../../packages/theme",
"link": true
@@ -5909,6 +5991,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/js-sha256": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.10.1.tgz",
"integrity": "sha512-5obBtsz9301ULlsgggLg542s/jqtddfOpV5KJc4hajc9JV8GeY2gZHSVpYBn4nWqAUTJ9v+xwtbJ1mIBgIH5Vw==",
"license": "MIT"
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -7470,6 +7558,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/moo": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz",
"integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==",
"license": "BSD-3-Clause"
},
"node_modules/morgan": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
@@ -10336,6 +10430,12 @@
"node": ">= 0.8"
}
},
"node_modules/unraw": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/unraw/-/unraw-3.0.0.tgz",
"integrity": "sha512-08/DA66UF65OlpUDIQtbJyrqTR0jTAlJ+jsnkQ4jxR7+K5g5YG1APZKQSMCE1vqqmD+2pv6+IdEjmopFatacvg==",
"license": "MIT"
},
"node_modules/update-browserslist-db": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/monograph",
"version": "1.0.0",
"version": "1.1.0",
"private": true,
"sideEffects": false,
"type": "module",
@@ -15,9 +15,12 @@
"@emotion/react": "11.11.1",
"@emotion/server": "11.11.0",
"@mdi/js": "^7.4.47",
"@lingui/core": "4.11.4",
"@lingui/react": "4.11.4",
"@notesnook/core": "file:../../packages/core",
"@notesnook/crypto": "file:../../packages/crypto",
"@notesnook/editor": "file:../../packages/editor",
"@notesnook/intl": "file:../../packages/intl",
"@notesnook/theme": "file:../../packages/theme",
"@notesnook/ui": "file:../../packages/ui",
"@remix-run/node": "^2.12.1",

View File

@@ -39,7 +39,7 @@ const DEDUPE = [
];
const DEFAULT_THEME_KEY =
process.env.NODE_ENV === "development"
? "DEFAULT_THEME"
? "globalThis.DEFAULT_THEME"
: "globalThis.DEFAULT_THEME";
export default defineConfig(({ isSsrBuild }) => ({
plugins: [

View File

@@ -123,6 +123,7 @@ test("applying coupon should change discount & total price", async () => {
const pricing = await plan.open();
const title = await pricing.getTitle();
if (!title) continue;
await pricing.waitForPaddleFrame();
await pricing.applyCoupon("INTRO50");
planPrices[title.toLowerCase()] = roundOffPrices(await pricing.getPrices());
@@ -141,6 +142,7 @@ test("apply coupon through url", async () => {
for (const plan of ["monthly", "yearly"] as const) {
await app.checkout.goto(plan, "INTRO50");
const pricing = await app.checkout.getPricing();
await pricing.waitForPaddleFrame();
await pricing.waitForCoupon();
planPrices[plan] = roundOffPrices(await pricing.getPrices());
@@ -164,6 +166,7 @@ test("apply coupon after changing country", async () => {
const pricing = await plan.open();
const title = await pricing.getTitle();
if (!title) continue;
await pricing.waitForPaddleFrame();
await pricing.changeCountry("IN", 110001);
await pricing.applyCoupon("INTRO50");

View File

@@ -1,4 +1,4 @@
Subtotal: ₹300
Sales tax: ₹200
Discount: -₹300
Total: ₹300
Total: ₹300/mo

View File

@@ -1,4 +1,4 @@
Subtotal: ₹300
Sales tax: ₹200
Discount: -₹300
Total: ₹300
Total: ₹300/mo

View File

@@ -1,4 +1,4 @@
Subtotal: ₹300
Sales tax: ₹200
Discount: -₹300
Total: ₹300
Total: ₹300/mo

View File

@@ -1,4 +1,4 @@
Subtotal: $100
Sales tax: $0
Discount: -$100
Total: $100
Total: $100/mo

View File

@@ -1,4 +1,4 @@
Subtotal: $100
Sales tax: $0
Discount: -$100
Total: $100
Total: $100/mo

View File

@@ -1,4 +1,4 @@
Subtotal: $100
Sales tax: $0
Discount: -$100
Total: $100
Total: $100/mo

View File

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

View File

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

View File

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

View File

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

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