mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-31 02:58:34 +02:00
Compare commits
94 Commits
fix/io-err
...
fix/intl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f545216dc3 | ||
|
|
989de21e44 | ||
|
|
2c051f9d21 | ||
|
|
4845d40c0b | ||
|
|
b4615fa9aa | ||
|
|
f24ff63c06 | ||
|
|
7518e6badc | ||
|
|
0fcbdbcdc3 | ||
|
|
999856311f | ||
|
|
98f4e08c62 | ||
|
|
f3140cbd8f | ||
|
|
029cadedc4 | ||
|
|
6cdeb48e56 | ||
|
|
2c8abc4d00 | ||
|
|
04820f6500 | ||
|
|
fc6a843a1a | ||
|
|
129dad06c2 | ||
|
|
063b387807 | ||
|
|
e0058fe30b | ||
|
|
a3c297636e | ||
|
|
afc534806d | ||
|
|
4456cc2184 | ||
|
|
2610db90d2 | ||
|
|
f16c9804ee | ||
|
|
7a3a1aa9c2 | ||
|
|
57d0904bee | ||
|
|
5ca6f98c6c | ||
|
|
dbe32e8af6 | ||
|
|
e0758bf353 | ||
|
|
3c1b5d1ee8 | ||
|
|
074008ac67 | ||
|
|
7c56a6beb3 | ||
|
|
3e9d7a70d0 | ||
|
|
e2ca1a0df6 | ||
|
|
392456a0d9 | ||
|
|
a5faa4acea | ||
|
|
4014f2a5b5 | ||
|
|
fb62839dad | ||
|
|
7bf8f1cfc1 | ||
|
|
b8a727eee8 | ||
|
|
52e12de296 | ||
|
|
0bb2345ccb | ||
|
|
78e957eed4 | ||
|
|
865efaa860 | ||
|
|
2931fd0b3e | ||
|
|
faae68928b | ||
|
|
21ada0e2d2 | ||
|
|
2337b8cd85 | ||
|
|
52c8d0b86c | ||
|
|
ce98cea4e7 | ||
|
|
b66a093cc2 | ||
|
|
a3e5ae4cf5 | ||
|
|
cddd08c32d | ||
|
|
5c3ad96cbe | ||
|
|
55b441d14e | ||
|
|
f740a211ce | ||
|
|
3cf00cb0f5 | ||
|
|
5659576ce3 | ||
|
|
55842d44ea | ||
|
|
43238692e4 | ||
|
|
fa1f64f505 | ||
|
|
25e3d56435 | ||
|
|
8722524c98 | ||
|
|
c9b1b42461 | ||
|
|
fa54f7e7f7 | ||
|
|
26d1de0620 | ||
|
|
b21548780a | ||
|
|
c8ba9266a7 | ||
|
|
9e9ef15737 | ||
|
|
4d7d2fbf31 | ||
|
|
b7ddae6950 | ||
|
|
1d60f2132d | ||
|
|
e9e4a76ac2 | ||
|
|
4d3a2cc574 | ||
|
|
c516c8a0ba | ||
|
|
24d5a0746b | ||
|
|
dee6c86d15 | ||
|
|
33e64416f3 | ||
|
|
a74637e9c0 | ||
|
|
ccd1b6d69b | ||
|
|
3750dd38dc | ||
|
|
1506cc257d | ||
|
|
fe7c7889e8 | ||
|
|
3f54eee11d | ||
|
|
11b5922c2c | ||
|
|
71b85d2a4d | ||
|
|
289832d166 | ||
|
|
a88e612c35 | ||
|
|
bda456362b | ||
|
|
f6f644c55d | ||
|
|
08c92333d6 | ||
|
|
4d72b77f29 | ||
|
|
4f9d06737f | ||
|
|
bfc494a3d4 |
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -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:
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -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!
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -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
120
.github/workflows/android.e2e.yml
vendored
Normal 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
|
||||
8
.github/workflows/monograph.publish.yml
vendored
8
.github/workflows/monograph.publish.yml
vendored
@@ -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
|
||||
|
||||
657
apps/desktop/package-lock.json
generated
657
apps/desktop/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/",
|
||||
"lib": ["ESNext"]
|
||||
"lib": ["ESNext"],
|
||||
"moduleResolution": "nodenext",
|
||||
"module": "nodenext"
|
||||
},
|
||||
"include": ["src", "global.d.ts"]
|
||||
}
|
||||
|
||||
1
apps/mobile/.gitignore
vendored
1
apps/mobile/.gitignore
vendored
@@ -39,6 +39,7 @@ DerivedData
|
||||
*.xcuserstate
|
||||
*.hprof
|
||||
**/.xcode.env.local
|
||||
native/cache
|
||||
# Android/IntelliJ
|
||||
#
|
||||
rn-build-deps/
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -171,7 +171,7 @@ const ExportNotesSheet = ({
|
||||
>
|
||||
<DialogHeader
|
||||
icon="export"
|
||||
title={`${strings.export()} ${strings.notes(ids.length)}`}
|
||||
title={strings.exportNotes(ids.length)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -70,7 +70,6 @@ interface InputProps extends TextInputProps {
|
||||
};
|
||||
buttons?: React.ReactNode;
|
||||
onBlurInput?: () => void;
|
||||
onPress?: () => void;
|
||||
height?: number;
|
||||
fontSize?: number;
|
||||
onFocusInput?: () => void;
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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() + "?"
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
29
apps/mobile/e2e/globalSetup.ts
Normal file
29
apps/mobile/e2e/globalSetup.ts
Normal 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;
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -74,7 +74,7 @@ async function login() {
|
||||
// }
|
||||
|
||||
describe("AUTH", () => {
|
||||
it("Login", async () => {
|
||||
it.skip("Login", async () => {
|
||||
await prepare();
|
||||
await openSideMenu();
|
||||
await login();
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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."
|
||||
);
|
||||
});
|
||||
});
|
||||
116
apps/mobile/e2e/tests/note.e2e.ts
Normal file
116
apps/mobile/e2e/tests/note.e2e.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
233
apps/mobile/e2e/tests/notebook.e2e.ts
Normal file
233
apps/mobile/e2e/tests/notebook.e2e.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
93
apps/mobile/e2e/tests/sort.e2e.ts
Normal file
93
apps/mobile/e2e/tests/sort.e2e.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
95
apps/mobile/e2e/tests/tag.e2e.ts
Normal file
95
apps/mobile/e2e/tests/tag.e2e.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
};
|
||||
149
apps/mobile/e2e/tests/utils.ts
Normal file
149
apps/mobile/e2e/tests/utils.ts
Normal 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 };
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
143
apps/mobile/e2e/tests/vault.e2e.ts
Normal file
143
apps/mobile/e2e/tests/vault.e2e.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -52,7 +52,7 @@ module.exports = {
|
||||
emulator: {
|
||||
type: 'android.emulator',
|
||||
device: {
|
||||
avdName: 'Pixel_5_API_31'
|
||||
avdName: 'Pixel_5_API_34'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
529
apps/mobile/package-lock.json
generated
529
apps/mobile/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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" }} />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
28
apps/monograph/app/routes/api.version.ts
Normal file
28
apps/monograph/app/routes/api.version.ts
Normal 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
|
||||
});
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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) {
|
||||
|
||||
104
apps/monograph/package-lock.json
generated
104
apps/monograph/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Subtotal: ₹300
|
||||
Sales tax: ₹200
|
||||
Discount: -₹300
|
||||
Total: ₹300
|
||||
Total: ₹300/mo
|
||||
@@ -1,4 +1,4 @@
|
||||
Subtotal: ₹300
|
||||
Sales tax: ₹200
|
||||
Discount: -₹300
|
||||
Total: ₹300
|
||||
Total: ₹300/mo
|
||||
@@ -1,4 +1,4 @@
|
||||
Subtotal: ₹300
|
||||
Sales tax: ₹200
|
||||
Discount: -₹300
|
||||
Total: ₹300
|
||||
Total: ₹300/mo
|
||||
@@ -1,4 +1,4 @@
|
||||
Subtotal: $100
|
||||
Sales tax: $0
|
||||
Discount: -$100
|
||||
Total: $100
|
||||
Total: $100/mo
|
||||
@@ -1,4 +1,4 @@
|
||||
Subtotal: $100
|
||||
Sales tax: $0
|
||||
Discount: -$100
|
||||
Total: $100
|
||||
Total: $100/mo
|
||||
@@ -1,4 +1,4 @@
|
||||
Subtotal: $100
|
||||
Sales tax: $0
|
||||
Discount: -$100
|
||||
Total: $100
|
||||
Total: $100/mo
|
||||
@@ -1,4 +1,4 @@
|
||||
Subtotal: ₹400
|
||||
Sales tax: ₹300
|
||||
Discount: -₹400
|
||||
Total: ₹400
|
||||
Total: ₹400/yr
|
||||
@@ -1,4 +1,4 @@
|
||||
Subtotal: ₹400
|
||||
Sales tax: ₹300
|
||||
Discount: -₹400
|
||||
Total: ₹400
|
||||
Total: ₹400/yr
|
||||
@@ -1,4 +1,4 @@
|
||||
Subtotal: ₹400
|
||||
Sales tax: ₹300
|
||||
Discount: -₹400
|
||||
Total: ₹400
|
||||
Total: ₹400/yr
|
||||
@@ -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
Reference in New Issue
Block a user