Compare commits

..

9 Commits

Author SHA1 Message Date
Ammar Ahmed
dd1f598393 mobile: update lock file 2024-10-19 15:35:22 +05:00
Ammar Ahmed
f33b9d15fe mobile: bump version 2024-10-19 14:58:19 +05:00
Ammar Ahmed
94a95a3211 mobile: fix file uploads 2024-10-19 14:56:41 +05:00
Ammar Ahmed
fa6adddcc8 mobile: remove try/catch block 2024-10-17 12:02:26 +05:00
Ammar Ahmed
e5f609641f mobile: fix cache dir does not exist error 2024-10-17 11:52:34 +05:00
Ammar Ahmed
c8f2821d7e editor: fix heading and paragraph overlap 2024-10-17 09:32:32 +05:00
Ammar Ahmed
f865d7a68f mobile: fix no error shown if copy/share fails 2024-10-16 16:12:32 +05:00
Ammar Ahmed
2b98b5d5f2 mobile: load async scripts with script manager 2024-10-16 16:12:18 +05:00
Ammar Ahmed
7e1146fb7c mobile: fix single file export on ios 2024-10-16 16:04:31 +05:00
606 changed files with 18881 additions and 47501 deletions

View File

@@ -10,7 +10,7 @@ const authors = readFileSync("AUTHORS", "utf-8");
const isAuthor = authors.includes(`<${authorEmail}>`);
const SCOPES = [
// for full list of scopes + details see: https://github.com/streetwriters/notesnook/blob/master/CONTRIBUTING.md#commit-guidelines
// for full list of scopes + details see: https://github.com/streetwriters/notesnook-private/blob/master/CONTRIBUTING.md#commit-guidelines
"mobile",
"web",
@@ -35,9 +35,7 @@ const SCOPES = [
"common",
"global",
"docs",
"themebuilder",
"intl",
"webclipper"
"themebuilder"
];
module.exports = {

View File

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

View File

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

View File

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

View File

@@ -1,100 +0,0 @@
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: Detox build
run: |
yarn build:android
ls apps/mobile/native/android/app/build/outputs/apk
ls apps/mobile/native/android/app/build/outputs/apk/release
- 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
- name: Upload artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: detox-artifacts
path: artifacts
retention-days: 14

View File

@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

@@ -40,7 +40,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -84,26 +84,6 @@ jobs:
files: ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip
prerelease: ${{ endsWith(steps.app_metadata.outputs.app_version, '-beta') }}
- name: Generate flatpak sources
if: inputs.publish-github && inputs.build-linux
run: |
sudo apt-get install -y flatpak flatpak-builder pipx
git clone https://github.com/flatpak/flatpak-builder-tools.git /tmp/flatpak-builder-tools
cd /tmp/flatpak-builder-tools/node
pipx install .
cd -
node scripts/generate-sources.mjs
- name: Upload flatpak sources
uses: softprops/action-gh-release@v1
if: inputs.publish-github && inputs.build-linux
with:
draft: true
name: Notesnook Desktop v${{ steps.app_metadata.outputs.app_version }}
tag_name: v${{ steps.app_metadata.outputs.app_version }}
files: ./generated-sources.json
prerelease: ${{ endsWith(steps.app_metadata.outputs.app_version, '-beta') }}
build-macos:
name: Build for macOS
needs: build
@@ -112,7 +92,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -156,12 +136,6 @@ jobs:
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu x64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Install provisioning profile
run: echo "${{ secrets.MAC_PROVISIONING_PROFILE }}" | base64 --decode > embedded.provisionprofile
working-directory: ./apps/desktop
@@ -214,7 +188,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -238,12 +212,6 @@ jobs:
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu x64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Build Electron wrapper
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
@@ -279,7 +247,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup .NET Core SDK
uses: actions/setup-dotnet@v2
@@ -304,12 +272,6 @@ jobs:
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu x64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Build
run: npx nx run release --project @notesnook/desktop

View File

@@ -1,233 +0,0 @@
name: Test @notesnook/desktop
on:
workflow_dispatch:
push:
branches:
- "master"
paths:
- "app/desktop/**"
# re-run workflow if workflow file changes
- ".github/workflows/desktop.tests.yml"
pull_request:
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=web
- name: Setup environment
run: |
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
- name: Generate desktop build
run: npx nx build:desktop @notesnook/web
- name: Archive build artifact
uses: actions/upload-artifact@v4
with:
name: build
path: apps/web/build/**/*
test-macos-x64:
name: Test macOS x64
needs: build
runs-on: macos-13
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Build Electron wrapper
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
- name: Build app
run: |
yarn electron-builder --mac --dir --x64
working-directory: ./apps/desktop
- name: Run tests x64
run: |
EXECUTABLE_PATH=output/mac/Notesnook.app/Contents/MacOS/Notesnook npm run test
working-directory: ./apps/desktop
- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results-macos-x64
path: apps/desktop/test-results
retention-days: 5
test-macos:
name: Test macOS
needs: build
runs-on: macos-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Build Electron wrapper
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
- name: Build app
run: |
yarn electron-builder --mac --dir --arm64
working-directory: ./apps/desktop
- name: Run tests arm64
run: |
EXECUTABLE_PATH=output/mac-arm64/Notesnook.app/Contents/MacOS/Notesnook npm run test
working-directory: ./apps/desktop
- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results-macos
path: apps/desktop/test-results
retention-days: 5
test-linux:
name: Test for Linux
needs: build
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu x64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Build Electron wrapper
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
- name: Build app
run: |
yarn electron-builder --linux --dir --arm64 --x64
working-directory: ./apps/desktop
- name: Run tests
run: |
EXECUTABLE_PATH=output/linux-unpacked/notesnook xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- npm run test
working-directory: ./apps/desktop
- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results-linux
path: apps/desktop/test-results
retention-days: 5
test-windows:
name: Test for Windows
needs: build
runs-on: windows-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Download build
uses: actions/download-artifact@v4
with:
name: build
path: ./apps/web/build
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=desktop
- name: Install sqlite-better-trigram for all arch
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu x64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Build Electron wrapper
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
- name: Build app
run: |
npx cross-env NOTESNOOK_STAGING=true yarn electron-builder --win --dir --arm64 --x64
working-directory: ./apps/desktop
- name: Run tests
run: |
npx cross-env EXECUTABLE_PATH=output/win-unpacked/Notesnook.exe npm run test
working-directory: ./apps/desktop
- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results-win
path: apps/desktop/test-results
retention-days: 5

View File

@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

@@ -1,6 +1,6 @@
name: Publish Notesnook Help
on:
on:
workflow_dispatch:
push:
branches:
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: stable

View File

@@ -4,7 +4,7 @@ on: workflow_dispatch
jobs:
build:
runs-on: macos-latest
runs-on: macos-13
timeout-minutes: 60
steps:

View File

@@ -32,12 +32,6 @@ 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
@@ -61,8 +55,8 @@ jobs:
run: docker pull streetwriters/monograph:latest
# Setup QEMU
# - name: Set up QEMU
# uses: docker/setup-qemu-action@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Extract metadata (tags, labels) for Docker
id: meta
@@ -78,7 +72,7 @@ jobs:
file: apps/monograph/Dockerfile
push: true
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
tags: streetwriters/monograph:${{ steps.package_metadata.outputs.app_version }},streetwriters/monograph:latest
tags: ${{ steps.meta.outputs.tags }}
cache-from: streetwriters/monograph:latest
- name: Generate artifact attestation

View File

@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

@@ -1,44 +0,0 @@
name: Benchmark @notesnook/web
on:
workflow_dispatch:
push:
branches:
- "master"
paths:
- "apps/web/**"
# re-run workflow if workflow file changes
- ".github/workflows/web.benchmarks.yml"
pull_request:
jobs:
bench:
name: Bench
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install packages
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=web
- name: Setup environment
run: |
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
- name: Build
run: npm run build:web
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: apps/web
- name: Run benchmarks
id: benchmark
run: node __bench__/app.bench.mjs
working-directory: apps/web

View File

@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache

View File

@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -53,7 +53,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Download build
uses: actions/download-artifact@v4

View File

@@ -1,53 +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 test from "node:test";
import { launchApp } from "./utils.mjs";
import assert from "assert";
import slugify from "slugify";
import path from "path";
import { mkdir } from "fs/promises";
test("make sure app loads", async (t) => {
const { app, page } = await launchApp();
try {
await page.waitForSelector("#authForm");
assert.ok(
await page.getByRole("button", { name: "Create account" }).isVisible()
);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await page.waitForSelector(".ProseMirror");
} catch (e) {
await mkdir("test-results", { recursive: true });
await page.screenshot({
path: path.join(
"test-results",
`${slugify(t.name)}-${process.platform}-${process.arch}-error.png`
)
});
throw e;
} finally {
await app.close();
}
});

File diff suppressed because it is too large Load Diff

View File

@@ -2,38 +2,21 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "3.0.23",
"version": "3.0.18",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts",
"sideEffects": false,
"exports": {
".": {
"require": {
"types": "./dist/types/index.d.ts",
"default": "./dist/cjs/index.js"
},
"import": {
"types": "./dist/types/index.d.ts",
"default": "./dist/esm/index.js"
}
}
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"homepage": "https://notesnook.com/",
"repository": "https://github.com/streetwriters/notesnook",
"license": "GPL-3.0-or-later",
"dependencies": {
"@lingui/core": "4.11.4",
"@notesnook/intl": "file:../../packages/intl",
"@trpc/client": "10.45.2",
"@trpc/server": "10.45.2",
"better-sqlite3-multiple-ciphers": "11.5.0",
"better-sqlite3-multiple-ciphers": "11.2.1",
"electron-trpc": "0.6.1",
"electron-updater": "^6.3.4",
"icojs": "^0.19.4",
"sqlite-better-trigram": "^0.0.2",
"typed-emitter": "^2.1.0",
"yargs": "^17.7.2",
"zod": "^3.23.8"
@@ -43,17 +26,14 @@
"@types/node": "22.7.0",
"@types/yargs": "^17.0.33",
"chokidar": "^4.0.1",
"electron": "^31.7.4",
"electron-builder": "^25.1.8",
"electron": "^30.5.1",
"electron-builder": "^25.0.5",
"esbuild": "^0.24.0",
"node-abi": "^3.68.0",
"node-gyp-build": "^4.8.2",
"playwright": "^1.48.2",
"prebuildify": "^6.0.1",
"slugify": "^1.6.6",
"tree-kill": "^1.2.2",
"undici": "^6.19.8",
"vitest": "^2.1.5"
"undici": "^6.19.8"
},
"optionalDependencies": {
"dmg-license": "^1.0.11"
@@ -62,11 +42,10 @@
"start": "node scripts/dev.mjs",
"staging": "node scripts/build.mjs --run",
"release": "node scripts/build.mjs",
"build": "node ../../scripts/build.mjs",
"build": "tsc",
"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",
"test": "node --test --test-timeout 30000 --test-force-exit"
"postinstall": "patch-package"
},
"author": {
"name": "Streetwriters (Private) Limited",
@@ -95,20 +74,7 @@
"node_modules/file-uri-to-path",
"node_modules/bindings",
"node_modules/node-gyp-build",
"node_modules/sqlite-better-trigram",
"node_modules/sodium-native/prebuilds/${platform}-${arch}",
{
"from": "node_modules/sqlite-better-trigram-linux-${arch}",
"to": "node_modules/sqlite-better-trigram-linux-${arch}"
},
{
"from": "node_modules/sqlite-better-trigram-darwin-${arch}",
"to": "node_modules/sqlite-better-trigram-darwin-${arch}"
},
{
"from": "node_modules/sqlite-better-trigram-windows-${arch}",
"to": "node_modules/sqlite-better-trigram-windows-${arch}"
},
"node_modules/sodium-native/index.js",
"node_modules/sodium-native/package.json"
],
@@ -181,12 +147,10 @@
]
}
],
"signtoolOptions": {
"signingHashAlgorithms": [
"sha256"
],
"sign": "./scripts/sign.js"
},
"signingHashAlgorithms": [
"sha256"
],
"sign": "./scripts/sign.js",
"icon": "assets/icons/app.ico"
},
"portable": {

View File

@@ -18,17 +18,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import path from "path";
import fs from "fs/promises";
import { existsSync } from "fs";
import fs, { readFile, writeFile } from "fs/promises";
import { existsSync, readFileSync } from "fs";
import yargs from "yargs-parser";
import os from "os";
import * as childProcess from "child_process";
import { fileURLToPath } from "url";
import { patchBetterSQLite3 } from "./patch-better-sqlite3.mjs";
const args = yargs(process.argv);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const packageJson = JSON.parse(
readFileSync(path.join(__dirname, "..", "package.json"), "utf-8")
);
const webAppPath = path.resolve(path.join(__dirname, "..", "..", "web"));
@@ -44,6 +46,12 @@ if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
// temporary until there's support for prebuilt binaries for linux ARM
if (os.platform() === "linux") await patchBetterSQLite3();
// if (os.platform() === "win32")
// await exec(
// `npx prebuildify --arch=arm64 --strip -t electron@${packageJson.devDependencies.electron}`,
// path.join(__dirname, "..", "node_modules", "sodium-native")
// );
await fs.cp(path.join(webAppPath, "build"), "build", {
recursive: true,
force: true
@@ -55,7 +63,7 @@ if (args.variant === "mas") {
await exec(`yarn run bundle`);
}
await exec(`yarn run build`);
await exec(`yarn tsc`);
if (args.run) {
await exec(`yarn electron-builder --dir --x64`);
@@ -75,3 +83,21 @@ async function exec(cmd, cwd) {
cwd: cwd || process.cwd()
});
}
async function patchBetterSQLite3() {
const jsonPath = path.join(
__dirname,
"..",
"node_modules",
"better-sqlite3-multiple-ciphers",
"package.json"
);
const json = JSON.parse(await readFile(jsonPath, "utf-8"));
json.version = "11.2.2";
json.homepage = "https://github.com/thecodrr/better-sqlite3-multiple-ciphers";
json.repository.url =
"git://github.com/thecodrr/better-sqlite3-multiple-ciphers.git";
await writeFile(jsonPath, JSON.stringify(json));
}

View File

@@ -36,8 +36,6 @@ const ENV = {
FORCE_COLOR: "false",
COLOR: "0"
};
process.chdir(path.join(__dirname, ".."));
await onChange(true);
console.log("Watching...");
@@ -55,13 +53,11 @@ async function onChange(first) {
if (first) {
await fs.rm("./build/", { force: true, recursive: true });
await exec("npm rebuild electron --verbose --foreground-scripts");
await exec("yarn electron-builder install-app-deps");
}
await exec(`yarn run bundle`);
await exec(`yarn run build`);
execAsync(`yarn`, [`tsc`]);
if (await isBundleSame()) {
console.log("Bundle is same. Doing nothing.");

View File

@@ -1,48 +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 { readFile, writeFile } from "fs/promises";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export async function patchBetterSQLite3() {
const jsonPath = path.join(
__dirname,
"..",
"node_modules",
"better-sqlite3-multiple-ciphers",
"package.json"
);
const json = JSON.parse(await readFile(jsonPath, "utf-8"));
json.version = "11.5.1";
json.homepage = "https://github.com/thecodrr/better-sqlite3-multiple-ciphers";
json.repository.url =
"git://github.com/thecodrr/better-sqlite3-multiple-ciphers.git";
await writeFile(jsonPath, JSON.stringify(json));
}
if (process.argv[1] === __filename) {
console.log("Patching better-sqlite3");
patchBetterSQLite3();
}

View File

@@ -46,8 +46,6 @@ export class SQLite {
this.sqlite = require("better-sqlite3-multiple-ciphers")(
filePath
).unsafeMode(true);
const betterTrigram = require("sqlite-better-trigram");
betterTrigram.load(this.sqlite);
}
/**

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
export { PATHS } from "./constants";
export * from "./constants";
export type { AppRouter } from "./api";
export { type UpdateInfo } from "builder-util-runtime";
export { type DesktopIntegration } from "./utils/config";

View File

@@ -36,20 +36,6 @@ import { bringToFront } from "./utils/bring-to-front";
import { bridge } from "./api/bridge";
import { setupDesktopIntegration } from "./utils/desktop-integration";
import { disableCustomDns, enableCustomDns } from "./utils/custom-dns";
import { Messages, setI18nGlobal } from "@notesnook/intl";
import { i18n } from "@lingui/core";
const locale =
process.env.NODE_ENV === "development"
? 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);
// only run a single instance
if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
@@ -101,7 +87,7 @@ async function createWindow() {
...(config.desktopSettings.nativeTitlebar
? {}
: {
titleBarStyle: process.platform === "win32" || process.platform === "darwin" ? "hidden" : "default",
titleBarStyle: "hidden",
frame: process.platform === "win32" || process.platform === "darwin",
titleBarOverlay: {
height: 37,

View File

@@ -19,21 +19,22 @@ 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: any;
var electronTRPC: RendererGlobalElectronTRPC;
// var NativeNNCrypto: (new () => NNCrypto) | undefined;
}
process.once("loaded", async () => {
const electronTRPC = {
sendMessage: (operation: any) =>
const electronTRPC: RendererGlobalElectronTRPC = {
sendMessage: (operation) =>
ipcRenderer.send(ELECTRON_TRPC_CHANNEL, operation),
onMessage: (callback: any) =>
onMessage: (callback) =>
ipcRenderer.on(ELECTRON_TRPC_CHANNEL, (_event, args) => callback(args))
};
globalThis.electronTRPC = electronTRPC;

View File

@@ -17,7 +17,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { strings } from "@notesnook/intl";
import { Menu, MenuItem, clipboard, shell } from "electron";
function setupMenu() {
@@ -41,7 +40,7 @@ function setupMenu() {
if (params.misspelledWord) {
menu.append(
new MenuItem({
label: strings.addToDictionary(),
label: "Add to dictionary",
click: () =>
globalThis.window?.webContents.session.addWordToSpellCheckerDictionary(
params.misspelledWord
@@ -60,7 +59,7 @@ function setupMenu() {
if (params.linkURL.length) {
menu.append(
new MenuItem({
label: strings.openInBrowser(),
label: "Open in browser",
click: () => shell.openExternal(params.linkURL)
})
);
@@ -69,7 +68,7 @@ function setupMenu() {
if (params.isEditable) {
menu.append(
new MenuItem({
label: strings.undo(),
label: "Undo",
role: "undo",
enabled: params.isEditable,
accelerator: "CommandOrControl+Z"
@@ -78,7 +77,7 @@ function setupMenu() {
menu.append(
new MenuItem({
label: strings.redo(),
label: "Redo",
role: "redo",
enabled: params.isEditable,
accelerator: "CommandOrControl+Y"
@@ -95,7 +94,7 @@ function setupMenu() {
if (params.isEditable)
menu.append(
new MenuItem({
label: strings.cut(),
label: "Cut",
role: "cut",
enabled: params.selectionText.length > 0,
accelerator: "CommandOrControl+X"
@@ -105,7 +104,7 @@ function setupMenu() {
if (params.linkURL?.length) {
menu.append(
new MenuItem({
label: strings.copyLink(),
label: "Copy link",
click() {
clipboard.writeText(params.linkURL);
}
@@ -114,7 +113,7 @@ function setupMenu() {
menu.append(
new MenuItem({
label: strings.copyLinkText(),
label: "Copy link text",
click() {
clipboard.writeText(params.linkText);
}
@@ -125,7 +124,7 @@ function setupMenu() {
if (params.selectionText.length) {
menu.append(
new MenuItem({
label: strings.copy(),
label: "Copy",
role: "copy",
accelerator: "CommandOrControl+C"
})
@@ -136,7 +135,7 @@ function setupMenu() {
menu.append(
new MenuItem({
id: "copy-image",
label: strings.copyImage(),
label: "Copy Image",
click() {
globalThis.window?.webContents.copyImageAt(params.x, params.y);
}
@@ -146,7 +145,7 @@ function setupMenu() {
if (params.isEditable)
menu.append(
new MenuItem({
label: strings.paste(),
label: "Paste",
role: "pasteAndMatchStyle",
enabled: clipboard.readText("clipboard").length > 0,
accelerator: "CommandOrControl+V"

View File

@@ -57,8 +57,7 @@ function getSystemTheme() {
nativeTheme.themeSource = oldThemeSource;
setTimeout(
() =>
listeners.forEach((a) => nativeTheme.addListener("updated", () => a())),
() => listeners.forEach((a) => nativeTheme.addListener("updated", a)),
1000
);
return currentTheme;
@@ -73,8 +72,7 @@ function changeTheme(theme: Theme) {
nativeTheme.themeSource = theme;
setTimeout(
() =>
listeners.forEach((a) => nativeTheme.addListener("updated", () => a())),
() => listeners.forEach((a) => nativeTheme.addListener("updated", a)),
1000
);
}

View File

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

View File

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

View File

@@ -16,8 +16,6 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import {
THEME_COMPATIBILITY_VERSION,
useThemeEngineStore
@@ -65,9 +63,7 @@ const App = () => {
<View
style={{
height: "100%",
width: "100%",
justifyContent: "center",
alignItems: "center"
width: "100%"
}}
>
<View
@@ -142,11 +138,7 @@ export const withTheme = (Element: () => JSX.Element) => {
currTheme = nextTheme;
}
return (
<I18nProvider i18n={i18n}>
<Element />
</I18nProvider>
);
return <Element />;
};
};

View File

@@ -17,16 +17,15 @@ 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 Sodium, { Cipher, Password } from "@ammarahmed/react-native-sodium";
import { SerializedKey } from "@notesnook/crypto";
import Sodium from "@ammarahmed/react-native-sodium";
import { Platform } from "react-native";
import "react-native-get-random-values";
import * as Keychain from "react-native-keychain";
import { MMKVLoader, ProcessingModes } from "react-native-mmkv-storage";
import { generateSecureRandom } from "react-native-securerandom";
import { DatabaseLogger } from ".";
import { ToastManager } from "../../services/event-manager";
import { MMKV } from "./mmkv";
import { ToastManager } from "../../services/event-manager";
// Database key cipher is persisted across different user sessions hence it has
// it's independent storage which we will never clear. This is only used when application has
@@ -63,7 +62,6 @@ const KEYSTORE_CONFIG = Platform.select({
function generatePassword() {
const length = 80;
//@ts-ignore
const crypto = window.crypto || window.msCrypto;
if (typeof crypto === "undefined") {
throw new Error(
@@ -80,27 +78,27 @@ function generatePassword() {
return secret;
}
export async function encryptDatabaseKeyWithPassword(appLockPassword: string) {
const key = (await getDatabaseKey()) as string;
export async function encryptDatabaseKeyWithPassword(appLockPassword) {
const key = getDatabaseKey();
const appLockCredentials = await Sodium.deriveKey(
appLockPassword,
NOTESNOOK_APPLOCK_KEY_SALT
);
const databaseKeyCipher = (await encrypt(appLockCredentials, key)) as Cipher;
const databaseKeyCipher = await encrypt(appLockCredentials, key);
MMKV.setMap(DB_KEY_CIPHER, databaseKeyCipher);
// We reset the database key from keychain once app lock password is set.
await Keychain.resetInternetCredentials(KEYCHAIN_SERVER_DBKEY);
return true;
}
export async function restoreDatabaseKeyToKeyChain(appLockPassword: string) {
const databaseKeyCipher: Cipher = CipherStorage.getMap(DB_KEY_CIPHER);
const databaseKey = (await decrypt(
export async function restoreDatabaseKeyToKeyChain(appLockPassword) {
const databaseKeyCipher = CipherStorage.getMap(DB_KEY_CIPHER);
const databaseKey = await decrypt(
{
password: appLockPassword
},
databaseKeyCipher
)) as string;
);
await Keychain.setInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
@@ -112,16 +110,13 @@ export async function restoreDatabaseKeyToKeyChain(appLockPassword: string) {
return true;
}
export async function setAppLockVerificationCipher(appLockPassword: string) {
export async function setAppLockVerificationCipher(appLockPassword) {
try {
const appLockCredentials = await Sodium.deriveKey(
appLockPassword,
NOTESNOOK_APPLOCK_KEY_SALT
);
const encrypted = (await encrypt(
appLockCredentials,
generatePassword()
)) as Cipher;
const encrypted = await encrypt(appLockCredentials, generatePassword());
CipherStorage.setMap(APPLOCK_CIPHER, encrypted);
DatabaseLogger.info("setAppLockVerificationCipher");
} catch (e) {
@@ -134,9 +129,9 @@ export async function clearAppLockVerificationCipher() {
CipherStorage.removeItem(APPLOCK_CIPHER);
}
export async function validateAppLockPassword(appLockPassword: string) {
export async function validateAppLockPassword(appLockPassword) {
try {
const appLockCipher: Cipher = CipherStorage.getMap(APPLOCK_CIPHER);
const appLockCipher = CipherStorage.getMap(APPLOCK_CIPHER);
if (!appLockCipher) return true;
const key = await Sodium.deriveKey(appLockPassword, appLockCipher.salt);
const decrypted = await decrypt(key, appLockCipher);
@@ -151,18 +146,17 @@ export async function validateAppLockPassword(appLockPassword: string) {
}
}
let DB_KEY: string | undefined;
let DB_KEY;
export function clearDatabaseKey() {
DB_KEY = undefined;
DatabaseLogger.info("Cleared database key");
}
export async function getDatabaseKey(appLockPassword?: string) {
export async function getDatabaseKey(appLockPassword) {
if (DB_KEY) return DB_KEY;
try {
if (appLockPassword) {
const databaseKeyCipher: Cipher =
CipherStorage.getMap("databaseKeyCipher");
const databaseKeyCipher = CipherStorage.getMap("databaseKeyCipher");
const databaseKey = await decrypt(
{
password: appLockPassword
@@ -178,12 +172,13 @@ export async function getDatabaseKey(appLockPassword?: string) {
KEYCHAIN_SERVER_DBKEY
);
if (hasKey) {
const credentials = await Keychain.getInternetCredentials(
KEYCHAIN_SERVER_DBKEY
let credentials = await Keychain.getInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
KEYSTORE_CONFIG
);
DatabaseLogger.info("Getting database key from Keychain");
DB_KEY = (credentials as Keychain.UserCredentials).password;
DB_KEY = credentials.password;
}
}
@@ -195,7 +190,7 @@ export async function getDatabaseKey(appLockPassword?: string) {
NOTESNOOK_DB_KEY_SALT
);
DB_KEY = derivedDatabaseKey.key as string;
DB_KEY = derivedDatabaseKey.key;
await Keychain.setInternetCredentials(
KEYCHAIN_SERVER_DBKEY,
@@ -207,17 +202,18 @@ export async function getDatabaseKey(appLockPassword?: string) {
if (await Keychain.hasInternetCredentials("notesnook")) {
const userKeyCredentials = await Keychain.getInternetCredentials(
"notesnook"
"notesnook",
KEYSTORE_CONFIG
);
if (userKeyCredentials) {
const userKeyCipher: Cipher = (await encrypt(
const userKeyCipher = await encrypt(
{
key: DB_KEY,
salt: NOTESNOOK_DB_KEY_SALT
},
userKeyCredentials.password
)) as Cipher;
);
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
await Keychain.resetInternetCredentials("notesnook");
@@ -227,84 +223,45 @@ export async function getDatabaseKey(appLockPassword?: string) {
return DB_KEY;
} catch (e) {
ToastManager.error(e as Error, "Error getting database key");
ToastManager.error(e, "Error getting database key");
console.log(e, "error");
DatabaseLogger.error(e);
return null;
}
}
export async function deriveCryptoKeyFallback(data: SerializedKey) {
if (Platform.OS !== "ios") return;
export async function deriveCryptoKey(data) {
try {
if (!data.password || !data.salt)
throw new Error(
"Invalid password and salt provided to deriveCryptoKeyFallback"
);
const credentials = await Sodium.deriveKeyFallback?.(
data.password,
data.salt
let credentials = await Sodium.deriveKey(data.password, data.salt);
const userKeyCipher = await encrypt(
{
key: await getDatabaseKey(),
salt: NOTESNOOK_DB_KEY_SALT
},
credentials.key
);
if (!credentials) return;
const userKeyCipher = (await encrypt(
{
key: (await getDatabaseKey()) as string,
salt: NOTESNOOK_DB_KEY_SALT
},
credentials.key as string
)) as Cipher<"base64">;
DatabaseLogger.info("User key fallback stored: ", {
userKeyCipher: !!userKeyCipher
});
DatabaseLogger.info("User key stored: ", !!userKeyCipher);
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
return credentials.key;
} catch (e) {
DatabaseLogger.error(e);
}
}
export async function deriveCryptoKey(data: SerializedKey) {
export async function getCryptoKey(_name) {
try {
if (!data.password || !data.salt)
throw new Error("Invalid password and salt provided to deriveCryptoKey");
const keyCipher = MMKV.getMap(USER_KEY_CIPHER);
const credentials = (await Sodium.deriveKey(
data.password,
data.salt
)) as Password;
const userKeyCipher = (await encrypt(
{
key: (await getDatabaseKey()) as string,
salt: NOTESNOOK_DB_KEY_SALT
},
credentials.key as string
)) as Cipher<"base64">;
DatabaseLogger.info("User key stored: ", {
userKeyCipher: !!userKeyCipher
});
// Store encrypted user key in MMKV
MMKV.setMap(USER_KEY_CIPHER, userKeyCipher);
} catch (e) {
DatabaseLogger.error(e);
}
}
export async function getCryptoKey() {
try {
const keyCipher: Cipher = MMKV.getMap(USER_KEY_CIPHER);
if (!keyCipher) {
DatabaseLogger.info("User key cipher is null");
return undefined;
return null;
}
const key = await decrypt(
{
key: (await getDatabaseKey()) as string,
key: await getDatabaseKey(),
salt: keyCipher.salt
},
keyCipher
@@ -317,7 +274,7 @@ export async function getCryptoKey() {
}
}
export async function removeCryptoKey() {
export async function removeCryptoKey(_name) {
try {
MMKV.removeItem(USER_KEY_CIPHER);
await Keychain.resetInternetCredentials("notesnook");
@@ -327,79 +284,44 @@ export async function removeCryptoKey() {
}
}
export async function getRandomBytes(length: number) {
export async function getRandomBytes(length) {
return await generateSecureRandom(length);
}
export async function hash(
password: string,
email: string,
options?: { usesFallback?: boolean }
) {
DatabaseLogger.log(`Hashing password: fallback: ${options?.usesFallback}`);
export async function hash(password, email) {
let result = await Sodium.hashPassword(password, email);
return result;
}
if (options?.usesFallback && Platform.OS !== "ios") {
return "";
export async function generateCryptoKey(password, salt) {
try {
let credentials = await Sodium.deriveKey(password, salt || null);
return credentials;
} catch (e) {
DatabaseLogger.error(e);
}
return (
options?.usesFallback
? await Sodium.hashPasswordFallback?.(password, email)
: await Sodium.hashPassword(password, email)
) as string;
}
export async function generateCryptoKey(password: string, salt?: string) {
return Sodium.deriveKey(password, salt) as Promise<SerializedKey>;
}
export async function generateCryptoKeyFallback(
password: string,
salt?: string
): Promise<SerializedKey> {
return Sodium.deriveKeyFallback?.(
password,
salt as string
) as Promise<SerializedKey>;
}
export function getAlgorithm(base64Variant: number) {
export function getAlgorithm(base64Variant) {
return `xcha-argon2i13-${base64Variant}`;
}
export async function decrypt(password: SerializedKey, data: Cipher<"base64">) {
const _data = { ...data };
export async function decrypt(password, data) {
if (!password.password && !password.key) return undefined;
if (password.password && password.password === "" && !password.key)
return undefined;
let _data = { ...data };
_data.output = "plain";
if (!password.salt) password.salt = data.salt;
if (Platform.OS === "ios" && !password.key && password.password) {
const key = await Sodium.deriveKey(password.password, password.salt);
try {
return await Sodium.decrypt(key, _data);
} catch (e) {
const fallbackKey = await Sodium.deriveKeyFallback?.(
password.password,
password.salt
);
if (Platform.OS === "ios" && fallbackKey) {
DatabaseLogger.info("Using fallback key for decryption");
}
if (fallbackKey) {
return await Sodium.decrypt(fallbackKey, _data);
} else {
throw e;
}
}
}
return await Sodium.decrypt(password, _data);
}
export async function decryptMulti(
password: Password,
data: Cipher<"base64">[]
) {
export async function decryptMulti(password, data) {
if (!password.password && !password.key) return undefined;
if (password.password && password.password === "" && !password.key)
return undefined;
data = data.map((d) => {
d.output = "plain";
return d;
@@ -409,30 +331,10 @@ export async function decryptMulti(
password.salt = data[0].salt;
}
if (Platform.OS === "ios" && !password.key && password.password) {
const key = await Sodium.deriveKey(password.password, password.salt);
try {
return await Sodium.decryptMulti(key, data);
} catch (e) {
const fallbackKey = await Sodium.deriveKeyFallback?.(
password.password,
password.salt as string
);
if (Platform.OS === "ios" && fallbackKey) {
DatabaseLogger.info("Using fallback key for decryption");
}
if (fallbackKey) {
return await Sodium.decryptMulti(fallbackKey, data);
} else {
throw e;
}
}
}
return await Sodium.decryptMulti(password, data);
}
export function parseAlgorithm(alg: string) {
export function parseAlgorithm(alg) {
if (!alg) return {};
const [enc, kdf, compressed, compressionAlg, base64variant] = alg.split("-");
return {
@@ -444,11 +346,16 @@ export function parseAlgorithm(alg: string) {
};
}
export async function encrypt(password: SerializedKey, plainText: string) {
const result = await Sodium.encrypt<"base64">(password, {
export async function encrypt(password, data) {
if (!password.password && !password.key) return undefined;
if (password.password && password.password === "" && !password.key)
return undefined;
let message = {
type: "plain",
data: plainText
});
data: data
};
let result = await Sodium.encrypt(password, message);
return {
...result,
@@ -456,13 +363,14 @@ export async function encrypt(password: SerializedKey, plainText: string) {
};
}
export async function encryptMulti(
password: SerializedKey,
plainText: string[]
) {
const results = await Sodium.encryptMulti<"base64">(
export async function encryptMulti(password, data) {
if (!password.password && !password.key) return undefined;
if (password.password && password.password === "" && !password.key)
return undefined;
let results = await Sodium.encryptMulti(
password,
plainText.map((item) => ({
data.map((item) => ({
type: "plain",
data: item
}))

View File

@@ -16,28 +16,28 @@ 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 "./logger";
import { database } from "@notesnook/common";
import { logger as dbLogger, ICompressor } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { logger as dbLogger } from "@notesnook/core";
import { Platform } from "react-native";
import * as Gzip from "react-native-gzip";
import EventSource from "../../utils/sse/even-source-ios";
import AndroidEventSource from "../../utils/sse/event-source";
import {
SqliteAdapter,
SqliteIntrospector,
SqliteQueryCompiler
} from "@streetwriters/kysely";
import { Platform } from "react-native";
import * as Gzip from "react-native-gzip";
import SettingsService from "../../services/settings";
import EventSource from "../../utils/sse/even-source-ios";
import AndroidEventSource from "../../utils/sse/event-source";
import { FileStorage } from "../filesystem";
import { getDatabaseKey } from "./encryption";
import "./logger";
import filesystem from "../filesystem";
import Storage from "./storage";
import { RNSqliteDriver } from "./sqlite.kysely";
import { Storage } from "./storage";
import { getDatabaseKey } from "./encryption";
import SettingsService from "../../services/settings";
export async function setupDatabase(password?: string) {
export async function setupDatabase(password) {
const key = await getDatabaseKey(password);
if (!key) throw new Error(strings.databaseSetupFailed());
if (!key)
throw new Error("Database setup failed, could not get database key");
console.log("Opening database with key:", !!key);
@@ -47,21 +47,17 @@ export async function setupDatabase(password?: string) {
SSE_HOST: "https://events.streetwriters.co",
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
ISSUES_HOST: "https://issues.streetwriters.co",
MONOGRAPH_HOST: "https://monogr.ph",
...(SettingsService.getProperty("serverUrls") || {})
});
database.setup({
storage: Storage,
eventsource: (Platform.OS === "ios"
? EventSource
: AndroidEventSource) as any,
fs: FileStorage,
compressor: async () =>
({
compress: Gzip.deflate,
decompress: Gzip.inflate
} as ICompressor),
eventsource: Platform.OS === "ios" ? EventSource : AndroidEventSource,
fs: filesystem,
compressor: {
compress: Gzip.deflate,
decompress: Gzip.inflate
},
batchSize: 100,
sqliteOptions: {
dialect: (name) => ({

View File

@@ -24,9 +24,8 @@ import type {
} from "@streetwriters/kysely";
import { CompiledQuery } from "@streetwriters/kysely";
import { QuickSQLiteConnection, open } from "react-native-quick-sqlite";
import { strings } from "@notesnook/intl";
type Config = { dbName: string; async: boolean; location?: string };
type Config = { dbName: string; async: boolean; location: string };
export class RNSqliteDriver implements Driver {
private connection?: DatabaseConnection;
@@ -98,7 +97,7 @@ class RNSqliteConnection implements DatabaseConnection {
constructor(private readonly db: QuickSQLiteConnection) {}
streamQuery<R>(): AsyncIterableIterator<QueryResult<R>> {
throw new Error(strings.streamingNotSupported());
throw new Error("wasqlite driver doesn't support streaming");
}
async executeQuery<R>(

View File

@@ -16,52 +16,58 @@ 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 { IStorage } from "@notesnook/core";
import { MMKVInstance } from "react-native-mmkv-storage";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import {
decrypt,
decryptMulti,
deriveCryptoKey,
deriveCryptoKeyFallback,
encrypt,
encryptMulti,
generateCryptoKey,
getCryptoKey,
getRandomBytes,
hash,
generateCryptoKeyFallback
removeCryptoKey
} from "./encryption";
import { MMKV } from "./mmkv";
export class KV {
storage: MMKVInstance;
constructor(storage: MMKVInstance) {
/**
* @type {typeof MMKV}
*/
storage = null;
constructor(storage) {
this.storage = storage;
}
async read<T>(key: string, isArray?: boolean) {
if (!key) return undefined;
const data = this.storage.getString(key);
if (!data) return undefined;
async read(key) {
if (!key) return null;
let data = this.storage.getString(key);
if (!data) return null;
try {
return JSON.parse(data) as T;
let parse = JSON.parse(data);
return parse;
} catch (e) {
return data as T;
return data;
}
}
async write<T>(key: string, data: T) {
async write(key, data) {
this.storage.setString(
key,
typeof data === "string" ? data : JSON.stringify(data)
);
return true;
}
async readMulti<T>(keys: string[]) {
async readMulti(keys) {
if (keys.length <= 0) {
return [];
} else {
try {
const data = await this.storage.getMultipleItemsAsync<any>(
let data = await this.storage.getMultipleItemsAsync(
keys.slice(),
"string"
);
@@ -73,24 +79,24 @@ export class KV {
obj = value;
}
return [key, obj];
}) as [string, T][];
});
} catch (e) {
return [];
console.log(e);
}
}
}
async remove(key: string) {
this.storage.removeItem(key);
async remove(key) {
return this.storage.removeItem(key);
}
async removeMulti(keys: string[]) {
if (!keys) return;
this.storage.removeItems(keys);
async removeMulti(keys) {
if (!keys) return true;
return this.storage.removeItems(keys);
}
async clear() {
this.storage.clearStore();
return this.storage.clearStore();
}
async getAllKeys() {
@@ -107,46 +113,54 @@ export class KV {
return keys;
}
async writeMulti(items: [string, any][]) {
await this.storage.setMultipleItemsAsync(items, "object");
async writeMulti(items) {
return this.storage.setMultipleItemsAsync(items, "object");
}
}
const DefaultStorage = new KV(MMKV);
export const Storage: IStorage = {
write<T>(key: string, data: T): Promise<void> {
return DefaultStorage.write(key, data);
},
writeMulti<T>(entries: [string, T][]): Promise<void> {
return DefaultStorage.writeMulti(entries);
},
readMulti<T>(keys: string[]): Promise<[string, T][]> {
return DefaultStorage.readMulti(keys);
},
read<T>(key: string, isArray?: boolean): Promise<T | undefined> {
return DefaultStorage.read(key, isArray);
},
remove(key: string): Promise<void> {
return DefaultStorage.remove(key);
},
removeMulti(keys: string[]): Promise<void> {
return DefaultStorage.removeMulti(keys);
},
clear(): Promise<void> {
return DefaultStorage.clear();
},
getAllKeys(): Promise<string[]> {
return DefaultStorage.getAllKeys();
},
hash,
getCryptoKey,
async function requestPermission() {
if (Platform.OS === "ios") return true;
return true;
}
async function checkAndCreateDir(path) {
let dir =
Platform.OS === "ios"
? RNFetchBlob.fs.dirs.DocumentDir + path
: RNFetchBlob.fs.dirs.SDCardDir + "/Notesnook/" + path;
try {
let exists = await RNFetchBlob.fs.exists(dir);
let isDir = await RNFetchBlob.fs.isDir(dir);
if (!exists || !isDir) {
await RNFetchBlob.fs.mkdir(dir);
}
} catch (e) {
await RNFetchBlob.fs.mkdir(dir);
}
return dir;
}
export default {
read: (key) => DefaultStorage.read(key),
write: (key, value) => DefaultStorage.write(key, value),
readMulti: (keys) => DefaultStorage.readMulti(keys),
remove: (key) => DefaultStorage.remove(key),
clear: () => DefaultStorage.clear(),
getAllKeys: () => DefaultStorage.getAllKeys(),
writeMulti: (items) => DefaultStorage.writeMulti(items),
removeMulti: (keys) => DefaultStorage.removeMulti(keys),
encrypt,
encryptMulti,
decrypt,
decryptMulti,
getRandomBytes,
checkAndCreateDir,
requestPermission,
deriveCryptoKey,
getCryptoKey,
removeCryptoKey,
hash,
generateCryptoKey,
generateCryptoKeyFallback,
deriveCryptoKeyFallback
encryptMulti
};

View File

@@ -16,20 +16,20 @@ 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 Sodium from "@ammarahmed/react-native-sodium";
import { getFileNameWithExtension } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import React from "react";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import * as ScopedStorage from "react-native-scoped-storage";
import { subscribe, zip } from "react-native-zip-archive";
import filesystem from ".";
import { ShareComponent } from "../../components/sheets/export-notes/share";
import { presentSheet, ToastManager } from "../../services/event-manager";
import { ToastManager, presentSheet } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { IOS_APPGROUPID } from "../../utils/constants";
import { DatabaseLogger, db } from "../database";
import Storage from "../database/storage";
import { createCacheDir, exists } from "./io";
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
@@ -50,23 +50,23 @@ export async function downloadAllAttachments() {
/**
* Downloads provided attachments to a .zip file
* on user's device.
* @param {string[]} attachmentIds
* @param {string[]} attachments
* @param onProgress
* @returns
*/
export async function downloadAttachments(attachmentIds: string[]) {
export async function downloadAttachments(attachments) {
await createCacheDir();
if (!attachmentIds || !attachmentIds.length) return;
if (!attachments || !attachments.length) return;
const groupId = `download-all-${Date.now()}`;
let outputFolder;
if (Platform.OS === "android") {
// Ask the user to select a directory to store the file
const file = await ScopedStorage.openDocumentTree(true);
let file = await ScopedStorage.openDocumentTree(true);
outputFolder = file.uri;
if (!outputFolder) return;
} else {
outputFolder = await filesystem.checkAndCreateDir("/downloads/");
outputFolder = await Storage.checkAndCreateDir("/downloads/");
}
// Create the folder to zip;
@@ -82,35 +82,33 @@ export async function downloadAttachments(attachmentIds: string[]) {
await RNFetchBlob.fs.mkdir(zipSourceFolder);
const isCancelled = () => {
if (useAttachmentStore.getState().downloading?.[groupId]?.canceled) {
if (useAttachmentStore.getState().downloading[groupId]?.canceled) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
useAttachmentStore.getState().setDownloading({
groupId,
current: 0,
total: 0,
success: false,
message: strings.network.dowloadCancelled(),
message: "Download cancelled",
canceled: true
});
return true;
}
};
for (let i = 0; i < attachmentIds.length; i++) {
for (let i = 0; i < attachments.length; i++) {
if (isCancelled()) return;
const attachment = await db.attachments.attachment(attachmentIds[i]);
if (!attachment) continue;
let attachment = await db.attachments.attachment(attachments[i]);
const hash = attachment.hash;
try {
useAttachmentStore.getState().setDownloading({
groupId: groupId,
current: i + 1,
total: attachmentIds.length,
total: attachments.length,
filename: attachment.hash
});
// Download to cache
const uri = await downloadAttachment(hash, false, {
let uri = await downloadAttachment(hash, false, {
silent: true,
cache: true,
groupId: groupId
@@ -118,7 +116,7 @@ export async function downloadAttachments(attachmentIds: string[]) {
if (isCancelled()) return;
if (!uri) throw new Error(strings.failedToDownloadFile());
if (!uri) throw new Error("Failed to download file");
// Move file to the source folder we will zip eventually and rename the file to it's actual name.
const filePath = `${zipSourceFolder}/${attachment.filename}`;
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, filePath);
@@ -141,7 +139,7 @@ export async function downloadAttachments(attachmentIds: string[]) {
useAttachmentStore.getState().setDownloading({
current: 0,
total: 1,
message: strings.savingZipFile(),
message: "Saving zip file... Please wait",
groupId
});
// If all goes well, zip the notesnook-attachments folder in cache.
@@ -151,7 +149,9 @@ export async function downloadAttachments(attachmentIds: string[]) {
groupId,
current: progress,
total: 1,
message: strings.savingZipFile((progress * 100).toFixed(1))
message: `Saving zip file (${(progress * 100).toFixed(
1
)}%)... Please wait`
});
});
await zip(zipSourceFolder, zipOutputFile);
@@ -185,7 +185,7 @@ export async function downloadAttachments(attachmentIds: string[]) {
});
releasePermissions(outputFolder);
sub?.remove();
ToastManager.error(e as Error, "Error zipping attachments");
ToastManager.error(e, "Error zipping attachments");
}
// Remove source & zip file from cache.
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
@@ -195,71 +195,49 @@ export async function downloadAttachments(attachmentIds: string[]) {
}
export default async function downloadAttachment(
hashOrId: string,
hash,
global = true,
options?: {
silent?: boolean;
cache?: boolean;
throwError?: boolean;
groupId?: string;
base64?: boolean;
text?: boolean;
options = {
silent: false,
cache: false,
throwError: false,
groupId: undefined,
base64: false,
text: false
}
) {
await createCacheDir();
const attachment = await db.attachments.attachment(hashOrId);
let attachment = await db.attachments.attachment(hash);
if (!attachment) {
DatabaseLogger.log("Attachment not found", {
hash: hashOrId
});
DatabaseLogger.log("Attachment not found");
return;
}
let folder: {
uri: string;
} | null = null;
if (!options?.cache) {
let folder = {};
if (!options.cache) {
if (Platform.OS === "android") {
folder = await ScopedStorage.openDocumentTree();
if (!folder) return;
} else {
folder = {
uri: await filesystem.checkAndCreateDir("/downloads/")
};
folder.uri = await Storage.checkAndCreateDir("/downloads/");
}
}
try {
useAttachmentStore.getState().setDownloading({
groupId: options?.groupId || attachment.hash,
current: 0,
total: 1,
filename: attachment.filename
});
await db
.fs()
.downloadFile(
options?.groupId || attachment.hash,
options.groupId || attachment.hash,
attachment.hash,
attachment.chunkSize
);
useAttachmentStore.getState().setDownloading({
groupId: options?.groupId || attachment.hash,
current: 1,
total: 1,
filename: attachment.filename,
success: true
});
if (!(await exists(attachment.hash))) {
DatabaseLogger.log("Attachment does not exist after download.");
return;
}
if (options?.base64 || options?.text) {
if (options.base64 || options.text) {
DatabaseLogger.log(`Starting to decrypt... hash: ${attachment.hash}`);
return await db.attachments.read(
attachment.hash,
@@ -267,16 +245,14 @@ export default async function downloadAttachment(
);
}
const filename = await getFileNameWithExtension(
let filename = getFileNameWithExtension(
attachment.filename,
attachment.mimeType
);
const key = await db.attachments.decryptKey(attachment.key);
let key = await db.attachments.decryptKey(attachment.key);
if (!key) return;
const info = {
let info = {
iv: attachment.iv,
salt: attachment.salt,
length: attachment.size,
@@ -284,34 +260,38 @@ export default async function downloadAttachment(
hash: attachment.hash,
hashType: attachment.hashType,
mime: attachment.mimeType,
fileName: options?.cache ? undefined : filename,
uri: options?.cache ? undefined : folder?.uri,
fileName: options.cache ? undefined : filename,
uri: options.cache ? undefined : folder.uri,
chunkSize: attachment.chunkSize,
appGroupId: IOS_APPGROUPID
};
let fileUri = await Sodium.decryptFile(
key,
info,
options?.cache ? "cache" : "file"
options.cache ? "cache" : "file"
);
if (!options?.silent) {
if (!options.silent) {
ToastManager.show({
heading: strings.network.downloadSuccess(),
message: strings.network.fileDownloaded(filename),
heading: "Download successful",
message: filename + " downloaded",
type: "success"
});
}
if (Platform.OS === "ios" && !options?.cache) {
fileUri = folder?.uri + `/${filename}`;
if (Platform.OS === "ios" && !options.cache) {
fileUri = folder.uri + `/${filename}`;
}
if (!options?.silent) {
if (!options.silent) {
presentSheet({
title: strings.network.fileDownloaded(),
paragraph: strings.fileSaved(filename, Platform.OS),
title: "File downloaded",
paragraph: `${filename} saved to ${
Platform.OS === "android"
? "selected path"
: "File Manager/Notesnook/downloads"
}`,
icon: "download",
context: global ? "global" : attachment.hash,
context: global ? null : attachment.hash,
component: <ShareComponent uri={fileUri} name={filename} padding={12} />
});
}
@@ -326,17 +306,9 @@ export default async function downloadAttachment(
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}_dcache`)
.catch(console.log);
}
useAttachmentStore.getState().setDownloading({
groupId: options?.groupId || attachment.hash,
current: 0,
total: 0,
filename: attachment.filename,
success: false
});
DatabaseLogger.error(e);
useAttachmentStore.getState().remove(attachment.hash);
if (options?.throwError) {
if (options.throwError) {
throw e;
}
}

View File

@@ -17,8 +17,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { RequestOptions } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import NetInfo from "@react-native-community/netinfo";
import RNFetchBlob from "react-native-blob-util";
import { ToastManager } from "../../services/event-manager";
@@ -27,13 +25,7 @@ import { DatabaseLogger, db } from "../database";
import { createCacheDir, exists } from "./io";
import { ABYTES, cacheDir, getUploadedFileSize, parseS3Error } from "./utils";
export async function downloadFile(
filename: string,
requestOptions: RequestOptions,
cancelToken: {
cancel: (reason?: string) => Promise<void>;
}
) {
export async function downloadFile(filename, requestOptions, cancelToken) {
if (!requestOptions) {
DatabaseLogger.log(
`Error downloading file: ${filename}, reason: No requestOptions`
@@ -43,11 +35,9 @@ export async function downloadFile(
DatabaseLogger.log(`Downloading ${filename}`);
await createCacheDir();
const { url, headers, chunkSize } = requestOptions;
const tempFilePath = `${cacheDir}/${filename}_temp`;
const originalFilePath = `${cacheDir}/${filename}`;
let { url, headers, chunkSize } = requestOptions;
let tempFilePath = `${cacheDir}/${filename}_temp`;
let originalFilePath = `${cacheDir}/${filename}`;
try {
if (await exists(filename)) {
DatabaseLogger.log(`File Exists already: ${filename}`);
@@ -55,17 +45,15 @@ export async function downloadFile(
}
const attachment = await db.attachments.attachment(filename);
if (!attachment) return false;
const size = await getUploadedFileSize(filename);
if (size === -1) {
const error = `${strings.fileVerificationFailed()} (File hash: ${filename})`;
const error = `Uploaded file verification failed. (File hash: ${filename})`;
throw new Error(error);
}
if (size === 0) {
const error = `${strings.fileLengthError()} (File hash: ${filename})`;
const error = `File length is 0. Please upload this file again from the attachment manager. (File hash: ${filename})`;
await db.attachments.markAsFailed(attachment.id, error);
throw new Error(error);
}
@@ -74,44 +62,39 @@ export async function downloadFile(
const decryptedLength = size - totalChunks * ABYTES;
if (attachment && attachment.size !== decryptedLength) {
const error = `${strings.fileLengthMismatch(
attachment.size,
decryptedLength
)} (File hash: ${filename})`;
const error = `File length mismatch. Expected ${attachment.size} but got ${decryptedLength} bytes. Please upload this file again from the attachment manager. (File hash: ${filename})`;
await db.attachments.markAsFailed(attachment.id, error);
throw new Error(error);
}
const resolveUrlResponse = await fetch(url, {
let res = await fetch(url, {
method: "GET",
headers
});
if (!resolveUrlResponse.ok) {
if (!res.ok) {
DatabaseLogger.log(
`Error downloading file: ${filename}, ${resolveUrlResponse.status}, ${resolveUrlResponse.statusText}, reason: Unable to resolve download url`
);
throw new Error(
`${resolveUrlResponse.status}: ${strings.failedToResolvedDownloadUrl()}`
`Error downloading file: ${filename}, ${res.status}, ${res.statusText}, reason: Unable to resolve download url`
);
throw new Error(`${res.status}: Unable to resolve download url`);
}
const downloadUrl = await resolveUrlResponse.text();
const downloadUrl = await res.text();
if (!downloadUrl) {
DatabaseLogger.log(
`Error downloading file: ${filename}, reason: Unable to resolve download url`
);
throw new Error(strings.failedToResolvedDownloadUrl());
throw new Error("Unable to resolve download url");
}
DatabaseLogger.log(`Download starting: ${filename}`);
const request = RNFetchBlob.config({
let request = RNFetchBlob.config({
path: tempFilePath,
IOSBackgroundTask: true,
overwrite: true
})
.fetch("GET", downloadUrl)
.fetch("GET", downloadUrl, null)
.progress(async (recieved, total) => {
useAttachmentStore
.getState()
@@ -120,14 +103,15 @@ export async function downloadFile(
DatabaseLogger.log(`Downloading: ${filename}, ${recieved}/${total}`);
});
cancelToken.cancel = async (reason) => {
cancelToken.cancel = () => {
useAttachmentStore.getState().remove(filename);
request.cancel();
RNFetchBlob.fs.unlink(tempFilePath).catch(console.log);
DatabaseLogger.log(`Download cancelled: ${reason} ${filename}`);
DatabaseLogger.log(`Download cancelled: ${filename}`);
};
const response = await request;
let response = await request;
console.log(response.info().headers);
const contentType =
response.info().headers?.["content-type"] ||
@@ -138,10 +122,10 @@ export async function downloadFile(
throw new Error(`[${error.Code}] ${error.Message}`);
}
const status = response.info().status;
let status = response.info().status;
useAttachmentStore.getState().remove(filename);
if (await exists(originalFilePath)) {
if (exists(originalFilePath)) {
await RNFetchBlob.fs.unlink(originalFilePath).catch(console.log);
}
@@ -153,32 +137,33 @@ export async function downloadFile(
return status >= 200 && status < 300;
} catch (e) {
if (
(e as Error).message !== "canceled" &&
!(e as Error).message.includes("NoSuchKey")
) {
const toast = {
heading: strings.downloadError((e as Error).message),
message: (e as Error).message,
type: "error" as const,
if (e.message !== "canceled" && !e.message.includes("NoSuchKey")) {
ToastManager.show({
heading: "Error downloading file",
message: e.message,
type: "error",
context: "global"
};
ToastManager.show(toast);
toast.context = "local";
ToastManager.show(toast);
});
ToastManager.show({
heading: "Error downloading file",
message: e.message,
type: "error",
context: "local"
});
}
useAttachmentStore.getState().remove(filename);
RNFetchBlob.fs.unlink(tempFilePath).catch(console.log);
RNFetchBlob.fs.unlink(originalFilePath).catch(console.log);
DatabaseLogger.error(e, "Download failed: ", {
url
DatabaseLogger.error(e, {
url,
headers
});
return false;
}
}
export async function checkAttachment(hash: string) {
export async function checkAttachment(hash) {
const internetState = await NetInfo.fetch();
const isInternetReachable =
internetState.isConnected && internetState.isInternetReachable;
@@ -196,7 +181,7 @@ export async function checkAttachment(hash: string) {
failed: `File length is 0. Please upload this file again from the attachment manager. (File hash: ${hash})`
};
} catch (e) {
return { failed: (e as Error)?.message };
return { failed: e?.message };
}
return { success: true };
}

View File

@@ -17,41 +17,24 @@ 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 { IFileStorage } from "@notesnook/core";
import { checkAttachment, downloadFile } from "./download";
import {
bulkExists,
clearCache,
clearFileStorage,
deleteCacheFileByName,
deleteCacheFileByPath,
deleteFile,
exists,
getCacheSize,
hashBase64,
readEncrypted,
writeEncryptedBase64
} from "./io";
import { uploadFile } from "./upload";
import {
cancelable,
checkAndCreateDir,
getUploadedFileSize,
requestPermission
} from "./utils";
export default {
checkAttachment,
writeEncryptedBase64,
hashBase64,
clearCache,
deleteCacheFileByName,
deleteCacheFileByPath,
getCacheSize,
requestPermission,
checkAndCreateDir,
getUploadedFileSize
};
bulkExists,
getCacheSize
} from "./io";
import { uploadFile } from "./upload";
import { cancelable, getUploadedFileSize } from "./utils";
export const FileStorage: IFileStorage = {
export default {
readEncrypted,
writeEncryptedBase64,
hashBase64,
@@ -61,5 +44,10 @@ export const FileStorage: IFileStorage = {
exists,
clearFileStorage,
getUploadedFileSize,
bulkExists
checkAttachment,
clearCache,
deleteCacheFileByName,
deleteCacheFileByPath,
bulkExists,
getCacheSize
};

View File

@@ -18,13 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Sodium from "@ammarahmed/react-native-sodium";
import {
FileEncryptionMetadataWithHash,
FileEncryptionMetadataWithOutputType,
Output,
RequestOptions
} from "@notesnook/core";
import { DataFormat, SerializedKey } from "@notesnook/crypto";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import { eSendEvent } from "../../services/event-manager";
@@ -32,21 +25,17 @@ import { IOS_APPGROUPID } from "../../utils/constants";
import { DatabaseLogger, db } from "../database";
import { ABYTES, cacheDir, cacheDirOld, getRandomId } from "./utils";
export async function readEncrypted<TOutputFormat extends DataFormat>(
filename: string,
key: SerializedKey,
cipherData: FileEncryptionMetadataWithOutputType<TOutputFormat>
) {
export async function readEncrypted(filename, key, cipherData) {
await migrateFilesFromCache();
DatabaseLogger.log("Read encrypted file...");
const path = `${cacheDir}/${filename}`;
let path = `${cacheDir}/${filename}`;
try {
if (!(await exists(filename))) {
return;
return false;
}
const output = await Sodium.decryptFile(
let output = await Sodium.decryptFile(
key,
{
...cipherData,
@@ -58,14 +47,15 @@ export async function readEncrypted<TOutputFormat extends DataFormat>(
DatabaseLogger.log("File decrypted...");
return output as Output<TOutputFormat>;
return output;
} catch (e) {
RNFetchBlob.fs.unlink(path).catch(console.log);
DatabaseLogger.error(e);
return false;
}
}
export async function hashBase64(data: string) {
export async function hashBase64(data) {
const hash = await Sodium.hashFile({
type: "base64",
data,
@@ -77,70 +67,61 @@ export async function hashBase64(data: string) {
};
}
export async function writeEncryptedBase64(
data: string,
encryptionKey: SerializedKey,
mimeType: string
): Promise<FileEncryptionMetadataWithHash> {
export async function writeEncryptedBase64(data, key) {
await createCacheDir();
const filepath = cacheDir + `/${getRandomId("imagecache_")}`;
let filepath = cacheDir + `/${getRandomId("imagecache_")}`;
await RNFetchBlob.fs.writeFile(filepath, data, "base64");
const output = await Sodium.encryptFile(encryptionKey, {
let output = await Sodium.encryptFile(key, {
uri: Platform.OS === "ios" ? filepath : "file://" + filepath,
type: "url"
});
RNFetchBlob.fs.unlink(filepath).catch(console.log);
console.log("encrypted file output: ", output);
output.size = output.length;
delete output.length;
return {
...output,
alg: "xcha-stream"
};
}
export async function deleteFile(
filename: string,
requestOptions?: RequestOptions
): Promise<boolean> {
export async function deleteFile(filename, data) {
await createCacheDir();
const localFilePath = cacheDir + `/${filename}`;
if (!requestOptions) {
RNFetchBlob.fs.unlink(localFilePath).catch(console.log);
let delFilePath = cacheDir + `/${filename}`;
if (!data) {
if (!filename) return;
RNFetchBlob.fs.unlink(delFilePath).catch(console.log);
return true;
}
const { url, headers } = requestOptions;
let { url, headers } = data;
try {
const response = await RNFetchBlob.fetch("DELETE", url, headers);
const status = response.info().status;
const ok = status >= 200 && status < 300;
let response = await RNFetchBlob.fetch("DELETE", url, headers);
let status = response.info().status;
let ok = status >= 200 && status < 300;
if (ok) {
RNFetchBlob.fs.unlink(localFilePath).catch(console.log);
RNFetchBlob.fs.unlink(delFilePath).catch(console.log);
}
return ok;
} catch (e) {
DatabaseLogger.error(e, "Delete file", {
url: url
});
console.log("delete file: ", e, url, headers);
return false;
}
}
export async function clearFileStorage() {
try {
const files = await RNFetchBlob.fs.ls(cacheDir);
const oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
let files = await RNFetchBlob.fs.ls(cacheDir);
let oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
for (const file of files) {
for (let file of files) {
await RNFetchBlob.fs.unlink(cacheDir + `/${file}`).catch(console.log);
}
for (const file of oldCache) {
for (let file of oldCache) {
await RNFetchBlob.fs.unlink(cacheDirOld + `/${file}`).catch(console.log);
}
} catch (e) {
DatabaseLogger.error(e, "clearFileStorage");
console.log("clearFileStorage", e);
}
}
@@ -165,11 +146,11 @@ export async function migrateFilesFromCache() {
return;
}
const files = await RNFetchBlob.fs.ls(cacheDir);
let files = await RNFetchBlob.fs.ls(cacheDir);
console.log("Files to migrate:", files.join(","));
const oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
for (const file of oldCache) {
let oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
for (let file of oldCache) {
if (file.startsWith("org.") || file.startsWith("com.")) continue;
RNFetchBlob.fs
.mv(cacheDirOld + `/${file}`, cacheDir + `/${file}`)
@@ -178,7 +159,7 @@ export async function migrateFilesFromCache() {
}
await RNFetchBlob.fs.createFile(migratedFilesPath, "1", "utf8");
} catch (e) {
DatabaseLogger.error(e, "migrateFilesFromCache");
console.log("migrateFilesFromCache", e);
}
}
@@ -188,14 +169,14 @@ export async function clearCache() {
eSendEvent("cache-cleared");
}
export async function deleteCacheFileByPath(path: string) {
export async function deleteCacheFileByPath(path) {
await RNFetchBlob.fs.unlink(path).catch(console.log);
}
export async function deleteCacheFileByName(name: string) {
export async function deleteCacheFileByName(name) {
const iosAppGroup =
Platform.OS === "ios"
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${name}`;
await RNFetchBlob.fs.unlink(appGroupPath).catch(console.log);
@@ -211,12 +192,12 @@ export async function deleteDCacheFiles() {
}
}
export async function exists(filename: string) {
const path = `${cacheDir}/${filename}`;
export async function exists(filename) {
let path = `${cacheDir}/${filename}`;
const iosAppGroup =
Platform.OS === "ios"
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
@@ -230,7 +211,6 @@ export async function exists(filename: string) {
if (exists || existsInAppGroup) {
const attachment = await db.attachments.attachment(filename);
if (!attachment) return false;
const totalChunks = Math.ceil(attachment.size / attachment.chunkSize);
const totalAbytes = totalChunks * ABYTES;
const expectedFileSize = attachment.size + totalAbytes;
@@ -254,7 +234,7 @@ export async function exists(filename: string) {
return exists;
}
export async function bulkExists(files: string[]) {
export async function bulkExists(files) {
try {
await createCacheDir();
const cacheFiles = await RNFetchBlob.fs.ls(cacheDir);
@@ -263,7 +243,7 @@ export async function bulkExists(files: string[]) {
if (Platform.OS === "ios") {
const iosAppGroup =
Platform.OS === "ios"
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupFiles = await RNFetchBlob.fs.ls(iosAppGroup);
missingFiles = missingFiles.filter(
@@ -282,8 +262,8 @@ export async function getCacheSize() {
const stat = await RNFetchBlob.fs.lstat(`file://` + cacheDir);
let total = 0;
console.log("Total files", stat.length);
stat.forEach((file) => {
total += parseInt(file.size as unknown as string);
stat.forEach((s) => {
total += parseInt(s.size);
});
return total;
}

View File

@@ -17,7 +17,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { RequestOptions } from "@notesnook/core";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import { ToastManager } from "../../services/event-manager";
@@ -25,22 +24,11 @@ import { useAttachmentStore } from "../../stores/use-attachment-store";
import { IOS_APPGROUPID } from "../../utils/constants";
import { DatabaseLogger, db } from "../database";
import { createCacheDir } from "./io";
import {
cacheDir,
checkUpload,
FileSizeResult,
getUploadedFileSize
} from "./utils";
import { cacheDir, checkUpload, getUploadedFileSize } from "./utils";
export async function uploadFile(
filename: string,
requestOptions: RequestOptions,
cancelToken: {
cancel: (reason?: string) => Promise<void>;
}
) {
export async function uploadFile(filename, requestOptions, cancelToken) {
if (!requestOptions) return false;
const { url, headers } = requestOptions;
let { url, headers } = requestOptions;
await createCacheDir();
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
@@ -51,7 +39,7 @@ export async function uploadFile(
if (!exists && Platform.OS === "ios") {
const iosAppGroup =
Platform.OS === "ios"
? await (RNFetchBlob.fs as any).pathForAppGroup(IOS_APPGROUPID)
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
filePath = appGroupPath;
@@ -66,15 +54,14 @@ export async function uploadFile(
const fileSize = (await RNFetchBlob.fs.stat(filePath)).size;
const remoteFileSize = await getUploadedFileSize(filename);
if (remoteFileSize === FileSizeResult.Error) return false;
if (remoteFileSize > FileSizeResult.Empty && remoteFileSize === fileSize) {
let remoteFileSize = await getUploadedFileSize(filename);
if (remoteFileSize === -1) return false;
if (remoteFileSize > 0 && remoteFileSize === fileSize) {
DatabaseLogger.log(`File ${filename} is already uploaded.`);
return true;
}
const uploadUrlResponse = await fetch(url, {
let uploadUrlResponse = await fetch(url, {
method: "PUT",
headers
});
@@ -91,8 +78,7 @@ export async function uploadFile(
DatabaseLogger.info(`Starting upload: ${filename}`);
const uploadRequest = RNFetchBlob.config({
//@ts-ignore
let uploadRequest = RNFetchBlob.config({
IOSBackgroundTask: !globalThis["IS_SHARE_EXTENSION"]
})
.fetch(
@@ -112,14 +98,14 @@ export async function uploadFile(
);
});
cancelToken.cancel = async () => {
cancelToken.cancel = () => {
useAttachmentStore.getState().remove(filename);
uploadRequest.cancel();
};
const uploadResponse = await uploadRequest;
const status = uploadResponse.info().status;
const uploaded = status >= 200 && status < 300;
let uploadResponse = await uploadRequest;
let status = uploadResponse.info().status;
let uploaded = status >= 200 && status < 300;
useAttachmentStore.getState().remove(filename);
@@ -132,13 +118,12 @@ export async function uploadFile(
);
}
const attachment = await db.attachments.attachment(filename);
if (!attachment) return false;
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 as Error, "File upload failed");
ToastManager.error(e, "File upload failed");
DatabaseLogger.error(e, "File upload failed", {
filename
});

View File

@@ -17,11 +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 { hosts, RequestOptions } from "@notesnook/core";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import * as ScopedStorage from "react-native-scoped-storage";
import { DatabaseLogger, db } from "../database";
import { hosts } from "@notesnook/core";
export const ABYTES = 17;
export const cacheDirOld = RNFetchBlob.fs.dirs.CacheDir;
@@ -31,13 +31,18 @@ export const cacheDir =
? RNFetchBlob.fs.dirs.LibraryDir + "/.cache"
: RNFetchBlob.fs.dirs.DocumentDir + "/.cache";
export function getRandomId(prefix: string) {
export function getRandomId(prefix) {
return Math.random()
.toString(36)
.replace("0.", prefix || "");
}
export function parseS3Error(data?: string) {
/**
*
* @param {string | undefined} data
* @returns
*/
export function parseS3Error(data) {
const xml = typeof data === "string" ? data : null;
const error = {
@@ -65,19 +70,11 @@ export function parseS3Error(data?: string) {
}
}
export function cancelable(
operation: (
filename: string,
requestOptions: RequestOptions,
cancelToken: {
cancel: (reason?: string) => Promise<void>;
}
) => Promise<boolean>
) {
export function cancelable(operation) {
const cancelToken = {
cancel: async (reason?: string) => {}
cancel: () => {}
};
return (filename: string, requestOptions: RequestOptions) => {
return (filename, requestOptions) => {
return {
execute: () => operation(filename, requestOptions, cancelToken),
cancel: async () => {
@@ -87,35 +84,29 @@ export function cancelable(
};
}
export function copyFileAsync(source: string, dest: string) {
export function copyFileAsync(source, dest) {
return new Promise((resolve, reject) => {
//@ts-ignore
ScopedStorage.copyFile(source, dest, (e: any, r: any) => {
ScopedStorage.copyFile(source, dest, (e, r) => {
if (e) {
reject(e);
return;
}
resolve(true);
resolve();
});
});
}
export async function releasePermissions(path: string) {
export async function releasePermissions(path) {
if (Platform.OS === "ios") return;
const uris = await ScopedStorage.getPersistedUriPermissions();
for (const uri of uris) {
for (let uri of uris) {
if (path.startsWith(uri)) {
await ScopedStorage.releasePersistableUriPermission(uri);
}
}
}
export const FileSizeResult = {
Empty: 0,
Error: -1
};
export async function getUploadedFileSize(hash: string) {
export async function getUploadedFileSize(hash) {
try {
const url = `${hosts.API_HOST}/s3?name=${hash}`;
const token = await db.tokenManager.getAccessToken();
@@ -124,20 +115,16 @@ export async function getUploadedFileSize(hash: string) {
headers: { Authorization: `Bearer ${token}` }
});
const contentLength = parseInt(
attachmentInfo.headers?.get("content-length") || "0"
attachmentInfo.headers?.get("content-length")
);
return isNaN(contentLength) ? FileSizeResult.Empty : contentLength;
return isNaN(contentLength) ? 0 : contentLength;
} catch (e) {
DatabaseLogger.error(e);
return FileSizeResult.Error;
return -1;
}
}
export async function checkUpload(
filename: string,
chunkSize: number,
expectedSize: number
) {
export async function checkUpload(filename, chunkSize, expectedSize) {
const size = await getUploadedFileSize(filename);
const totalChunks = Math.ceil(size / chunkSize);
const decryptedLength = size - totalChunks * ABYTES;
@@ -151,25 +138,3 @@ export async function checkUpload(
: undefined;
if (error) throw new Error(error);
}
export async function requestPermission() {
if (Platform.OS === "ios") return true;
return true;
}
export async function checkAndCreateDir(path: string) {
const dir =
Platform.OS === "ios"
? RNFetchBlob.fs.dirs.DocumentDir + path
: RNFetchBlob.fs.dirs.SDCardDir + "/Notesnook/" + path;
try {
const exists = await RNFetchBlob.fs.exists(dir);
const isDir = await RNFetchBlob.fs.isDir(dir);
if (!exists || !isDir) {
await RNFetchBlob.fs.mkdir(dir);
}
} catch (e) {
await RNFetchBlob.fs.mkdir(dir);
}
return dir;
}

View File

@@ -53,7 +53,6 @@ import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import { strings } from "@notesnook/intl";
const getUser = () => {
const user = MMKV.getString("user");
@@ -158,7 +157,7 @@ const AppLockedOverlay = () => {
password.current = undefined;
} else {
ToastManager.show({
heading: strings.invalid(keyboardType),
heading: `Invalid ${keyboardType === "numeric" ? "pin" : "password"}`,
type: "error",
context: "local"
});
@@ -251,7 +250,7 @@ const AppLockedOverlay = () => {
textAlign: "center"
}}
>
{strings.unlockNotes()}
Unlock your notes
</Heading>
<Paragraph
@@ -261,7 +260,7 @@ const AppLockedOverlay = () => {
maxWidth: "90%"
}}
>
{strings.verifyItsYou()}
{"Please verify it's you"}
</Paragraph>
<Seperator />
<View
@@ -279,13 +278,13 @@ const AppLockedOverlay = () => {
keyboardType={
appLockHasPasswordSecurity ? keyboardType : "default"
}
placeholder={
placeholder={`Enter ${
appLockHasPasswordSecurity
? keyboardType === "numeric"
? strings.enterApplockPassword()
: strings.enterApplockPin()
: strings.enterAccountPassword()
}
? `app lock ${
keyboardType === "numeric" ? "pin" : "password"
}`
: "account password"
}`}
onChangeText={(v) => (password.current = v)}
onSubmit={() => {
onSubmit();
@@ -302,7 +301,7 @@ const AppLockedOverlay = () => {
{user || appLockHasPasswordSecurity ? (
<>
<Button
title={strings.continue()}
title="Continue"
type="accent"
onPress={onSubmit}
width={250}
@@ -318,7 +317,7 @@ const AppLockedOverlay = () => {
{biometricsAuthEnabled ? (
<Button
title={strings.unlockWithBiometrics()}
title="Unlock with Biometrics"
width={250}
onPress={onUnlockAppRequested}
icon={"fingerprint"}

View File

@@ -57,17 +57,14 @@ import { Notice } from "../ui/notice";
import { Pressable } from "../ui/pressable";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
const Actions = ({
attachment,
close,
setAttachments,
fwdRef,
context
fwdRef
}: {
attachment: Attachment;
context: string;
setAttachments: (attachments?: VirtualizedGrouping<Attachment>) => void;
close?: () => void;
fwdRef: RefObject<ActionSheetRef>;
@@ -81,25 +78,26 @@ const Actions = ({
const [loading, setLoading] = useState<{
name?: string;
}>({});
const actions = [
{
name: strings.network.download(),
name: "Download",
onPress: async () => {
if (currentProgress) {
await db.fs().cancel(attachment.hash);
useAttachmentStore.getState().remove(attachment.hash);
}
downloadAttachment(attachment.hash, context === "global");
downloadAttachment(attachment.hash, false);
fwdRef.current?.hide();
},
icon: "download"
},
{
name: strings.network.reupload(),
name: "Reupload",
onPress: async () => {
if (!PremiumService.get()) {
ToastManager.show({
heading: strings.upgradeToPro(),
heading: "Upgrade to pro",
type: "error",
context: "local"
});
@@ -115,10 +113,10 @@ const Actions = ({
icon: "upload"
},
{
name: strings.fileCheck(),
name: "Run file check",
onPress: async () => {
setLoading({
name: strings.fileCheck()
name: "Run file check"
});
const result = await filesystem.checkAttachment(attachment.hash);
if (!result) return;
@@ -127,7 +125,7 @@ const Actions = ({
db.attachments.markAsFailed(attachment.id, result.failed);
setFailed(result.failed);
ToastManager.show({
heading: strings.fileCheckFailed(result.failed),
heading: "File check failed with error: " + result.failed,
type: "error",
context: "local"
});
@@ -136,7 +134,7 @@ const Actions = ({
db.attachments.markAsFailed(attachment.id);
eSendEvent(eDBItemUpdate, attachment.id);
ToastManager.show({
heading: strings.fileCheckPassed(),
heading: "File check passed",
type: "success",
context: "local"
});
@@ -150,11 +148,12 @@ const Actions = ({
icon: "file-check"
},
{
name: strings.rename(),
onPress: () => {
name: "Rename",
onPress: async () => {
presentDialog({
input: true,
title: strings.renameFile(),
title: "Rename file",
paragraph: "Enter a new name for the file",
defaultValue: attachment.filename,
positivePress: async (value) => {
if (value && value.length > 0) {
@@ -167,13 +166,13 @@ const Actions = ({
eSendEvent(eDBItemUpdate, attachment.id);
}
},
positiveText: strings.rename()
positiveText: "Rename"
});
},
icon: "form-textbox"
},
{
name: strings.delete(),
name: "Delete",
onPress: async () => {
const relations = await db.relations.to(attachment, "note").get();
await db.attachments.remove(attachment.hash, false);
@@ -257,7 +256,8 @@ const Actions = ({
{notes.length ? (
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
{strings.notes(notes.length)}
{notes.length} note
{notes.length > 1 ? "s" : ""}
</Paragraph>
) : null}
<Paragraph
@@ -265,7 +265,7 @@ const Actions = ({
Clipboard.setString(attachment.hash);
ToastManager.show({
type: "success",
heading: strings.hashCopied(),
heading: "Attachment hash copied",
context: "local"
});
}}
@@ -295,7 +295,7 @@ const Actions = ({
}}
size={SIZE.sm}
>
{strings.listOf()} {strings.dataTypesPlural.note()}:
List of notes:
</Heading>
{notes.map((item) => (
@@ -354,7 +354,7 @@ const Actions = ({
{failed ? (
<Notice
type="alert"
text={strings.fileCheckFailed(failed)}
text={`File check failed: ${failed} Try reuploading the file to fix the issue.`}
size="small"
/>
) : null}
@@ -376,7 +376,6 @@ Actions.present = (
setAttachments={set}
close={close}
attachment={attachment}
context={context || "global"}
/>
)
});

View File

@@ -30,8 +30,6 @@ import { IconButton } from "../ui/icon-button";
import { ProgressCircleComponent } from "../ui/svg/lazy";
import Paragraph from "../ui/typography/paragraph";
import Actions from "./actions";
import { strings } from "@notesnook/intl";
import { Pressable } from "../ui/pressable";
function getFileExtension(filename: string) {
const ext = /^.+\.([^.]+)$/.exec(filename);
@@ -70,7 +68,8 @@ export const AttachmentItem = ({
};
return errorOnly && attachment && !attachment?.failed ? null : (
<Pressable
<TouchableOpacity
activeOpacity={0.9}
onPress={onPress}
style={{
flexDirection: "row",
@@ -137,7 +136,7 @@ export const AttachmentItem = ({
{!hideWhenNotDownloading ? (
<Paragraph color={colors.secondary.paragraph} size={SIZE.xxs}>
{strings.fileSize()}: {formatBytes(attachment.size)}
File size: {formatBytes(attachment.size)}
</Paragraph>
) : null}
</View>
@@ -186,6 +185,6 @@ export const AttachmentItem = ({
)}
</>
)}
</Pressable>
</TouchableOpacity>
);
};

View File

@@ -24,7 +24,6 @@ import {
SortOptions,
VirtualizedGrouping
} from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import { ActivityIndicator, View } from "react-native";
@@ -95,31 +94,31 @@ const useRechecker = create<RecheckerState>((set) => ({
const attachmentTypes = [
{
title: strings.mediaTypes.all(),
title: "All",
filterBy: "all"
},
{
title: strings.mediaTypes.image(),
title: "Images",
filterBy: "images"
},
{
title: strings.mediaTypes.audio(),
filterBy: "audio"
},
{
title: strings.mediaTypes.video(),
filterBy: "video"
},
{
title: strings.mediaTypes.document(),
title: "Docs",
filterBy: "documents"
},
{
title: strings.mediaTypes.orphaned(),
title: "Video",
filterBy: "video"
},
{
title: "Audio",
filterBy: "audio"
},
{
title: "Orphaned",
filterBy: "orphaned"
},
{
title: strings.mediaTypes.errors(),
title: "Errors",
filterBy: "errors"
}
];
@@ -206,7 +205,7 @@ export const AttachmentDialog = ({
errorOnly={currentFilter === "errors"}
attachments={attachments}
id={index}
context={!isSheet ? "global" : "attachments-list"}
context="global"
/>
);
@@ -235,7 +234,7 @@ export const AttachmentDialog = ({
for (let i = 0; i < filteredAttachments.placeholders.length; i++) {
if (!getState().isWorking) {
ToastManager.show({
message: strings.attachmentRecheckCancelled(),
message: "Attachment recheck cancelled",
type: "info",
context: isSheet ? "local" : "global"
});
@@ -322,7 +321,7 @@ export const AttachmentDialog = ({
) : null}
{!isSheet ? (
<Header
title={strings.manageAttachments()}
title="Manage attachments"
renderedInRoute="SettingsGroup"
canGoBack
headerRightButtons={[
@@ -330,23 +329,23 @@ export const AttachmentDialog = ({
onPress() {
onCheck();
},
title: strings.recheckAll()
title: "Recheck all"
},
{
onPress() {
if (!attachments) return;
presentDialog({
title: strings.doActions.download.attachment(
attachments.placeholders.length
),
positiveText: strings.network.download(),
title: `Download ${attachments.placeholders.length} attachments`,
paragraph:
"Are you sure you want to download all attachments?",
positiveText: "Download",
positivePress: async () => {
downloadAttachments(await attachments.ids());
},
negativeText: strings.cancel()
negativeText: "Cancel"
});
},
title: strings.downloadAllAttachments()
title: "Download all"
}
]}
/>
@@ -359,7 +358,7 @@ export const AttachmentDialog = ({
paddingHorizontal: 12
}}
>
<Heading>{strings.dataTypesPluralCamelCase.attachment()}</Heading>
<Heading>Attachments</Heading>
<View
style={{
@@ -388,14 +387,15 @@ export const AttachmentDialog = ({
onPress={() => {
if (!attachments) return;
presentDialog({
title: strings.doActions.download.attachment(
attachments.placeholders.length
),
positiveText: strings.network.download(),
title: `Download ${attachments.placeholders.length} attachments`,
paragraph:
"Are you sure you want to download all attachments?",
context: "local",
positiveText: "Download",
positivePress: async () => {
downloadAttachments(await attachments.ids());
},
negativeText: strings.cancel()
negativeText: "Cancel"
});
}}
size={SIZE.lg}
@@ -414,7 +414,7 @@ export const AttachmentDialog = ({
>
<Seperator />
<Input
placeholder={strings.filterAttachments()}
placeholder="Filter attachments by filename, type or hash"
onChangeText={onChangeText}
onSubmit={() => {
onChangeText(attachmentSearchValue.current as string);
@@ -456,16 +456,14 @@ export const AttachmentDialog = ({
<Paragraph>
{rechecker.isWorking
? note
? strings.checkingNoteAttachments()
: strings.checkingAllAttachments()
: strings.attachmentRecheckComplete()}
? `Checking ${rechecker.filter.toLowerCase()} note attachments`
: `Checking ${rechecker.filter.toLowerCase()} attachments`
: "Attachments recheck complete"}
</Paragraph>
<Paragraph>
{`${
rechecker.isWorking ? `${strings.pleaseWait()} ` : ""
}${strings.passed()}: ${
{`${rechecker.isWorking ? "Please wait... " : ""}Passed: ${
rechecker.passed
}, ${strings.failed()}: ${rechecker.failed}`}
}, Failed: ${rechecker.failed}`}
</Paragraph>
</View>
</View>
@@ -556,7 +554,9 @@ export const AttachmentDialog = ({
size={60}
color={colors.secondary.icon}
/>
<Paragraph>{strings.noAttachments()}</Paragraph>
<Paragraph>
{note ? "No attachments on this note" : "No attachments"}
</Paragraph>
</>
)}
</View>

View File

@@ -36,7 +36,6 @@ import { IconButton } from "../ui/icon-button";
import { hideAuth, initialAuthMode } from "./common";
import { Login } from "./login";
import { Signup } from "./signup";
import { strings } from "@notesnook/intl";
export const AuthMode = {
login: 0,
@@ -154,7 +153,7 @@ const AuthModal = () => {
{initialAuthMode.current !== AuthMode.welcomeSignup ? null : (
<Button
title={strings.skip()}
title="Skip"
onPress={() => {
hideAuth();
}}

View File

@@ -35,7 +35,6 @@ import Seperator from "../ui/seperator";
import { Dialog } from "../dialog";
import BackupService from "../../services/backup";
import { sleep } from "../../utils/time";
import { strings } from "@notesnook/intl";
export const ChangePassword = () => {
const passwordInputRef = useRef();
@@ -50,8 +49,8 @@ export const ChangePassword = () => {
const changePassword = async () => {
if (!user?.isEmailConfirmed) {
ToastManager.show({
heading: strings.emailNotConfirmed(),
message: strings.emailNotConfirmedDesc(),
heading: "Email not confirmed",
message: "Please confirm your email to change account password",
type: "error",
context: "local"
});
@@ -59,8 +58,8 @@ export const ChangePassword = () => {
}
if (error || !oldPassword.current || !password.current) {
ToastManager.show({
heading: strings.allFieldsRequired(),
message: strings.allFieldsRequiredDesc(),
heading: "All fields required",
message: "Fill all the fields and try again.",
type: "error",
context: "local"
});
@@ -68,18 +67,14 @@ export const ChangePassword = () => {
}
setLoading(true);
try {
const result = await BackupService.run(
false,
"change-password-dialog",
"partial"
);
if (result.error) {
throw new Error(strings.backupFailed() + `: ${result.error}`);
}
const result = await BackupService.run(false, "change-password-dialog");
if (result.error)
throw new Error(`Failed to create backup: ${result.error}`);
await db.user.clearSessions();
await db.user.changePassword(oldPassword.current, password.current);
ToastManager.show({
heading: strings.passwordChangedSuccessfully(),
heading: "Account password updated",
type: "success",
context: "global"
});
@@ -88,10 +83,9 @@ export const ChangePassword = () => {
await sleep(300);
eSendEvent(eOpenRecoveryKeyDialog);
} catch (e) {
console.log(e.stack);
setLoading(false);
ToastManager.show({
heading: strings.passwordChangeFailed(),
heading: "Failed to change password",
message: e.message,
type: "error",
context: "local"
@@ -108,7 +102,10 @@ export const ChangePassword = () => {
}}
>
<Dialog context="change-password-dialog" />
<DialogHeader title={strings.changePassword()} />
<DialogHeader
title="Change password"
paragraph="Enter your old and new passwords"
/>
<Seperator />
<Input
@@ -122,7 +119,7 @@ export const ChangePassword = () => {
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder={strings.oldPassword()}
placeholder="Old Password"
/>
<Input
@@ -131,21 +128,27 @@ export const ChangePassword = () => {
password.current = value;
}}
onErrorCheck={(e) => setError(e)}
returnKeyLabel={strings.next()}
returnKeyLabel="Next"
returnKeyType="next"
secureTextEntry
validationType="password"
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder={strings.newPassword()}
placeholder="New password"
/>
<Notice text={strings.changePasswordNotice()} type="alert" />
<Notice
text={`Changing password is an irreversible process. You will be logged out from all your devices. Please make sure you do not close the app while your password is changing and have good internet connection.`}
type="alert"
/>
<View style={{ height: 10 }} />
<Notice text={strings.changePasswordNotice2()} type="alert" />
<Notice
text={`Once your password is changed, please make sure to save the new account recovery key.`}
type="alert"
/>
<Button
style={{
@@ -155,7 +158,7 @@ export const ChangePassword = () => {
loading={loading}
onPress={changePassword}
type="accent"
title={loading ? null : strings.changePasswordConfirm()}
title={loading ? null : "I understand, change my password"}
/>
</View>
);

View File

@@ -32,7 +32,6 @@ import Input from "../ui/input";
import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
export const ForgotPassword = () => {
const { colors } = useThemeColors("sheet");
@@ -45,7 +44,7 @@ export const ForgotPassword = () => {
const sendRecoveryEmail = async () => {
if (!email.current || error) {
ToastManager.show({
heading: strings.emailRequired(),
heading: "Account email is required.",
type: "error",
context: "local"
});
@@ -58,15 +57,15 @@ export const ForgotPassword = () => {
lastRecoveryEmailTime &&
Date.now() - JSON.parse(lastRecoveryEmailTime) < 60000 * 3
) {
throw new Error(strings.pleaseWaitBeforeSendEmail());
throw new Error("Please wait before requesting another email");
}
await db.user.recoverAccount(email.current.toLowerCase());
SettingsService.set({
lastRecoveryEmailTime: Date.now()
});
ToastManager.show({
heading: strings.recoveryEmailSent(),
message: strings.recoveryEmailSentDesc(),
heading: "Check your email to reset password",
message: `Recovery email has been sent to ${email.current.toLowerCase()}`,
type: "success",
context: "local",
duration: 7000
@@ -76,7 +75,7 @@ export const ForgotPassword = () => {
} catch (e) {
setLoading(false);
ToastManager.show({
heading: strings.recoveryEmailFailed(),
heading: "Recovery email not sent",
message: e.message,
type: "error",
context: "local"
@@ -122,13 +121,13 @@ export const ForgotPassword = () => {
name="email"
size={50}
/>
<Heading>{strings.recoveryEmailSent()}</Heading>
<Heading>Recovery email sent!</Heading>
<Paragraph
style={{
textAlign: "center"
}}
>
{strings.recoveryEmailSentDesc()}
Please follow the link in the email to recover your account.
</Paragraph>
</View>
) : (
@@ -141,7 +140,10 @@ export const ForgotPassword = () => {
padding: 12
}}
>
<DialogHeader title={strings.accountRecovery()} />
<DialogHeader
title="Account recovery"
paragraph="We will send you an email with steps on how to reset your password."
/>
<Seperator />
<Input
@@ -151,14 +153,14 @@ export const ForgotPassword = () => {
}}
defaultValue={email.current}
onErrorCheck={(e) => setError(e)}
returnKeyLabel={strings.next()}
returnKeyLabel="Next"
returnKeyType="next"
autoComplete="email"
validationType="email"
autoCorrect={false}
autoCapitalize="none"
errorMessage={strings.emailInvalid()}
placeholder={strings.email()}
errorMessage="Email is invalid"
placeholder="Email"
onSubmit={() => {}}
/>
@@ -170,7 +172,7 @@ export const ForgotPassword = () => {
loading={loading}
onPress={sendRecoveryEmail}
type="accent"
title={loading ? null : strings.next()}
title={loading ? null : "Next"}
/>
</View>
)}

View File

@@ -38,7 +38,6 @@ import Paragraph from "../ui/typography/paragraph";
import { hideAuth } from "./common";
import { ForgotPassword } from "./forgot-password";
import { useLogin } from "./use-login";
import { strings } from "@notesnook/intl";
const LoginSteps = {
emailAuth: 1,
@@ -148,7 +147,7 @@ export const Login = ({ changeMode }) => {
extraBold
size={SIZE.xxl}
>
{strings.loginToYourAccount()}
Login to your {"\n"}account
</Heading>
</View>
@@ -179,8 +178,8 @@ export const Login = ({ changeMode }) => {
validationType="email"
autoCorrect={false}
autoCapitalize="none"
errorMessage={strings.emailInvalid()}
placeholder={strings.email()}
errorMessage="Email is invalid"
placeholder="Enter your email"
defaultValue={email.current}
editable={step === LoginSteps.emailAuth && !loading}
onSubmit={() => {
@@ -200,20 +199,20 @@ export const Login = ({ changeMode }) => {
password.current = value;
}}
testID="input.password"
returnKeyLabel={strings.done()}
returnKeyLabel="Done"
returnKeyType="done"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder={strings.password()}
placeholder="Password"
marginBottom={0}
editable={!loading}
defaultValue={password.current}
onSubmit={() => login()}
/>
<Button
title={strings.forgotPassword()}
title="Forgot your password?"
style={{
alignSelf: "flex-end",
height: 30,
@@ -250,12 +249,12 @@ export const Login = ({ changeMode }) => {
height={50}
fontSize={SIZE.md}
type="accent"
title={!loading ? strings.continue() : null}
title={!loading ? "Continue" : null}
/>
{step === LoginSteps.passwordAuth && (
<Button
title={strings.cancelLogin()}
title="Cancel logging in"
style={{
alignSelf: "center",
height: 30,
@@ -291,12 +290,12 @@ export const Login = ({ changeMode }) => {
size={SIZE.xs + 1}
color={colors.secondary.paragraph}
>
{strings.dontHaveAccount()}{" "}
Don't have an account?{" "}
<Paragraph
size={SIZE.xs + 1}
style={{ color: colors.primary.accent }}
>
{strings.signUp()}
Sign up
</Paragraph>
</Paragraph>
</TouchableOpacity>

View File

@@ -47,7 +47,6 @@ import Input from "../ui/input";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { LoginSteps, useLogin } from "./use-login";
import { strings } from "@notesnook/intl";
function getObfuscatedEmail(email) {
if (!email) return "";
@@ -194,14 +193,15 @@ export const SessionExpired = () => {
size={50}
/>
<Heading size={SIZE.xxxl} color={colors.primary.heading}>
{strings.sessionExpired()}
Session expired
</Heading>
<Paragraph
style={{
textAlign: "center"
}}
>
{strings.sessionExpiredDesc(getObfuscatedEmail(email.current))}
Your session on this device has expired. Please enter password for{" "}
{getObfuscatedEmail(email.current)} to continue.
</Paragraph>
</View>
@@ -211,13 +211,13 @@ export const SessionExpired = () => {
onChangeText={(value) => {
password.current = value;
}}
returnKeyLabel={strings.done()}
returnKeyLabel="Next"
returnKeyType="next"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder={strings.password()}
placeholder="Password"
onSubmit={() => login()}
/>
) : null}
@@ -231,7 +231,7 @@ export const SessionExpired = () => {
loading={loading}
onPress={() => login()}
type="accent"
title={loading ? null : strings.login()}
title={loading ? null : "Login"}
/>
<Button
@@ -242,15 +242,16 @@ export const SessionExpired = () => {
onPress={() => {
presentDialog({
context: "session_expiry",
title: strings.logoutFromDevice(),
paragraph: strings.logoutDesc(),
positiveText: strings.logout(),
title: "Logout",
paragraph:
"Are you sure you want to logout from this device? Any unsynced changes will be lost.",
positiveText: "Logout",
positiveType: "errorShade",
positivePress: logout
});
}}
type="errorShade"
title={loading ? null : strings.logoutFromDevice()}
title={loading ? null : "Logout from this device"}
/>
</View>
<Toast context="local" />

View File

@@ -36,7 +36,6 @@ import Paragraph from "../ui/typography/paragraph";
import { hideAuth } from "./common";
import { useSettingStore } from "../../stores/use-setting-store";
import SettingsService from "../../services/settings";
import { strings } from "@notesnook/intl";
export const Signup = ({ changeMode, trial }) => {
const { colors } = useThemeColors();
@@ -55,8 +54,8 @@ export const Signup = ({ changeMode, trial }) => {
const validateInfo = () => {
if (!password.current || !email.current || !confirmPassword.current) {
ToastManager.show({
heading: strings.allFieldsRequired(),
message: strings.allFieldsRequiredDesc(),
heading: "All fields required",
message: "Fill all the fields and try again",
type: "error",
context: "local"
});
@@ -89,7 +88,7 @@ export const Signup = ({ changeMode, trial }) => {
} catch (e) {
setLoading(false);
ToastManager.show({
heading: strings.signupFailed(),
heading: "Signup failed",
message: e.message,
type: "error",
context: "local"
@@ -157,7 +156,7 @@ export const Signup = ({ changeMode, trial }) => {
}}
size={SIZE.xxl}
>
{strings.createYourAccount()}
Create your {"\n"}account
</Heading>
</View>
@@ -176,14 +175,14 @@ export const Signup = ({ changeMode, trial }) => {
}}
testID="input.email"
onErrorCheck={(e) => setError(e)}
returnKeyLabel={strings.next()}
returnKeyLabel="Next"
returnKeyType="next"
autoComplete="email"
validationType="email"
autoCorrect={false}
autoCapitalize="none"
errorMessage={strings.email()}
placeholder={strings.email()}
errorMessage="Email is invalid"
placeholder="Email"
onSubmit={() => {
passwordInputRef.current?.focus();
}}
@@ -196,14 +195,14 @@ export const Signup = ({ changeMode, trial }) => {
}}
testID="input.password"
onErrorCheck={(e) => setError(e)}
returnKeyLabel={strings.next()}
returnKeyLabel="Next"
returnKeyType="next"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
validationType="password"
autoCorrect={false}
placeholder={strings.password()}
placeholder="Password"
onSubmit={() => {
confirmPasswordInputRef.current?.focus();
}}
@@ -216,7 +215,7 @@ export const Signup = ({ changeMode, trial }) => {
}}
testID="input.confirmPassword"
onErrorCheck={(e) => setError(e)}
returnKeyLabel={strings.done()}
returnKeyLabel="Signup"
returnKeyType="done"
secureTextEntry
autoComplete="password"
@@ -224,7 +223,7 @@ export const Signup = ({ changeMode, trial }) => {
autoCorrect={false}
validationType="confirmPassword"
customValidator={() => password.current}
placeholder={strings.confirmPassword()}
placeholder="Confirm password"
marginBottom={12}
onSubmit={signup}
/>
@@ -236,7 +235,7 @@ export const Signup = ({ changeMode, trial }) => {
size={SIZE.xs}
color={colors.secondary.paragraph}
>
{strings.signupAgreement[0]()}
By signing up, you agree to our{" "}
<Paragraph
size={SIZE.xs}
onPress={() => {
@@ -247,9 +246,9 @@ export const Signup = ({ changeMode, trial }) => {
}}
color={colors.primary.accent}
>
{strings.signupAgreement[1]()}
Terms of Service{" "}
</Paragraph>
{strings.signupAgreement[2]()}
and{" "}
<Paragraph
size={SIZE.xs}
onPress={() => {
@@ -260,13 +259,14 @@ export const Signup = ({ changeMode, trial }) => {
}}
color={colors.primary.accent}
>
{strings.signupAgreement[3]()}
</Paragraph>
{strings.signupAgreement[4]()}
Privacy Policy.
</Paragraph>{" "}
You also agree to recieve marketing emails from us which you can
opt-out of from app settings.
</Paragraph>
<Button
title={!loading ? strings.continue() : null}
title={!loading ? "Continue" : null}
type="accent"
loading={loading}
onPress={signup}
@@ -291,12 +291,12 @@ export const Signup = ({ changeMode, trial }) => {
}}
>
<Paragraph size={SIZE.xs + 1} color={colors.secondary.paragraph}>
{strings.alreadyHaveAccount()}{" "}
Already have an account?{" "}
<Paragraph
size={SIZE.xs + 1}
style={{ color: colors.primary.accent }}
>
{strings.login()}
Login
</Paragraph>
</Paragraph>
</TouchableOpacity>

View File

@@ -17,11 +17,8 @@ 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, { useCallback, useEffect, useRef, useState } from "react";
import React, { 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 {
@@ -29,16 +26,20 @@ 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";
const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
const { colors } = useThemeColors();
const code = useRef();
const [currentMethod, setCurrentMethod] = useState({
@@ -50,6 +51,20 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
const inputRef = useRef();
const [sending, setSending] = useState(false);
const codeHelpText = {
app: "Enter the 6 digit code from your authenticator app to continue logging in",
sms: "Enter the 6 digit code sent to your phone number to continue logging in",
email: "Enter the 6 digit code sent to your email to continue logging in",
recoveryCode: "Enter the recovery code to continue logging in"
};
const secondaryMethodsText = {
app: "I don't have access to authenticator app",
sms: "I don't have access to my phone",
email: "I don't have access to email",
recoveryCode: "I don't have recovery codes"
};
const onNext = async () => {
if (!code.current || code.current.length < 6) return;
setLoading(true);
@@ -79,22 +94,22 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
const methods = [
{
id: "sms",
title: strings.sendCodeSms(),
title: "Send code via SMS",
icon: "message-plus-outline"
},
{
id: "email",
title: strings.sendCodeEmail(),
title: "Send code via email",
icon: "email-outline"
},
{
id: "app",
title: strings.authAppCode(),
title: "Enter code from authenticator app",
icon: "cellphone-key"
},
{
id: "recoveryCode",
title: strings.recoveryCode(),
title: "I have a recovery code",
icon: "key"
}
];
@@ -136,8 +151,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
<View
style={{
alignItems: "center",
paddingHorizontal: currentMethod.method ? 12 : 0,
gap: 12
paddingHorizontal: currentMethod.method ? 12 : 0
}}
>
<IconButton
@@ -154,7 +168,9 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
textAlign: "center"
}}
>
{currentMethod.method ? strings["2fa"]() : strings.select2faMethod()}
{currentMethod.method
? "Two factor authentication"
: "Select methods for two-factor authentication"}
</Heading>
<Paragraph
style={{
@@ -162,10 +178,12 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
textAlign: "center"
}}
>
{strings["2faCodeHelpText"][currentMethod.method]?.() ||
strings.select2faCodeHelpText()}
{codeHelpText[currentMethod.method] ||
"Select how you would like to recieve the code"}
</Paragraph>
<Seperator />
{currentMethod.method === "sms" || currentMethod.method === "email" ? (
<Button
onPress={onSendCode}
@@ -173,17 +191,15 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
title={
sending
? ""
: `${
seconds
? strings.resend2faCode(seconds)
: strings.sendCode()
}`
: `${seconds ? `Resend code in (${seconds})` : "Send code"}`
}
loading={sending}
height={30}
/>
) : null}
<Seperator />
{currentMethod.method ? (
<>
<Input
@@ -222,33 +238,25 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
minWidth: "50%"
}}
/>
<Seperator />
<Button
title={loading ? null : strings.next()}
title={loading ? null : "Next"}
type="accent"
width={250}
loading={loading}
onPress={onNext}
style={{
borderRadius: 100
borderRadius: 100,
marginBottom: 10
}}
/>
<Button
title={strings["2faCodeSecondaryMethodText"][
currentMethod.method
]()}
title={secondaryMethodsText[currentMethod.method]}
type="plain"
onPress={onRequestSecondaryMethod}
height={30}
/>
<Button
title={strings.cancel()}
type="plain"
onPress={onCancel}
height={30}
/>
</>
) : (
<>
@@ -299,16 +307,15 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
);
};
TwoFactorVerification.present = (onMfaLogin, data, onCancel, context) => {
TwoFactorVerification.present = (onMfaLogin, data, context) => {
presentSheet({
component: () => (
<TwoFactorVerification
onMfaLogin={onMfaLogin}
mfaInfo={data}
onCancel={onCancel}
/>
<TwoFactorVerification onMfaLogin={onMfaLogin} mfaInfo={data} />
),
context: context || "two_factor_verify",
onClose: () => {
onMfaLogin();
},
disableClosing: true
});
};

View File

@@ -26,7 +26,6 @@ import SettingsService from "../../services/settings";
import { useUserStore } from "../../stores/use-user-store";
import { eCloseSheet } from "../../utils/events";
import TwoFactorVerification from "./two-factor";
import { strings } from "@notesnook/intl";
export const LoginSteps = {
emailAuth: 1,
@@ -50,8 +49,8 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
(!email.current && step === LoginSteps.emailAuth)
) {
ToastManager.show({
heading: strings.allFieldsRequired(),
message: strings.allFieldsRequiredDesc(),
heading: "All fields required",
message: "Fill all the fields and try again",
type: "error",
context: "local"
});
@@ -72,41 +71,33 @@ 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()));
finishWithError(new Error("Unable to send 2FA code"));
}
break;
}
@@ -131,7 +122,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
if (e.message === "invalid_grant") setStep(LoginSteps.emailAuth);
setLoading(false);
ToastManager.show({
heading: strings.loginFailed(),
heading: "Login failed",
message: e.message,
type: "error",
context: "local"
@@ -140,13 +131,13 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
const finishLogin = async () => {
const user = await db.user.getUser();
if (!user) throw new Error(strings.emailOrPasswordIncorrect());
if (!user) throw new Error("Email or password incorrect!");
PremiumService.setPremiumStatus();
setUser(user);
clearMessage();
ToastManager.show({
heading: strings.loginSuccess(),
message: strings.loginSuccessDesc(),
heading: "Login successful",
message: `Logged in as ${user.email}`,
type: "success",
context: "global"
});

View File

@@ -34,9 +34,11 @@ import { useSelectionStore } from "../../stores/use-selection-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { getElevationStyle } from "../../utils/elevation";
import { SIZE, normalize } from "../../utils/size";
import NativeTooltip from "../../utils/tooltip";
import { Pressable } from "../ui/pressable";
interface FloatingButtonProps {
title: string;
onPress: () => void;
color?: string;
shouldShow?: boolean;
@@ -44,6 +46,7 @@ interface FloatingButtonProps {
}
const FloatingButton = ({
title,
onPress,
color,
alwaysVisible = false
@@ -123,6 +126,11 @@ const FloatingButton = ({
...getElevationStyle(5),
borderRadius: 100
}}
onLongPress={(event) => {
if (title) {
NativeTooltip.show(event, title, NativeTooltip.POSITIONS.LEFT);
}
}}
onPress={onPress}
>
<View

View File

@@ -38,7 +38,7 @@ import { useAppState } from "../../hooks/use-app-state";
export interface BaseDialogProps extends PropsWithChildren {
animation?: "fade" | "none" | "slide" | undefined;
visible?: boolean;
visible: boolean;
onRequestClose?: () => void;
onShow?: () => void;
premium?: boolean;

View File

@@ -26,13 +26,12 @@ import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import Paragraph from "../ui/typography/paragraph";
import { getColorLinearShade } from "../../utils/colors";
import { strings } from "@notesnook/intl";
const DialogButtons = ({
onPressPositive,
onPressNegative,
positiveTitle,
negativeTitle = strings.cancel(),
negativeTitle = "Cancel",
loading,
doneText,
positiveType

View File

@@ -38,7 +38,6 @@ import { useCallback } from "react";
import { Button } from "../ui/button";
import { getContainerBorder } from "../../utils/colors";
import { Notice } from "../ui/notice";
import { strings } from "@notesnook/intl";
export const Dialog = ({ context = "global" }) => {
const { colors } = useThemeColors();
@@ -51,8 +50,8 @@ export const Dialog = ({ context = "global" }) => {
const [dialogInfo, setDialogInfo] = useState({
title: "",
paragraph: "",
positiveText: strings.done(),
negativeText: strings.cancel(),
positiveText: "Done",
negativeText: "Cancel",
positivePress: () => {},
onClose: () => {},
positiveType: "transparent",

View File

@@ -49,7 +49,6 @@ import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import { strings } from "@notesnook/intl";
export const AppLockPassword = () => {
const { colors } = useThemeColors();
@@ -120,7 +119,22 @@ export const AppLockPassword = () => {
}}
>
<DialogHeader
title={strings.changeAppLockCredentials(mode, keyboardType)}
title={
mode === "change"
? `Change app lock ${keyboardType}`
: mode === "remove"
? `Remove app lock ${keyboardType}`
: `Set up app lock ${keyboardType}`
}
paragraph={
mode === "change"
? `Change app lock ${keyboardType}`
: mode === "remove"
? `Enter ${
accountPass ? "account password" : `app lock ${keyboardType}`
} to remove ${keyboardType}`
: `Set up a custom app lock ${keyboardType} to unlock the app`
}
icon="shield"
padding={12}
/>
@@ -143,15 +157,11 @@ export const AppLockPassword = () => {
}}
defaultValue={values.current.currentPassword}
autoComplete="password"
returnKeyLabel={strings.next()}
returnKeyLabel="Next"
keyboardType={keyboardType === "pin" ? "number-pad" : "default"}
returnKeyType="next"
secureTextEntry={secureTextEntry}
placeholder={
keyboardType === "pin"
? strings.currentPin()
: strings.currentPassword()
}
placeholder={`Current ${keyboardType}`}
/>
) : null}
@@ -169,9 +179,7 @@ export const AppLockPassword = () => {
keyboardType === "pin" && !accountPass ? "number-pad" : "default"
}
autoComplete="password"
returnKeyLabel={
mode !== "remove" ? strings.next() : strings.remove()
}
returnKeyLabel={mode !== "remove" ? "Next" : "Remove"}
returnKeyType={mode !== "remove" ? "next" : "done"}
secureTextEntry={secureTextEntry}
buttonLeft={
@@ -198,14 +206,10 @@ export const AppLockPassword = () => {
}
placeholder={
accountPass
? strings.enterAccountPassword()
? "Account password"
: mode === "change"
? keyboardType === "pin"
? strings.newPin()
: strings.newPassword()
: `${
keyboardType === "pin" ? strings.pin() : strings.password()
}`
? `New ${keyboardType}`
: `${keyboardType === "pin" ? "Pin" : "Password"}`
}
/>
@@ -224,14 +228,10 @@ export const AppLockPassword = () => {
customValidator={() => values.current.password || ""}
validationType="confirmPassword"
autoComplete="password"
returnKeyLabel={strings.done()}
returnKeyLabel="Done"
returnKeyType="done"
secureTextEntry={secureTextEntry}
placeholder={
keyboardType === "pin"
? strings.confirmPin()
: strings.confirmPassword()
}
placeholder={`Confirm ${keyboardType}`}
/>
) : null}
@@ -253,7 +253,7 @@ export const AppLockPassword = () => {
iconColor={
accountPass ? colors.primary.accent : colors.primary.icon
}
title={strings.useAccountPassword()}
title="Use account password instead"
style={{
width: "100%",
alignSelf: "flex-start",
@@ -272,7 +272,7 @@ export const AppLockPassword = () => {
if (mode === "create") {
if (!values.current.password || !values.current.confirmPassword) {
ToastManager.error(
new Error(strings.allFieldsRequired()),
new Error("All inputs are required"),
undefined,
"local"
);
@@ -281,7 +281,11 @@ export const AppLockPassword = () => {
if (values.current.password !== values.current.confirmPassword) {
ToastManager.error(
new Error(strings.mismatch(keyboardType)),
new Error(
`${
keyboardType === "pin" ? "Pin" : "Password"
} does not match`
),
undefined,
"local"
);
@@ -297,7 +301,7 @@ export const AppLockPassword = () => {
!values.current.confirmPassword
) {
ToastManager.error(
new Error(strings.allFieldsRequired()),
new Error("All inputs are required"),
undefined,
"local"
);
@@ -306,7 +310,11 @@ export const AppLockPassword = () => {
if (values.current.password !== values.current.confirmPassword) {
ToastManager.error(
new Error(strings.mismatch(keyboardType)),
new Error(
`${
keyboardType === "pin" ? "Pin" : "Password"
} does not match`
),
undefined,
"local"
);
@@ -318,7 +326,9 @@ export const AppLockPassword = () => {
if (!isCurrentPasswordCorrect) {
ToastManager.error(
new Error(strings.incorrect(keyboardType)),
new Error(
`${keyboardType === "pin" ? "Pin" : "Password"} incorrect`
),
undefined,
"local"
);
@@ -332,7 +342,7 @@ export const AppLockPassword = () => {
} else if (mode === "remove") {
if (!values.current.password) {
ToastManager.error(
new Error(strings.allFieldsRequired()),
new Error("All inputs are required"),
undefined,
"local"
);
@@ -347,8 +357,10 @@ export const AppLockPassword = () => {
ToastManager.error(
new Error(
accountPass
? strings.passwordIncorrect()
: strings.incorrect(keyboardType)
? "Account password incorrect"
: `${
keyboardType === "pin" ? "Pin" : "Password"
} incorrect`
),
undefined,
"local"
@@ -365,7 +377,7 @@ export const AppLockPassword = () => {
SettingsService.setProperty("appLockEnabled", false);
SettingsService.setPrivacyScreen(SettingsService.get());
ToastManager.show({
message: strings.applockDisabled(),
message: "App lock disabled",
type: "success"
});
}
@@ -379,13 +391,9 @@ export const AppLockPassword = () => {
close();
}}
positiveTitle={
mode === "remove"
? strings.remove()
: mode === "change"
? strings.change()
: strings.save()
mode === "remove" ? "Remove" : mode === "change" ? "Change" : "Save"
}
negativeTitle={strings.cancel()}
negativeTitle="Cancel"
positiveType="transparent"
loading={false}
doneText=""

View File

@@ -25,14 +25,13 @@ import {
View
} from "react-native";
import { Image as ImageType } from "react-native-image-crop-picker";
import { useThemeColors } from "@notesnook/theme";
import { useThemeColors } from "../../../../../../packages/theme/dist";
import { presentSheet } from "../../../services/event-manager";
import { SIZE } from "../../../utils/size";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import { Notice } from "../../ui/notice";
import Paragraph from "../../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
export default function AttachImage({
response,
@@ -65,7 +64,7 @@ export default function AttachImage({
}}
>
<Paragraph style={{ color: colors.primary.paragraph, marginBottom: 6 }}>
{strings.attachImageHeading(response?.length || 1)}
Attaching {response?.length} image(s):
</Paragraph>
<ScrollView horizontal>
{response?.map?.((item) => (
@@ -124,14 +123,14 @@ export default function AttachImage({
}}
size={SIZE.sm}
>
{strings.compress()} ({strings.recommended().toLowerCase()})
Compress (recommended)
</Paragraph>
</TouchableOpacity>
{!compress ? (
<Notice
type="alert"
text={strings.compressionOffNotice()}
text="Images uploaded without compression are slow to load and take more bandwidth. We recommend compressing images unless you need image in original quality."
size="small"
style={{
width: "100%",
@@ -141,7 +140,7 @@ export default function AttachImage({
) : (
<Notice
type="information"
text={strings.compressionOnNotice()}
text="Compressed images are uploaded in Full HD resolution and usually are good enough for most use cases."
size="small"
style={{
width: "100%",
@@ -151,7 +150,9 @@ export default function AttachImage({
)}
<Button
title={strings.attachImageHeading(response?.length || 1)}
title={`${
(response?.length || 0) > 1 ? "Attach Images" : "Attach Image"
}`}
type="accent"
width="100%"
onPress={() => {

View File

@@ -33,7 +33,6 @@ import { Toast } from "../../toast";
import { Button } from "../../ui/button";
import Input from "../../ui/input";
import { Pressable } from "../../ui/pressable";
import { strings } from "@notesnook/intl";
const HEX_COLOR_REGEX_ALPHA =
/^#(?:(?:[\da-fA-F]{3}){1,2}|(?:[\da-fA-F]{4}){1,2})$/;
@@ -149,37 +148,36 @@ const ColorPicker = ({
}}
testID="color-title-input"
defaultValue={title.current}
placeholder={title.current || strings.colorTitle()}
placeholder={title.current || "Color title"}
/>
<Button
title={strings.addColor()}
title="Add color"
style={{
marginBottom: 10
}}
onPress={async () => {
if (!selectedColor)
return ToastManager.error(
new Error(strings.noColorSelected()),
new Error("Select a color"),
undefined,
"color-picker"
);
if (!title.current)
return ToastManager.error(
new Error(strings.allFieldsRequired())
new Error("Enter a title for the color")
);
const exists = await db.colors.all.find((v) =>
v.and([v(`colorCode`, "==", selectedColor)])
);
if (exists)
return ToastManager.error(
new Error(strings.colorExists(selectedColor))
new Error(`Color #${selectedColor} already exists`)
);
const id = await db.colors.add({
title: title.current,
colorCode: selectedColor
});
if (!id) return;
useRelationStore.getState().update();
useMenuStore.getState().setColorNotes();
setVisible(false);

View File

@@ -45,7 +45,6 @@ import Paragraph from "../../ui/typography/paragraph";
import { sleep } from "../../../utils/time";
import { MMKV } from "../../../common/database/mmkv";
import { deleteCacheFileByPath } from "../../../common/filesystem/io";
import { strings } from "@notesnook/intl";
const WIN_WIDTH = Dimensions.get("window").width;
const WIN_HEIGHT = Dimensions.get("window").height;
@@ -138,10 +137,10 @@ const PDFPreview = () => {
presentDialog({
context: attachment?.hash,
input: true,
inputPlaceholder: strings.enterPassword(),
positiveText: strings.open(),
title: strings.pdfLocked(),
paragraph: strings.pdfLockedDesc(),
inputPlaceholder: "Enter password",
positiveText: "Unlock",
title: "Decrypt",
paragraph: "Please input password to view pdf.",
positivePress: (value) => {
setTimeout(() => {
setPassword(value);
@@ -188,7 +187,8 @@ const PDFPreview = () => {
}}
color={colors.static.white}
>
{strings.loadingWithProgress(progress?.percent)}
Loading {`${progress?.percent ? `(${progress?.percent})` : ""}`}
... Please wait
</Paragraph>
</Animated.View>
) : (

View File

@@ -164,7 +164,7 @@ export default function Progress() {
/>
</View>
{!data?.canHideProgress ? null : (
{data?.canHideProgress ? null : (
<Button
title={cancelCallback.current ? "Cancel" : "Hide"}
type="secondaryAccented"

View File

@@ -39,9 +39,10 @@ const ResultDialog = () => {
const { colors } = useThemeColors();
const [visible, setVisible] = useState(false);
const [dialogData, setDialogData] = useState({
title: "",
paragraph: "",
button: ""
title: "Thank you for signing up!",
paragraph:
"Try out all features of Notesnook free for 7 days. No limitations. No commitments.",
button: "Start taking notes"
});
useEffect(() => {
eSubscribeEvent(eOpenResultDialog, open);

View File

@@ -52,7 +52,6 @@ import { Button } from "../../ui/button";
import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import Paragraph from "../../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
export class VaultDialog extends Component {
constructor(props) {
@@ -77,7 +76,7 @@ export class VaultDialog extends Component {
changePassword: false,
copyNote: false,
revokeFingerprintAccess: false,
title: strings.goToEditor(),
title: "Unlock Note",
description: null,
clearVault: false,
deleteVault: false,
@@ -92,25 +91,46 @@ export class VaultDialog extends Component {
this.password = null;
this.confirmPassword = null;
this.newPassword = null;
this.title = !this.state.novault
? strings.createVault()
(this.title = !this.state.novault
? "Create Vault"
: this.state.fingerprintAccess
? strings.vaultFingerprintUnlock()
? "Vault Fingerprint Unlock"
: this.state.revokeFingerprintAccess
? strings.revokeVaultFingerprintUnlock()
? "Revoke Vault Fingerprint Unlock"
: this.state.changePassword
? strings.changeVaultPassword()
? "Change Vault Password"
: this.state.noteLocked
? this.state.deleteNote
? strings.deleteNote()
? "Delete note"
: this.state.share
? strings.shareNote()
? "Share note"
: this.state.copyNote
? strings.copyNote()
? "Copy note"
: this.state.goToEditor
? strings.goToEditor()
: strings.goToEditor()
: strings.lockNote();
? "Unlock note"
: "Unlock note"
: "Lock note"),
(this.description = !this.state.novault
? "Set a password to create vault"
: this.state.fingerprintAccess
? "Enter vault password to enable fingerprint unlocking."
: this.state.revokeFingerprintAccess
? "Disable vault fingerprint unlock "
: this.state.changePassword
? "Setup a new password for the vault."
: this.state.permanant
? "Enter password to remove note from vault."
: this.state.noteLocked
? this.state.deleteNote
? "Unlock note to delete it. If biometrics are not working, you can enter device pin to unlock vault."
: this.state.share
? "Unlock note to share it. If biometrics are not working, you can enter device pin to unlock vault."
: this.state.copyNote
? "Unlock note to copy it. If biometrics are not working, you can enter device pin to unlock vault."
: this.state.goToEditor
? "Unlock note to open it in editor."
: "Enter vault password to unlock note. If biometrics are not working, you can enter device pin to unlock vault."
: "Enter vault password to lock note. If biometrics are not working, you can enter device pin to lock note.");
}
componentDidMount() {
@@ -179,7 +199,7 @@ export class VaultDialog extends Component {
if (this.state.loading) {
ToastManager.show({
heading: this.state.title,
message: strings.pleaseWait() + "...",
message: "Please wait and do not close the app.",
type: "success",
context: "local"
});
@@ -214,7 +234,8 @@ export class VaultDialog extends Component {
if (!this.password) {
ToastManager.show({
heading: strings.passwordNotEntered(),
heading: "Password not entered",
message: "Enter a password for the vault and try again.",
type: "error",
context: "local"
});
@@ -224,7 +245,7 @@ export class VaultDialog extends Component {
if (!this.state.novault) {
if (this.password !== this.confirmPassword) {
ToastManager.show({
heading: strings.passwordNotMatched(),
heading: "Passwords do not match",
type: "error",
context: "local"
});
@@ -250,7 +271,7 @@ export class VaultDialog extends Component {
this._enrollFingerprint(this.newPassword);
}
ToastManager.show({
heading: strings.passwordUpdated(),
heading: "Vault password updated successfully",
type: "success",
context: "global"
});
@@ -262,7 +283,8 @@ export class VaultDialog extends Component {
});
if (e.message === db.vault.ERRORS.wrongPassword) {
ToastManager.show({
heading: strings.passwordIncorrect(),
heading: "Incorrect password",
message: "Please enter the correct password and try again",
type: "error",
context: "local"
});
@@ -273,7 +295,8 @@ export class VaultDialog extends Component {
} else if (this.state.locked) {
if (!this.password || this.password.trim() === 0) {
ToastManager.show({
heading: strings.passwordIncorrect(),
heading: "Incorrect password",
message: "Please enter the correct password and try again",
type: "error",
context: "local"
});
@@ -311,10 +334,8 @@ export class VaultDialog extends Component {
loading: true
});
try {
let verified = true;
if (await db.user.getUser()) {
verified = await db.user.verifyPassword(this.password);
}
let verified = await db.user.verifyPassword(this.password);
if (!(await db.user.getUser())) verified = true;
if (verified) {
let noteIds = [];
if (this.state.deleteAll) {
@@ -322,21 +343,20 @@ 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);
if (this.state.deleteAll) {
noteIds.forEach((id) => {
eSendEvent(
eUpdateNoteInEditor,
{
id: id,
deleted: true
},
true
);
});
}
console.log("VAULT UPDATED EVENT");
noteIds.forEach((id) => {
eSendEvent(
eUpdateNoteInEditor,
{
id: id,
deleted: true
},
true
);
});
eSendEvent("vaultUpdated");
this.setState({
loading: false
@@ -344,7 +364,8 @@ export class VaultDialog extends Component {
this.close();
} else {
ToastManager.show({
heading: strings.passwordIncorrect(),
heading: "Account password incorrect",
message: "Please enter correct password for your account.",
type: "error",
context: "local"
});
@@ -385,7 +406,8 @@ export class VaultDialog extends Component {
eSendEvent("vaultUpdated");
} catch (e) {
ToastManager.show({
heading: strings.passwordIncorrect(),
heading: "Vault password incorrect",
message: "Please enter correct password to clear vault.",
type: "error",
context: "local"
});
@@ -398,7 +420,7 @@ export class VaultDialog extends Component {
async _lockNote() {
if (!this.password || this.password.trim() === 0) {
ToastManager.show({
heading: strings.passwordIncorrect(),
heading: "Incorrect password",
type: "error",
context: "local"
});
@@ -411,7 +433,7 @@ export class VaultDialog extends Component {
this.close();
ToastManager.show({
message: strings.noteLocked(),
message: "Note locked successfully",
type: "error",
context: "local"
});
@@ -424,7 +446,8 @@ export class VaultDialog extends Component {
async _unlockNote() {
if (!this.password || this.password.trim() === 0) {
ToastManager.show({
heading: strings.passwordIncorrect(),
heading: "Incorrect password",
message: "Please enter the correct password and try again",
type: "error",
context: "local"
});
@@ -460,7 +483,7 @@ export class VaultDialog extends Component {
async _deleteNote() {
try {
await db.vault.remove(this.state.note.id, this.password);
await deleteItems("note", [this.state.note.id]);
await deleteItems([this.state.note.id], "note");
this.close();
} catch (e) {
this._takeErrorAction(e);
@@ -481,7 +504,8 @@ export class VaultDialog extends Component {
});
eSendEvent("vaultUpdated");
ToastManager.show({
heading: strings.biometricUnlockEnabled(),
heading: "Biometric unlocking enabled!",
message: "Now you can unlock notes in vault with biometrics.",
type: "success",
context: "global"
});
@@ -489,7 +513,9 @@ export class VaultDialog extends Component {
} catch (e) {
this.close();
ToastManager.show({
heading: strings.passwordIncorrect(),
heading: "Incorrect password",
message:
"Please enter the correct vault password to enable biometrics.",
type: "error",
context: "local"
});
@@ -514,20 +540,20 @@ export class VaultDialog extends Component {
loading: false
});
ToastManager.show({
heading: strings.noteLocked(),
heading: "Note added to vault",
type: "success",
context: "global"
});
this.close();
} else {
eSendEvent("vaultUpdated");
ToastManager.show({
heading: strings.vaultCreated(),
heading: "Vault created successfully",
type: "success",
context: "global"
});
this.close();
}
eSendEvent("vaultUpdated");
}
_permanantUnlock() {
@@ -535,7 +561,7 @@ export class VaultDialog extends Component {
.remove(this.state.note.id, this.password)
.then(() => {
ToastManager.show({
heading: strings.noteUnlocked(),
heading: "Note permanently unlocked.",
type: "success",
context: "global"
});
@@ -543,7 +569,6 @@ export class VaultDialog extends Component {
this.close();
})
.catch((e) => {
console.log("Error", e);
this._takeErrorAction(e);
});
}
@@ -563,8 +588,9 @@ export class VaultDialog extends Component {
async _copyNote(note) {
Clipboard.setString((await convertNoteToText(note, true)) || "");
ToastManager.show({
heading: strings.noteCopied(),
heading: "Note copied",
type: "success",
message: "Note has been copied to clipboard!",
context: "global"
});
this.close();
@@ -574,7 +600,7 @@ export class VaultDialog extends Component {
this.close();
try {
await Share.open({
heading: note.title,
heading: "Share note",
failOnCancel: false,
message: (await convertNoteToText(note)) || ""
});
@@ -584,17 +610,24 @@ export class VaultDialog extends Component {
}
_takeErrorAction(e) {
this.setState({
wrongPassword: true,
visible: true
});
setTimeout(() => {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
if (
e.message === db.vault.ERRORS.wrongPassword ||
e.message === "FAILURE"
) {
this.setState({
wrongPassword: true,
visible: true
});
}, 500);
setTimeout(() => {
ToastManager.show({
heading: "Incorrect password",
type: "error",
context: "local"
});
}, 500);
return;
}
}
_revokeFingerprintAccess = async () => {
@@ -602,13 +635,14 @@ export class VaultDialog extends Component {
await BiometricService.resetCredentials();
eSendEvent("vaultUpdated");
ToastManager.show({
heading: strings.biometricUnlockDisabled(),
heading: "Biometric unlocking disabled!",
type: "success",
context: "global"
});
} catch (e) {
ToastManager.show({
heading: e.message,
heading: "Failed to disable Biometric unlocking.",
message: e.message,
type: "success",
context: "global"
});
@@ -674,7 +708,12 @@ export class VaultDialog extends Component {
paddingTop: 12
}}
>
<DialogHeader title={this.state.title} icon="shield" padding={12} />
<DialogHeader
title={this.state.title}
paragraph={this.state.description}
icon="shield"
padding={12}
/>
<Seperator half />
<View
@@ -710,16 +749,10 @@ export class VaultDialog extends Component {
: this.onPress;
}}
autoComplete="password"
returnKeyLabel={
changePassword ? strings.next() : this.state.title
}
returnKeyLabel={changePassword ? "Next" : this.state.title}
returnKeyType={changePassword ? "next" : "done"}
secureTextEntry
placeholder={
changePassword
? strings.currentPassword()
: strings.password()
}
placeholder={changePassword ? "Current password" : "Password"}
/>
{!this.state.biometricUnlock ||
@@ -728,11 +761,11 @@ export class VaultDialog extends Component {
changePassword ? null : (
<Button
onPress={() =>
this._onPressFingerprintAuth(strings.unlockNote(), "")
this._onPressFingerprintAuth("Unlock note", "")
}
icon="fingerprint"
width="100%"
title={strings.unlockWithBiometrics()}
title={"Biometric unlock"}
type="transparent"
/>
)}
@@ -755,7 +788,7 @@ export class VaultDialog extends Component {
marginTop: 10
}}
width="100%"
title={strings.deleteAllNotes()}
title={"Delete all notes"}
type="errorShade"
/>
)}
@@ -776,7 +809,7 @@ export class VaultDialog extends Component {
returnKeyLabel="Change"
returnKeyType="done"
secureTextEntry
placeholder={strings.newPassword()}
placeholder={"New password"}
/>
</>
) : null}
@@ -791,13 +824,13 @@ export class VaultDialog extends Component {
this.password = value;
}}
autoComplete="password"
returnKeyLabel={strings.next()}
returnKeyLabel="Next"
returnKeyType="next"
secureTextEntry
onSubmit={() => {
this.confirmPassRef.current?.focus();
}}
placeholder={strings.password()}
placeholder="Password"
/>
<Input
@@ -826,7 +859,7 @@ export class VaultDialog extends Component {
}
}}
onSubmit={this.onPress}
placeholder={strings.confirmPassword()}
placeholder="Confirm password"
/>
</View>
) : null}
@@ -834,7 +867,9 @@ export class VaultDialog extends Component {
{this.state.biometricUnlock &&
!this.state.isBiometryEnrolled &&
novault ? (
<Paragraph>{strings.vaultEnableBiometrics()}</Paragraph>
<Paragraph>
Unlock with password once to enable biometric access.
</Paragraph>
) : null}
{this.state.isBiometryAvailable &&
@@ -853,7 +888,7 @@ export class VaultDialog extends Component {
}}
icon="fingerprint"
width="100%"
title={strings.unlockWithBiometrics()}
title="Biometric unlocking"
iconColor={
this.state.biometricUnlock
? colors.selected.accent
@@ -873,26 +908,26 @@ export class VaultDialog extends Component {
}
positiveTitle={
deleteVault
? strings.delete()
? "Delete"
: clearVault
? strings.clear()
? "Clear"
: fingerprintAccess
? strings.enable()
? "Enable"
: this.state.revokeFingerprintAccess
? strings.revoke()
? "Revoke"
: changePassword
? strings.change()
? "Change"
: this.state.noteLocked
? deleteNote
? strings.delete()
? "Delete"
: share
? strings.share()
? "Share "
: goToEditor
? strings.open()
: strings.unlock()
? "Open"
: "Unlock"
: !note.id
? strings.create()
: strings.lock()
? "Create"
: "Lock"
}
/>
</View>

View File

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

View File

@@ -19,8 +19,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useRef } from "react";
import { Platform, StyleSheet, View } from "react-native";
//@ts-ignore
import { useThemeColors } from "@notesnook/theme";
import { Menu } from "react-native-material-menu";
import Menu from "react-native-reanimated-material-menu";
import { notesnook } from "../../../e2e/test.ids";
import {
HeaderRightButton,
@@ -96,16 +97,14 @@ export const RightMenus = ({
style={{
borderRadius: 5,
backgroundColor: contextMenuColors.primary.background,
marginTop: 35
marginTop: -40
}}
onRequestClose={() => {
//@ts-ignore
menuRef.current?.hide();
}}
anchor={
<IconButton
onPress={() => {
//@ts-ignore
menuRef.current?.show();
}}
name="dots-vertical"
@@ -117,10 +116,9 @@ export const RightMenus = ({
{headerRightButtons.map((item) => (
<Button
style={{
width: 150,
justifyContent: "flex-start",
borderRadius: 0,
alignSelf: "flex-start",
width: "100%"
borderRadius: 0
}}
type="plain"
buttonType={{
@@ -129,7 +127,6 @@ export const RightMenus = ({
key={item.title}
title={item.title}
onPress={async () => {
//@ts-ignore
menuRef.current?.hide();
if (Platform.OS === "ios") await sleep(300);
item.onPress();

View File

@@ -17,23 +17,29 @@ 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, useWindowDimensions, View } from "react-native";
import {
Linking,
ScrollView,
TouchableOpacity,
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";
const Intro = ({ navigation }) => {
const { colors } = useThemeColors();
@@ -83,20 +89,18 @@ const Intro = ({ navigation }) => {
>
{item.headings?.map((heading) => (
<Heading
key={heading()}
key={heading}
style={{
marginBottom: 5
}}
extraBold
size={SIZE.xxl}
>
{heading()}
{heading}
</Heading>
))}
{item.body ? (
<Paragraph size={SIZE.sm}>{item.body()}</Paragraph>
) : null}
{item.body ? <Paragraph size={SIZE.sm}>{item.body}</Paragraph> : null}
{item.tesimonial ? (
<Paragraph
@@ -108,7 +112,7 @@ const Intro = ({ navigation }) => {
Linking.openURL(item.link);
}}
>
{item.tesimonial()} {item.user}
{item.tesimonial} {item.user}
</Paragraph>
) : null}
</View>
@@ -128,6 +132,7 @@ const Intro = ({ navigation }) => {
style={{
width: deviceMode !== "mobile" ? width / 2 : "100%",
backgroundColor: colors.secondary.background,
marginBottom: 20,
borderBottomWidth: 1,
borderBottomColor: colors.primary.border,
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
@@ -137,7 +142,7 @@ const Intro = ({ navigation }) => {
marginTop: deviceMode !== "mobile" ? 50 : null,
paddingTop: insets.top + 10,
paddingBottom: insets.top + 10,
minHeight: height * 0.7 - (insets.top + insets.bottom)
minHeight: height * 0.7
}}
>
<SwiperFlatList
@@ -147,7 +152,26 @@ const Intro = ({ navigation }) => {
index={0}
useReactNativeGestureHandler={true}
showPagination
data={strings.introData}
data={[
{
headings: ["Open source.", "End to end encrypted.", "Private."],
body: "Write notes with freedom, no spying, no tracking."
},
{
headings: [
"Privacy for everyone",
"— not just the",
"privileged few"
],
body: "Your privacy matters to us, no matter who you are. In a world where everyone is trying to spy on you, Notesnook encrypts all your data before it leaves your device. With Notesnook no one can ever sell your data again."
},
{
tesimonial:
"You simply cannot get any better of a note taking app than @notesnook. The UI is clean and slick, it is feature rich, encrypted, reasonably priced (esp. for students & educators) & open source",
link: "https://twitter.com/andrewsayer/status/1637817220113002503",
user: "@andrewsayer on Twitter"
}
]}
paginationActiveColor={colors.primary.accent}
paginationStyleItem={{
width: 10,
@@ -188,7 +212,7 @@ const Intro = ({ navigation }) => {
}}
fontSize={SIZE.md}
type="accent"
title={strings.getStarted()}
title="Get started"
/>
</View>
</ScrollView>

View File

@@ -29,7 +29,6 @@ import { IconButton } from "../../ui/icon-button";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { getFormattedDate } from "@notesnook/common";
import { strings } from "@notesnook/intl";
export const NotebookHeader = ({
notebook,
@@ -58,7 +57,7 @@ export const NotebookHeader = ({
}
});
ToastManager.show({
heading: strings.shortcutCreated(),
heading: "Shortcut created",
type: "success"
});
}
@@ -149,7 +148,11 @@ export const NotebookHeader = ({
size={SIZE.xs}
color={colors.secondary.paragraph}
>
{strings.notes(totalNotes || 0)}
{notebook && totalNotes > 1
? totalNotes + " notes"
: totalNotes === 1
? totalNotes + " note"
: "0 notes"}
</Paragraph>
</View>
);

View File

@@ -18,7 +18,6 @@ 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";
@@ -27,6 +26,7 @@ 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";
@@ -57,11 +57,17 @@ export const SectionHeader = React.memo<
}: SectionHeaderProps) {
const { colors } = useThemeColors();
const { fontScale } = useWindowDimensions();
const groupBy = strings.groupByStrings[groupOptions.groupBy]();
let groupBy = Object.keys(GROUP).find(
(key) => GROUP[key as keyof typeof GROUP] === groupOptions.groupBy
);
const isCompactModeEnabled = useIsCompactModeEnabled(
dataType as "note" | "notebook"
);
groupBy = !groupBy
? "Default"
: groupBy.slice(0, 1).toUpperCase() + groupBy.slice(1, groupBy.length);
return (
<View
style={{
@@ -97,7 +103,7 @@ export const SectionHeader = React.memo<
textAlignVertical: "center"
}}
>
{!item.title || item.title === "" ? strings.pinned() : item.title}
{!item.title || item.title === "" ? "Pinned" : item.title}
</Heading>
</TouchableOpacity>
@@ -111,10 +117,12 @@ 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"
@@ -148,6 +156,11 @@ export const SectionHeader = React.memo<
screen !== "Notes"
}
testID="icon-compact-mode"
tooltipText={
isCompactModeEnabled
? "Switch to normal mode"
: "Switch to compact mode"
}
color={colors.secondary.icon}
name={isCompactModeEnabled ? "view-list" : "view-list-outline"}
onPress={() => {

View File

@@ -46,7 +46,6 @@ import { ReminderTime } from "../../ui/reminder-time";
import { TimeSince } from "../../ui/time-since";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
type NoteItemProps = {
item: Note | BaseTrashItem<Note>;
@@ -342,10 +341,9 @@ const NoteItem = ({
marginRight: 6
}}
>
Deleted on{" "}
{item && item.dateDeleted
? strings.deletedOn(
new Date(item.dateDeleted).toISOString().slice(0, 10)
)
? new Date(item.dateDeleted).toISOString().slice(0, 10)
: null}
</Paragraph>
@@ -356,7 +354,8 @@ const NoteItem = ({
marginRight: 6
}}
>
{strings.note()}
{(item as TrashItem).itemType[0].toUpperCase() +
(item as TrashItem).itemType.slice(1)}
</Paragraph>
</>
)}

View File

@@ -30,7 +30,6 @@ import { Properties } from "../../properties";
import { IconButton } from "../../ui/icon-button";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
type NotebookItemProps = {
item: Notebook | BaseTrashItem<Notebook>;
@@ -113,11 +112,10 @@ export const NotebookItem = ({
marginRight: 6
}}
>
{strings.deletedOn(
{"Deleted on " +
new Date((item as TrashItem).dateDeleted)
.toISOString()
.slice(0, 10)
)}
.slice(0, 10)}
</Paragraph>
<Paragraph
color={colors.primary.accent}
@@ -149,7 +147,11 @@ export const NotebookItem = ({
marginRight: 6
}}
>
{strings.notes(totalNotes)}
{item && totalNotes > 1
? totalNotes + " notes"
: totalNotes === 1
? totalNotes + " note"
: "0 notes"}
</Paragraph>
{item.pinned ? (
@@ -181,7 +183,11 @@ export const NotebookItem = ({
marginRight: 6
}}
>
{strings.notes(totalNotes)}
{item && totalNotes > 1
? totalNotes + " notes"
: totalNotes === 1
? totalNotes + " note"
: "0 notes"}
</Paragraph>
</>
) : null}

View File

@@ -28,7 +28,6 @@ import { useSelectionStore } from "../../../stores/use-selection-store";
import { useTrashStore } from "../../../stores/use-trash-store";
import { presentDialog } from "../../dialog/functions";
import SelectionWrapper, { selectItem } from "../selection-wrapper";
import { strings } from "@notesnook/intl";
export const openNotebook = (item: Notebook | BaseTrashItem<Notebook>) => {
const isTrash = item.type === "trash";
@@ -37,15 +36,16 @@ export const openNotebook = (item: Notebook | BaseTrashItem<Notebook>) => {
if (isTrash) {
presentDialog({
title: strings.restoreNotebook(),
positiveText: strings.restore(),
negativeText: strings.delete(),
title: `Restore notebook`,
paragraph: `Restore or delete notebook forever`,
positiveText: "Restore",
negativeText: "Delete",
positivePress: async () => {
if ((await db.trash.restore(item.id)) === false) return;
await db.trash.restore(item.id);
Navigation.queueRoutesForUpdate();
useSelectionStore.getState().setSelectionMode(undefined);
ToastManager.show({
heading: strings.notebookRestored(),
heading: "Restore successful",
type: "success"
});
},
@@ -54,7 +54,7 @@ export const openNotebook = (item: Notebook | BaseTrashItem<Notebook>) => {
useTrashStore.getState().refresh();
useSelectionStore.getState().setSelectionMode(undefined);
ToastManager.show({
heading: strings.permanentlyDeletedNotebook(),
heading: "Permanently deleted items",
type: "success",
context: "local"
});

View File

@@ -30,7 +30,6 @@ import { ReminderTime } from "../../ui/reminder-time";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import SelectionWrapper, { selectItem } from "../selection-wrapper";
import { strings } from "@notesnook/intl";
const ReminderItem = React.memo(
({
@@ -81,8 +80,7 @@ const ReminderItem = React.memo(
<View
style={{
flexDirection: "row",
flexWrap: "wrap",
marginTop: 5
flexWrap: "wrap"
}}
>
{item.disabled ? (
@@ -110,7 +108,7 @@ const ReminderItem = React.memo(
color={colors.secondary.paragraph}
style={{ marginLeft: 5 }}
>
{strings.disabled()}
Disabled
</Paragraph>
</View>
) : null}
@@ -139,7 +137,9 @@ const ReminderItem = React.memo(
color={colors.secondary.paragraph}
style={{ marginLeft: 5 }}
>
{strings.reminderRecurringMode[item.recurringMode]()}
{item.recurringMode.slice(0, 1).toUpperCase() +
item.recurringMode.replace("y", "i").slice(1) +
"ly"}
</Paragraph>
</View>
) : null}
@@ -150,8 +150,10 @@ const ReminderItem = React.memo(
fontSize={SIZE.xs}
style={{
justifyContent: "flex-start",
height: 25,
alignSelf: "flex-start"
borderWidth: 0,
height: 30,
alignSelf: "flex-start",
marginTop: 5
}}
/>
</View>

View File

@@ -70,7 +70,6 @@ const SelectionWrapper = ({
}
const onLongPress = () => {
if (isSheet) return;
if (useSelectionStore.getState().selectionMode !== item.type) {
useSelectionStore.getState().setSelectionMode(item.type);
}
@@ -79,7 +78,7 @@ const SelectionWrapper = ({
return (
<Pressable
customColor={isSheet ? colors.secondary.background : "transparent"}
customColor={isSheet ? colors.primary.hover : "transparent"}
testID={testID}
onLongPress={onLongPress}
onPress={onPress}

View File

@@ -29,7 +29,6 @@ import { IconButton } from "../../ui/icon-button";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import SelectionWrapper, { selectItem } from "../selection-wrapper";
import { strings } from "@notesnook/intl";
const TagItem = React.memo(
({
@@ -78,7 +77,11 @@ const TagItem = React.memo(
marginTop: 5
}}
>
{strings.notes(totalNotes)}
{totalNotes > 1
? totalNotes + " notes"
: totalNotes === 1
? totalNotes + " note"
: null}
</Paragraph>
</View>

View File

@@ -87,8 +87,8 @@ export const Empty = React.memo(function Empty({
color={color}
tip={
screen !== "Search"
? tip || ({ text: () => placeholder?.paragraph } as TTip)
: ({ text: () => placeholder?.paragraph } as TTip)
? tip || ({ text: placeholder?.paragraph } as TTip)
: ({ text: placeholder?.paragraph } as TTip)
}
style={{
backgroundColor: "transparent",

View File

@@ -34,13 +34,12 @@ import {
Reminder,
Tag,
TrashItem,
VirtualizedGrouping,
getSortValue
VirtualizedGrouping
} 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";
@@ -49,6 +48,7 @@ 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;
@@ -297,6 +297,7 @@ function getDate(item: Item, groupType?: GroupingKey): number {
groupType
? db.settings.getGroupOptions(groupType)
: {
groupBy: "default",
sortBy: "dateEdited",
sortDirection: "desc"
},

View File

@@ -48,7 +48,6 @@ import { IconButton } from "../ui/icon-button";
import Seperator from "../ui/seperator";
import Paragraph from "../ui/typography/paragraph";
import { diff } from "diffblazer";
import { strings } from "@notesnook/intl";
const MergeConflicts = () => {
const { colors } = useThemeColors();
@@ -172,11 +171,7 @@ const MergeConflicts = () => {
fontWeight: "bold"
}}
>
(
{isCurrent
? strings.mergeConflict.thisDevice()
: strings.mergeConflict.otherDevice()}
)
{isCurrent ? "(This Device)" : "(Incoming)"}
</Text>
{"\n"}
{getFormattedDate(contentToKeep?.dateEdited)}
@@ -196,7 +191,7 @@ const MergeConflicts = () => {
setCopy(contentToKeep);
setDialogVisible(true);
}}
title={strings.saveACopy()}
title="Save a copy"
type="secondary"
height={30}
style={{
@@ -209,7 +204,7 @@ const MergeConflicts = () => {
<View style={{ width: 10 }} />
{isDiscarded ? (
<Button
title={strings.discard()}
title="Discard"
type="accent"
buttonType={{
color: colors.static.red,
@@ -239,9 +234,7 @@ const MergeConflicts = () => {
}}
type="accent"
fontSize={SIZE.xs}
title={
keeping && !isDiscarded ? strings.undo() : strings.keep()
}
title={keeping && !isDiscarded ? "Undo" : "Keep"}
onPress={() => {
setKeep(keeping && !isDiscarded ? null : contentToKeep);
}}
@@ -284,7 +277,11 @@ const MergeConflicts = () => {
{dialogVisible && (
<BaseDialog visible={true}>
<DialogContainer>
<DialogHeader title={strings.applyChanges()} padding={12} />
<DialogHeader
title="Apply Changes"
paragraph="Apply selected changes to note?"
padding={12}
/>
<Seperator />
<DialogButtons
positiveTitle="Apply"

View File

@@ -37,7 +37,6 @@ import { Pressable } from "../ui/pressable";
import Seperator from "../ui/seperator";
import Paragraph from "../ui/typography/paragraph";
import NotePreview from "./preview";
import { strings } from "@notesnook/intl";
const HistoryItem = ({
index,
@@ -142,7 +141,11 @@ export default function NoteHistory({
return (
<View>
<SheetProvider context="note_history" />
<DialogHeader title={strings.noteHistory()} padding={12} />
<DialogHeader
title="Note history"
paragraph="Revert back to an older version of this note"
padding={12}
/>
<Seperator />
@@ -177,7 +180,7 @@ export default function NoteHistory({
<>
<Icon name="history" size={50} color={colors.primary.icon} />
<Paragraph color={colors.secondary.paragraph}>
{strings.noteHistoryPlaceholder()}
No note history found on this device.
</Paragraph>
</>
)}
@@ -193,7 +196,7 @@ export default function NoteHistory({
alignSelf: "center"
}}
>
{strings.noteHistoryNotice[0]()}{" "}
Note version history is local only.{" "}
<Text
onPress={() => {
openLinkInBrowser(
@@ -205,7 +208,7 @@ export default function NoteHistory({
textDecorationLine: "underline"
}}
>
{strings.noteHistoryNotice[1]()}
Learn how this works.
</Text>
</Paragraph>
</View>

View File

@@ -35,7 +35,6 @@ import { presentDialog } from "../dialog/functions";
import { Button } from "../ui/button";
import Paragraph from "../ui/typography/paragraph";
import { diff } from "diffblazer";
import { strings } from "@notesnook/intl";
/**
*
@@ -48,11 +47,11 @@ export default function NotePreview({ session, content, note }) {
async function restore() {
if (note && note.type === "trash") {
if ((await db.trash.restore(note.id)) === false) return;
await db.trash.restore(note.id);
Navigation.queueRoutesForUpdate();
useSelectionStore.getState().setSelectionMode(false);
ToastManager.show({
heading: strings.noteRestored(),
heading: "Restore successful",
type: "success"
});
eSendEvent(eCloseSheet);
@@ -73,7 +72,7 @@ export default function NotePreview({ session, content, note }) {
Navigation.queueRoutesForUpdate();
ToastManager.show({
heading: strings.noteRestoredFromHistory(),
heading: "Note restored successfully",
type: "success"
});
}
@@ -84,17 +83,17 @@ export default function NotePreview({ session, content, note }) {
const deleteNote = async () => {
presentDialog({
title: strings.deleteNote(),
paragraph: strings.deleteNoteConfirmation(),
positiveText: strings.delete(),
negativeText: strings.cancel(),
title: `Delete note permanently`,
paragraph: `Are you sure you want to delete this note from trash permanentaly`,
positiveText: "Delete",
negativeText: "Cancel",
context: "local",
positivePress: async () => {
await db.trash.delete(note.id);
useTrashStore.getState().refresh();
useSelectionStore.getState().setSelectionMode(false);
ToastManager.show({
heading: strings.noteDeleted(),
heading: "Permanently deleted items",
type: "success",
context: "local"
});
@@ -151,7 +150,7 @@ export default function NotePreview({ session, content, note }) {
}}
>
<Paragraph color={colors.secondary.paragraph}>
{strings.encryptedNoteHistoryNotice()}
Preview not available, content is encrypted.
</Paragraph>
</View>
)}
@@ -161,15 +160,10 @@ export default function NotePreview({ session, content, note }) {
paddingHorizontal: 12
}}
>
<Button
onPress={restore}
title={strings.restore()}
type="accent"
width="100%"
/>
<Button onPress={restore} title="Restore" type="accent" width="100%" />
<Button
onPress={deleteNote}
title={strings.deletePermanently()}
title="Delete permanently"
type="error"
width="100%"
style={{

View File

@@ -48,7 +48,6 @@ import Paragraph from "../ui/typography/paragraph";
import { Walkthrough } from "../walkthroughs";
import { PricingItem } from "./pricing-item";
import { useSettingStore } from "../../stores/use-setting-store";
import { strings } from "@notesnook/intl";
const UUID_PREFIX = "0bdaea";
const UUID_VERSION = "4";
@@ -129,10 +128,7 @@ export const PricingPlans = ({
if (code.startsWith("com.streetwriters.notesnook")) {
skuId = code;
} else {
skuId = await db.offers?.getCode(
code.split(":")[0],
Platform.OS as "ios" | "android"
);
skuId = await db.offers?.getCode(code.split(":")[0], Platform.OS);
}
const products = await PremiumService.getProducts();
@@ -520,7 +516,7 @@ export const PricingPlans = ({
setBuying(true);
try {
if (!(await getPromo(value as string)))
throw new Error(strings.errorApplyingPromoCode());
throw new Error("Error applying promo code");
ToastManager.show({
heading: "Discount applied!",
type: "success",

View File

@@ -36,7 +36,6 @@ import ColorPicker from "../dialogs/color-picker";
import { Button } from "../ui/button";
import NativeTooltip from "../../utils/tooltip";
import { Pressable } from "../ui/pressable";
import { strings } from "@notesnook/intl";
const ColorItem = ({ item, note }: { item: Color; note: Note }) => {
const { colors } = useThemeColors();
@@ -146,7 +145,7 @@ export const ColorTags = ({ item }: { item: Note }) => {
buttonType={{
text: colors.primary.accent
}}
title={strings.addColor()}
title="Add color"
type="secondary"
icon="plus"
iconPosition="right"

View File

@@ -23,10 +23,26 @@ import { useThemeColors } from "@notesnook/theme";
import { SIZE } from "../../utils/size";
import Paragraph from "../ui/typography/paragraph";
import { getFormattedDate } from "@notesnook/common";
import { strings } from "@notesnook/intl";
export const DateMeta = ({ item }) => {
const { colors } = useThemeColors();
const getNameFromKey = (key) => {
switch (key) {
case "dateCreated":
return "Created at:";
case "dateEdited":
return "Last edited at:";
case "dateModified":
return "Last modified at:";
case "dateDeleted":
return "Deleted at:";
case "dateUploaded":
return "Uploaded at:";
default:
return key;
}
};
function getDateMeta() {
let keys = Object.keys(item);
if (keys.includes("dateEdited"))
@@ -48,7 +64,7 @@ export const DateMeta = ({ item }) => {
}}
>
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
{strings.dateDescFromKey(key)}
{getNameFromKey(key)}
</Paragraph>
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
{getFormattedDate(item[key], "date-time")}

View File

@@ -39,7 +39,6 @@ import Notebooks from "./notebooks";
import { Synced } from "./synced";
import { TagStrip, Tags } from "./tags";
import { tabBarRef } from "../../utils/global-refs";
import { strings } from "@notesnook/intl";
const Line = ({ top = 6, bottom = 6 }) => {
const { colors } = useThemeColors();
@@ -62,7 +61,7 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
if (!item || !item.id) {
return (
<Paragraph style={{ marginVertical: 10, alignSelf: "center" }}>
{strings.noNotePropertiesNotice()}
Start writing to save your note.
</Paragraph>
);
}

View File

@@ -137,13 +137,14 @@ export const Items = ({ item, buttons, close }) => {
style={{
alignSelf: "flex-start",
paddingHorizontal: 0,
flex: 1
width: topBarItemWidth
}}
>
<View
onPress={item.func}
style={{
height: topBarItemWidth,
width: topBarItemWidth,
justifyContent: "center",
alignItems: "center",
textAlign: "center",
@@ -168,7 +169,7 @@ export const Items = ({ item, buttons, close }) => {
<Paragraph
textBreakStrategy="simple"
size={SIZE.xxs}
size={SIZE.xxs + 1}
style={{ textAlign: "center" }}
>
{item.title}
@@ -241,7 +242,7 @@ export const Items = ({ item, buttons, close }) => {
showsHorizontalScrollIndicator={false}
contentContainerStyle={{
paddingRight: 25,
gap: 15
gap: 5
}}
>
{topBarItems.map(renderTopBarItem)}

View File

@@ -29,7 +29,6 @@ import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import { Pressable } from "../ui/pressable";
import { strings } from "@notesnook/intl";
export default function Notebooks({ note, close, full }) {
const { colors } = useThemeColors();
@@ -103,7 +102,7 @@ export default function Notebooks({ note, close, full }) {
{noteNotebooks.length > 1 && !full ? (
<Button
title={strings.viewAllLinkedNotebooks()}
title={`View all linked notebooks`}
fontSize={SIZE.xs}
style={{
alignSelf: "flex-end",

View File

@@ -28,7 +28,6 @@ import { sleep } from "../../utils/time";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
export const Synced = ({ item, close }) => {
const { colors } = useThemeColors();
const user = useUserStore((state) => state.user);
@@ -72,7 +71,7 @@ export const Synced = ({ item, close }) => {
flexWrap: "wrap"
}}
>
{strings.noteSyncedNoticeHeading()}
Encrypted and synced
</Heading>
{shouldShrink ? null : (
<Paragraph
@@ -82,7 +81,7 @@ export const Synced = ({ item, close }) => {
size={SIZE.xs}
color={colors.primary.paragraph}
>
{strings.noteSyncedNoticeDesc(item.itemType || item.type)}
No one can view this {item.itemType || item.type} except you.
</Paragraph>
)}
</View>
@@ -100,7 +99,7 @@ export const Synced = ({ item, close }) => {
console.error(e);
}
}}
title={strings.learnMore()}
title="Learn more"
fontSize={SIZE.xs}
height={30}
type="secondaryAccented"

View File

@@ -27,7 +27,6 @@ import { sleep } from "../../utils/time";
import ManageTagsSheet from "../sheets/manage-tags";
import { Button } from "../ui/button";
import { ColorTags } from "./color-tags";
import { strings } from "@notesnook/intl";
export const Tags = ({ item, close }) => {
const { colors } = useThemeColors();
@@ -52,7 +51,7 @@ export const Tags = ({ item, close }) => {
buttonType={{
text: colors.primary.accent
}}
title={strings.addTag()}
title="Add tag"
type="secondary"
icon="plus"
iconPosition="right"

View File

@@ -18,7 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Item, ItemType, VirtualizedGrouping } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef } from "react";
import {
@@ -27,7 +26,7 @@ import {
Platform,
View
} from "react-native";
import { Menu } from "react-native-material-menu";
import Menu from "react-native-reanimated-material-menu/src/Menu";
import { db } from "../../common/database";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { ToastManager } from "../../services/event-manager";
@@ -90,26 +89,26 @@ export const SelectionHeader = React.memo(
const restoreItem = async () => {
if (!selectedItemsList.length) return;
if ((await db.trash.restore(...selectedItemsList)) === false) return;
await db.trash.restore(...selectedItemsList);
Navigation.queueRoutesForUpdate();
clearSelection();
ToastManager.show({
heading: strings.restored(),
heading: "Restore successful",
type: "success"
});
};
const deleteItem = async () => {
if (!type) return;
presentDialog({
title: strings.doActions.delete.unknown(type, selectedItemsList.length),
paragraph: strings.actionConfirmations.delete.unknown(
type,
selectedItemsList.length
),
positiveText: strings.delete(),
negativeText: strings.cancel(),
title: `Delete ${selectedItemsList.length > 1 ? "items" : "item"}`,
paragraph: `Are you sure you want to delete ${
selectedItemsList.length > 1
? "these items permanently?"
: "this item permanently?"
}`,
positiveText: "Delete",
negativeText: "Cancel",
positivePress: async () => {
if (!selectedItemsList.length) return;
await db.trash.delete(...selectedItemsList);
@@ -220,16 +219,14 @@ export const SelectionHeader = React.memo(
style={{
borderRadius: 5,
backgroundColor: contextMenuColors.primary.background,
marginTop: 35
marginTop: -20
}}
onRequestClose={() => {
//@ts-ignore
menuRef.current?.hide();
}}
anchor={
<IconButton
onPress={() => {
//@ts-ignore
menuRef.current?.show();
}}
name="dots-vertical"
@@ -239,7 +236,10 @@ export const SelectionHeader = React.memo(
>
{[
{
title: strings.move(),
title:
selectedItemsList.length > 1
? "Move notebooks"
: "Move notebook",
onPress: async () => {
const ids = selectedItemsList;
const notebooks = await db.notebooks.all.items(ids);
@@ -249,7 +249,7 @@ export const SelectionHeader = React.memo(
icon: "arrow-right-bold-box-outline"
},
{
title: strings.manageTags(),
title: "Manage tags",
onPress: async () => {
await sleep(100);
ManageTagsSheet.present(selectedItemsList);
@@ -258,7 +258,7 @@ export const SelectionHeader = React.memo(
icon: "pound"
},
{
title: strings.export(),
title: "Export",
onPress: async () => {
await sleep(100);
ExportNotesSheet.present(selectedItemsList);
@@ -267,7 +267,7 @@ export const SelectionHeader = React.memo(
icon: "export"
},
{
title: strings.linkNotebook(),
title: "Link notebook",
onPress: async () => {
await sleep(100);
MoveNoteSheet.present();
@@ -276,7 +276,7 @@ export const SelectionHeader = React.memo(
icon: "plus"
},
{
title: strings.unlinkNotebook(),
title: "Unlink notebook",
onPress: async () => {
if (!id) return;
await db.notes.removeFromNotebook(id, ...selectedItemsList);
@@ -288,53 +288,33 @@ export const SelectionHeader = React.memo(
icon: "minus"
},
{
title: strings.unfavorite(),
title: "Unfavorite",
onPress: addToFavorite,
visible: focusedRouteId === "Favorites",
icon: "star-off"
},
{
title: strings.moveToTrash(),
title: `Move to trash`,
onPress: async () => {
const selection = useSelectionStore.getState();
if (!selection.selectionMode) return;
await deleteItems(
selection.selectionMode as ItemType,
selection.selectedItemsList
);
selection.clearSelection();
selection.setSelectionMode(undefined);
deleteItems(
undefined,
useSelectionStore.getState().selectionMode
).then(() => {
useSelectionStore.getState().clearSelection();
useSelectionStore.getState().setSelectionMode(undefined);
});
},
visible: type === "note" || type === "notebook",
visible: type !== "trash",
icon: "delete"
},
{
title: strings.doActions.delete.unknown(
type!,
selectedItemsList.length
),
onPress: async () => {
const selection = useSelectionStore.getState();
if (!selection.selectionMode) return;
await deleteItems(
selection.selectionMode as ItemType,
selection.selectedItemsList
);
selection.clearSelection();
selection.setSelectionMode(undefined);
},
visible:
type !== "trash" && type !== "note" && type !== "notebook",
icon: "delete"
},
{
title: strings.restore(),
title: `Restore`,
onPress: restoreItem,
visible: type === "trash",
icon: "delete-restore"
},
{
title: strings.delete(),
title: `Delete`,
onPress: deleteItem,
visible: type === "trash",
icon: "delete"
@@ -343,10 +323,9 @@ export const SelectionHeader = React.memo(
!item.visible ? null : (
<Button
style={{
width: 150,
justifyContent: "flex-start",
borderRadius: 0,
alignSelf: "flex-start",
width: "100%"
borderRadius: 0
}}
type="plain"
buttonType={{
@@ -356,7 +335,6 @@ export const SelectionHeader = React.memo(
key={item.title}
title={item.title}
onPress={async () => {
//@ts-ignore
menuRef.current?.hide();
if (Platform.OS === "ios") await sleep(300);
item.onPress();

View File

@@ -40,7 +40,6 @@ import { MoveNotes } from "../move-notes/movenote";
import { eOnNotebookUpdated } from "../../../utils/events";
import { getParentNotebookId } from "../../../utils/notebooks";
import { useNotebookStore } from "../../../stores/use-notebook-store";
import { strings } from "@notesnook/intl";
export const AddNotebookSheet = ({
notebook,
@@ -63,25 +62,25 @@ export const AddNotebookSheet = ({
const onSaveChanges = async () => {
if (loading) return;
setLoading(true);
if (!title.current || title?.current.trim().length === 0) {
ToastManager.show({
heading: strings.enterNotebookTitle(),
heading: "Notebook title is required",
type: "error",
context: "local"
});
setLoading(false);
return;
}
setLoading(true);
const id = await db.notebooks.add({
title: title.current,
description: description.current,
id: notebook?.id
});
if (parentNotebook && id) {
if (parentNotebook) {
await db.relations.add(parentNotebook, {
type: "notebook",
id: id
@@ -94,8 +93,7 @@ export const AddNotebookSheet = ({
useNotebookStore.getState().refresh();
const parent =
parentNotebook?.id ||
(await getParentNotebookId(notebook?.id || (id as string)));
parentNotebook?.id || (await getParentNotebookId(notebook?.id || id));
eSendEvent(eOnNotebookUpdated, parent);
if (notebook) {
@@ -105,7 +103,7 @@ export const AddNotebookSheet = ({
});
}
if (!notebook && showMoveNotesOnComplete && id) {
if (!notebook && showMoveNotesOnComplete) {
MoveNotes.present(await db.notebooks.notebook(id));
} else {
close?.(true);
@@ -121,7 +119,7 @@ export const AddNotebookSheet = ({
}}
>
<Heading size={SIZE.lg}>
{notebook ? strings.editNotebook() : strings.newNotebook()}
{notebook ? "Edit Notebook" : "New Notebook"}
</Heading>
<Seperator />
@@ -137,7 +135,7 @@ export const AddNotebookSheet = ({
titleInput?.current?.focus();
});
}}
placeholder={strings.enterNotebookTitle()}
placeholder="Enter notebook title"
onSubmit={() => {
descriptionInput.current?.focus();
}}
@@ -152,14 +150,14 @@ export const AddNotebookSheet = ({
onChangeText={(value) => {
description.current = value;
}}
placeholder={strings.enterNotebookDescription()}
returnKeyLabel={strings.next()}
placeholder="Enter notebook description"
returnKeyLabel="Next"
returnKeyType="next"
defaultValue={notebook ? notebook.description : ""}
/>
<Button
title={notebook ? strings.save() : strings.add()}
title={notebook ? "Save" : "Add"}
type="accent"
height={45}
fontSize={SIZE.md}

View File

@@ -18,7 +18,6 @@ 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,
@@ -50,11 +49,12 @@ 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";
async function updateInitialSelectionState(items: string[]) {
const relations = await db.relations
@@ -200,11 +200,11 @@ const MoveNoteSheet = ({
minHeight: 10,
flexShrink: 1
}}
title={strings.selectNotebooks()}
title="Select notebooks"
paragraph={
!multiSelect
? strings.enableMultiSelect()
: strings.selectNotebooksDesc()
? "Long press to enable multi-select."
: "Select notebooks you want to add note(s) to."
}
/>
@@ -236,7 +236,7 @@ const MoveNoteSheet = ({
paddingHorizontal: 24,
alignSelf: "flex-start"
}}
title={strings.save()}
title="Save"
type={"accent"}
onPress={onSave}
/>
@@ -266,7 +266,7 @@ const MoveNoteSheet = ({
}}
>
<Input
placeholder={strings.searchNotebooks()}
placeholder="Search notebooks...."
button={{
icon: "plus",
onPress: () => {
@@ -319,7 +319,7 @@ const MoveNoteSheet = ({
/>
) : (
<Paragraph color={colors.primary.icon}>
{strings.emptyPlaceholders("notebook")}
No notebooks
</Paragraph>
)}
</View>

View File

@@ -30,7 +30,6 @@ import DialogHeader from "../../dialog/dialog-header";
import { Button } from "../../ui/button";
import Input from "../../ui/input";
import { eUserLoggedIn } from "../../../utils/events";
import { strings } from "@notesnook/intl";
type ChangeEmailProps = {
actionSheetRef: RefObject<ActionSheetRef>;
@@ -69,7 +68,7 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
const verified = await db.user?.verifyPassword(
emailChangeData.current.password
);
if (!verified) throw new Error(strings.passwordIncorrect());
if (!verified) throw new Error("Password is incorrect");
await db.user?.sendVerificationEmail(emailChangeData.current.email);
setStep(EmailChangeSteps.changeEmail);
setLoading(false);
@@ -89,7 +88,8 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
eSendEvent(eUserLoggedIn);
close?.();
ToastManager.show({
heading: strings.emailUpdated(emailChangeData.current.email),
heading: `Email changed`,
message: `Your account email has been updated to ${emailChangeData.current.email}`,
type: "success",
context: "global"
});
@@ -103,8 +103,8 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
return (
<View style={{ paddingHorizontal: 12 }}>
<DialogHeader
title={strings.changeEmail()}
paragraph={strings.changeEmailDesc()}
title="Change email"
paragraph="Your account email will be changed without affecting your subscription or any other settings."
/>
<View
style={{
@@ -115,7 +115,7 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
<>
<Input
fwdRef={emailInputRef}
placeholder={strings.enterNewEmail()}
placeholder="Enter your new email"
validationType="email"
onErrorCheck={(e) => setError(e)}
onChangeText={(email) => {
@@ -124,7 +124,7 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
/>
<Input
fwdRef={passInputRef}
placeholder={strings.enterAccountPassword()}
placeholder="Enter your account password"
secureTextEntry
onChangeText={(pass) => {
emailChangeData.current.password = pass;
@@ -134,10 +134,8 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
) : (
<>
<Input
key="code-input"
fwdRef={codeInputRef}
placeholder={strings.code()}
defaultValue=""
placeholder="Enter verification code sent to your new email"
onChangeText={(code) => {
emailChangeData.current.code = code;
}}
@@ -151,8 +149,8 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
loading
? undefined
: step === EmailChangeSteps.verify
? strings.verify()
: strings.changeEmail()
? "Verify"
: "Change email"
}
type="accent"
width={250}

View File

@@ -35,7 +35,6 @@ import { IconButton } from "../../ui/icon-button";
import { Pressable } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
type TabItem = {
id: number;
@@ -138,9 +137,7 @@ const TabItemComponent = (props: {
numberOfLines={1}
size={SIZE.md}
>
{props.tab.noteId
? item?.title || strings.untitledNote()
: strings.newNote()}
{props.tab.noteId ? item?.title || "Untitled note" : "New note"}
</Paragraph>
</View>
@@ -232,7 +229,7 @@ export default function EditorTabs({
alignItems: "center"
}}
>
<Heading size={SIZE.lg}>{strings.tabs()}</Heading>
<Heading size={SIZE.lg}>Tabs</Heading>
<Button
onPress={() => {
useTabStore.getState().newTab();
@@ -243,7 +240,7 @@ export default function EditorTabs({
}, 500);
close?.();
}}
title={strings.newTab()}
title="New tab"
icon="plus"
style={{
flexDirection: "row",

View File

@@ -52,7 +52,6 @@ import { Pressable } from "../../ui/pressable";
import Seperator from "../../ui/seperator";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
const ExportNotesSheet = ({
ids,
@@ -119,6 +118,7 @@ const ExportNotesSheet = ({
await exportNoteAs("pdf");
},
icon: "file-pdf-box",
desc: "View in any pdf reader app",
id: notesnook.ids.dialogs.export.pdf,
pro: premium
},
@@ -128,6 +128,7 @@ const ExportNotesSheet = ({
await exportNoteAs("md");
},
icon: "language-markdown",
desc: "View in any text or markdown editor",
id: notesnook.ids.dialogs.export.md,
pro: premium
},
@@ -137,6 +138,7 @@ const ExportNotesSheet = ({
await exportNoteAs("md-frontmatter");
},
icon: "language-markdown",
desc: "View in any text or markdown editor",
id: notesnook.ids.dialogs.export.md,
pro: premium
},
@@ -146,6 +148,7 @@ const ExportNotesSheet = ({
await exportNoteAs("txt");
},
icon: "card-text",
desc: "View in any text editor",
id: notesnook.ids.dialogs.export.text,
pro: true
},
@@ -155,6 +158,7 @@ const ExportNotesSheet = ({
await exportNoteAs("html");
},
icon: "language-html5",
desc: "View in any web browser & html reader",
id: notesnook.ids.dialogs.export.html,
pro: premium
}
@@ -171,7 +175,14 @@ const ExportNotesSheet = ({
>
<DialogHeader
icon="export"
title={strings.exportNotes(ids.length)}
title={
ids.length > 1 ? `Export ${ids.length} Notes` : "Export Note"
}
paragraph={`All exports are saved in ${
Platform.OS === "android"
? "the selected"
: "Notesnook/exported"
} folder in phone storage`}
/>
</View>
@@ -227,13 +238,13 @@ const ExportNotesSheet = ({
<Heading style={{ marginLeft: 10 }} size={SIZE.md}>
{item.title}
</Heading>
{/* <Paragraph
<Paragraph
style={{ marginLeft: 10 }}
size={SIZE.sm}
color={colors.secondary.paragraph}
>
{item.desc}
</Paragraph> */}
</Paragraph>
</View>
</Pressable>
</Fragment>
@@ -253,10 +264,9 @@ const ExportNotesSheet = ({
<>
<ActivityIndicator />
<Paragraph>
{strings.exportingNotes(status) +
"..." +
" " +
strings.pleaseWait()}
{`${
status ? `${status})` : `Exporting notes`
}... Please wait`}
</Paragraph>
</>
) : (
@@ -277,20 +287,21 @@ const ExportNotesSheet = ({
}}
color={colors.secondary.heading}
>
{strings.exportSuccessHeading(ids.length)}
{ids.length > 1
? `${ids.length} Notes exported`
: "Note exported"}
</Heading>
<Paragraph
style={{
textAlign: "center"
}}
>
{strings.exportSuccessDesc(result?.fileName as string)}
Your {ids.length > 1 ? "notes are" : "note is"} exported
successfully as {result?.fileName}
</Paragraph>
<Button
title={
Platform.OS === "android"
? strings.openFileLocation()
: strings.open()
Platform.OS === "android" ? "Open file location" : "Open"
}
type="accent"
width={250}
@@ -311,7 +322,8 @@ const ExportNotesSheet = ({
showAppsSuggestions: true
}).catch((e) => {
ToastManager.show({
heading: strings.noApplicationFound(result.name),
heading: "Cannot open",
message: `No application found to open ${result.name} file.`,
type: "success",
context: "local"
});
@@ -320,7 +332,7 @@ const ExportNotesSheet = ({
}}
/>
<Button
title={strings.share()}
title="Share"
type="secondaryAccented"
width={250}
fontSize={SIZE.md}
@@ -344,7 +356,7 @@ const ExportNotesSheet = ({
}}
/>
<Button
title={strings.exportAgain()}
title="Export in another format"
type="inverted"
width={250}
fontSize={SIZE.md}

View File

@@ -23,7 +23,6 @@ import FileViewer from "react-native-file-viewer";
import { ToastManager } from "../../../services/event-manager";
import { SIZE } from "../../../utils/size";
import { Button } from "../../ui/button";
import { strings } from "@notesnook/intl";
export const ShareComponent = ({ uri, name, padding }) => {
return (
<View
@@ -32,7 +31,7 @@ export const ShareComponent = ({ uri, name, padding }) => {
}}
>
<Button
title={strings.open()}
title="Open"
type="accent"
width="100%"
fontSize={SIZE.md}
@@ -42,7 +41,8 @@ export const ShareComponent = ({ uri, name, padding }) => {
showAppsSuggestions: true
}).catch(() => {
ToastManager.show({
heading: strings.noApplicationFound(name),
heading: "Cannot open",
message: `No application found to open ${name} file.`,
type: "success",
context: "local"
});
@@ -50,7 +50,7 @@ export const ShareComponent = ({ uri, name, padding }) => {
}}
/>
<Button
title={strings.share()}
title="Share"
type="shade"
width="100%"
fontSize={SIZE.md}

View File

@@ -34,7 +34,6 @@ import { Button } from "../../ui/button";
import Seperator from "../../ui/seperator";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
export const Issue = ({ defaultTitle, defaultBody, issueTitle }) => {
const { colors } = useThemeColors();
@@ -76,7 +75,8 @@ Logged in: ${user ? "yes" : "no"}`,
} catch (e) {
setLoading(false);
ToastManager.show({
heading: e.message,
heading: "An error occured",
message: e.message,
type: "error"
});
}
@@ -99,14 +99,14 @@ Logged in: ${user ? "yes" : "no"}`,
gap: 10
}}
>
<Heading>{strings.issueCreatedHeading()}</Heading>
<Heading>Issue submitted</Heading>
<Paragraph
style={{
textAlign: "center"
}}
selectable={true}
>
{strings.issueCreatedDesc[0]()}
You can track your issue at{" "}
<Paragraph
style={{
textDecorationLine: "underline",
@@ -118,11 +118,12 @@ Logged in: ${user ? "yes" : "no"}`,
>
{issueUrl.current}
</Paragraph>
. {strings.issueCreatedDesc[1]()}
. Please note that we will respond to your issue on the given
link. We recommend that you save it.
</Paragraph>
<Button
title={strings.openIssue()}
title="Open issue"
onPress={() => {
Linking.openURL(issueUrl.current);
}}
@@ -134,14 +135,18 @@ Logged in: ${user ? "yes" : "no"}`,
) : (
<>
<DialogHeader
title={issueTitle || strings.issueTitle()}
paragraph={issueTitle ? strings.issueDesc() : strings.issueDesc2()}
title={issueTitle || "Report issue"}
paragraph={
issueTitle
? "We are sorry, it seems that the app crashed due to an error. You can submit a bug report below so we can fix this asap."
: "Let us know if you have faced any issue/bug while using Notesnook."
}
/>
<Seperator half />
<TextInput
placeholder={strings.title()}
placeholder="Title"
onChangeText={(v) => (title.current = v)}
defaultValue={title.current}
style={{
@@ -160,7 +165,13 @@ Logged in: ${user ? "yes" : "no"}`,
<TextInput
ref={bodyRef}
multiline
placeholder={strings.issuePlaceholder()}
placeholder={`Tell us more about the issue you are facing.
For example:
- What were you trying to do in the app?
- What did you expect to happen?
- Steps to reproduce the issue
- Things you have tried etc.`}
numberOfLines={5}
textAlignVertical="top"
onChangeText={(v) => (body.current = v)}
@@ -200,7 +211,7 @@ Logged in: ${user ? "yes" : "no"}`,
<Seperator />
<Button
onPress={onPress}
title={loading ? null : strings.submit()}
title={loading ? null : "Submit"}
loading={loading}
width="100%"
type="accent"
@@ -214,10 +225,10 @@ Logged in: ${user ? "yes" : "no"}`,
textAlign: "center"
}}
>
{strings.issueNotice[0]()}{" "}
The information above will be publically available at{" "}
<Text
onPress={() => {
Linking.openURL("https://github.com/streetwriters/notesnook/issues");
Linking.openURL("https://github.com/streetwriters/notesnook");
}}
style={{
textDecorationLine: "underline",
@@ -226,7 +237,8 @@ Logged in: ${user ? "yes" : "no"}`,
>
github.com/streetwriters/notesnook.
</Text>{" "}
{strings.issueNotice[1]()}{" "}
If you want to ask something in general or need some assistance, we
would suggest that you{" "}
<Text
style={{
textDecorationLine: "underline",
@@ -243,7 +255,7 @@ Logged in: ${user ? "yes" : "no"}`,
}
}}
>
{strings.issueNotice[2]()}
join our community on Discord.
</Text>
</Paragraph>
</>

View File

@@ -39,7 +39,6 @@ import {
EditorEvents,
editorController
} from "../../../screens/editor/tiptap/utils";
import { strings } from "@notesnook/intl";
const ListNoteItem = ({
id,
@@ -118,7 +117,7 @@ const ListBlockItem = ({
{item?.content.length > 200
? item?.content.slice(0, 200) + "..."
: !item.content || item.content.trim() === ""
? strings.linkNoteEmptyBlock()
? "(Empty block)"
: item.content}
</Paragraph>
@@ -238,8 +237,8 @@ export default function LinkNote(props: {
<Input
placeholder={
selectedNote
? strings.searchSectionToLinkPlaceholder()
: strings.searchNoteToLinkPlaceholder()
? "Search a section of a note to link to"
: "Search a note to link to"
}
containerStyle={{
width: "100%"
@@ -257,7 +256,7 @@ export default function LinkNote(props: {
}}
>
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
{strings.linkNoteSelectedNote()}
SELECTED NOTE
</Paragraph>
<Pressable
onPress={() => {
@@ -287,7 +286,7 @@ export default function LinkNote(props: {
<Paragraph numberOfLines={1}>{selectedNote?.title}</Paragraph>
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
{strings.tapToDeselect()}
Tap to deselect
</Paragraph>
</View>
</Pressable>
@@ -300,7 +299,7 @@ export default function LinkNote(props: {
color={colors.secondary.paragraph}
size={SIZE.xs}
>
{strings.linkNoteToSection()}
LINK TO A SECTION
</Paragraph>
) : null}
</View>
@@ -343,7 +342,7 @@ export default function LinkNote(props: {
style={{
marginTop: 10
}}
title={strings.createLink()}
title="Create link"
type="accent"
width="100%"
onPress={() => {

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