Compare commits

..

1 Commits

Author SHA1 Message Date
Ammar Ahmed
bb42a2e1a4 ci: skip testflight 2024-08-08 10:19:10 +05:00
1065 changed files with 100599 additions and 84667 deletions

View File

@@ -15,7 +15,6 @@ const SCOPES = [
"mobile",
"web",
"vericrypt",
"monograph",
"desktop",
"crypto",
"editor",
@@ -35,8 +34,7 @@ const SCOPES = [
"common",
"global",
"docs",
"themebuilder",
"intl"
"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

@@ -22,7 +22,7 @@ jobs:
- name: Use specific Java version for the builds
uses: joschi/setup-jdk@v2
with:
java-version: "17"
java-version: "11"
architecture: "x64"
- name: Install node modules

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:
@@ -89,13 +89,13 @@ jobs:
- name: CCache Stats After Build
run: ccache -sv
- name: "Upload app to TestFlight"
uses: apple-actions/upload-testflight-build@v1
with:
app-path: Notesnook.ipa
issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
api-key-id: ${{ secrets.API_KEY_ID }}
api-private-key: ${{ secrets.API_KEY }}
# - name: "Upload app to TestFlight"
# uses: apple-actions/upload-testflight-build@v1
# with:
# app-path: Notesnook.ipa
# issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
# api-key-id: ${{ secrets.API_KEY_ID }}
# api-private-key: ${{ secrets.API_KEY }}
- name: Upload Notesnook.ipa to Github
uses: actions/upload-artifact@v4

View File

@@ -1,89 +0,0 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# GitHub recommends pinning actions to a commit SHA.
# To get a newer version, you will need to update the SHA.
# You can also reference a tag or branch, but the action may change without warning.
name: Publish @notesnook/monograph
on: workflow_dispatch
jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
attestations: write
id-token: write
steps:
- name: Check out the repo
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=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
# Setup Buildx
- name: Docker Setup Buildx
uses: docker/setup-buildx-action@v3
with:
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
ecr: auto
logout: true
# Pull previous image from docker hub to use it as cache to improve the image build time.
- name: docker pull cache image
continue-on-error: true
run: docker pull streetwriters/monograph:latest
# Setup QEMU
# - name: Set up QEMU
# uses: docker/setup-qemu-action@v2
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: streetwriters/monograph
- name: Build and push Docker image
id: push
uses: docker/build-push-action@v6
with:
context: apps/monograph
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
cache-from: streetwriters/monograph:latest
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v1
with:
subject-name: index.docker.io/streetwriters/monograph
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true

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();
}
});

View File

@@ -1,38 +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 { _electron as electron } from "playwright";
const executablePath = process.env.EXECUTABLE_PATH;
// process.platform === "linux"
// ? "output/linux-unpacked/Notesnook"
// : process.platform === "darwin"
// ? "output/mac/Notesnook.app/Contents/MacOS/Notesnook"
// : "output/win-unpacked/Notesnook.exe";
export async function launchApp() {
const app = await electron.launch({
executablePath
});
const page = await app.firstWindow();
return { app, page };
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,58 +2,39 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "3.0.22",
"version": "3.0.13",
"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",
"electron-trpc": "0.6.1",
"electron-updater": "^6.3.4",
"icojs": "^0.19.4",
"sqlite-better-trigram": "^0.0.2",
"@notesnook/crypto": "file:../../packages/crypto",
"@trpc/client": "10.38.3",
"@trpc/server": "10.38.3",
"better-sqlite3-multiple-ciphers": "^9.5.0",
"electron-trpc": "0.5.2",
"electron-updater": "^6.2.1",
"icojs": "^0.17.1",
"sodium-native": "^4.1.1",
"typed-emitter": "^2.1.0",
"yargs": "^17.7.2",
"zod": "^3.23.8"
"yargs": "^17.6.2",
"zod": "^3.21.4"
},
"devDependencies": {
"@streetwriters/kysely": "^0.27.4",
"@types/node": "22.7.0",
"@types/yargs": "^17.0.33",
"chokidar": "^4.0.1",
"electron": "^31.7.4",
"electron-builder": "^25.1.8",
"esbuild": "^0.24.0",
"node-abi": "^3.68.0",
"node-gyp-build": "^4.8.2",
"playwright": "^1.48.2",
"@types/node": "18.16.1",
"@types/yargs": "^17.0.24",
"chokidar": "^3.5.3",
"electron": "^29.3.1",
"electron-builder": "^24.13.3",
"esbuild": "^0.23.0",
"kysely": "^0.27.3",
"node-gyp-build": "^4.8.0",
"prebuildify": "^6.0.1",
"slugify": "^1.6.6",
"tree-kill": "^1.2.2",
"undici": "^6.19.8",
"vitest": "^2.1.5"
"undici": "^6.14.1"
},
"optionalDependencies": {
"dmg-license": "^1.0.11"
@@ -62,11 +43,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 +75,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 +148,10 @@
]
}
],
"signtoolOptions": {
"signingHashAlgorithms": [
"sha256"
],
"sign": "./scripts/sign.js"
},
"signingHashAlgorithms": [
"sha256"
],
"sign": "./scripts/sign.js",
"icon": "assets/icons/app.ico"
},
"portable": {
@@ -194,8 +159,7 @@
},
"nsis": {
"oneClick": true,
"createDesktopShortcut": "always",
"deleteAppDataOnUninstall": true
"createDesktopShortcut": "always"
},
"linux": {
"target": [

View File

@@ -1,8 +1,8 @@
diff --git a/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js b/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
index 739a48c..7a7b4af 100644
index fcb7f54..cb1c7f7 100644
--- a/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
+++ b/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
@@ -117,6 +117,7 @@ class LinuxTargetHelper {
@@ -116,6 +116,7 @@ class LinuxTargetHelper {
StartupWMClass: appInfo.productName,
...extra,
...targetSpecificOptions.desktop,
@@ -10,7 +10,7 @@ index 739a48c..7a7b4af 100644
};
const description = this.getDescription(targetSpecificOptions);
if (!(0, builder_util_1.isEmptyOrSpaces)(description)) {
@@ -160,6 +161,23 @@ class LinuxTargetHelper {
@@ -159,6 +160,23 @@ class LinuxTargetHelper {
data += `\n${name}=${desktopMeta[name]}`;
}
data += "\n";

View File

@@ -0,0 +1,23 @@
diff --git a/node_modules/electron-trpc/dist/main.mjs b/node_modules/electron-trpc/dist/main.mjs
index 379cf3b..2644e5d 100644
--- a/node_modules/electron-trpc/dist/main.mjs
+++ b/node_modules/electron-trpc/dist/main.mjs
@@ -221,9 +221,16 @@ class G {
i(this, c).includes(r) || (i(this, c).push(r), I(this, T, W).call(this, r));
}
detachWindow(r) {
+
y(this, c, i(this, c).filter((n) => n !== r));
- for (const [n, t] of i(this, u).entries())
- n.startsWith(`${r.webContents.id}-`) && (t.unsubscribe(), i(this, u).delete(n));
+ for (const [n, t] of i(this, u).entries()) {
+ try {
+ n.startsWith(`${r.webContents.id}-`) && (t.unsubscribe(), i(this, u).delete(n));
+ } catch(e) {
+ console.error(e);
+ // ignore
+ }
+ }
}
}
c = new WeakMap(), u = new WeakMap(), T = new WeakSet(), W = function(r) {

View File

@@ -1,39 +0,0 @@
diff --git a/node_modules/electron-trpc/dist/main.mjs b/node_modules/electron-trpc/dist/main.mjs
index 07971db..b67a3cf 100644
--- a/node_modules/electron-trpc/dist/main.mjs
+++ b/node_modules/electron-trpc/dist/main.mjs
@@ -537,8 +537,15 @@ class be {
attachWindow(r) {
F(this, w).includes(r) || (y("Attaching window", r.id), F(this, w).push(r), v(this, N, Z).call(this, r));
}
- detachWindow(r) {
- y("Detaching window", r.id), L(this, w, F(this, w).filter((e) => e !== r)), v(this, R, M).call(this, { webContentsId: r.webContents.id });
+ detachWindow(r, webContentsId) {
+ y("Detaching window", r.id);
+
+ if (r.isDestroyed() && webContentsId === undefined) {
+ throw new Error('webContentsId is required when calling detachWindow on a destroyed window');
+ }
+
+ L(this, w, F(this, w).filter((e) => e !== r));
+ v(this, R, M).call(this, { webContentsId: webContentsId ?? r.webContents.id });
}
}
w = new WeakMap(), E = new WeakMap(), R = new WeakSet(), M = function({
@@ -548,6 +555,7 @@ w = new WeakMap(), E = new WeakMap(), R = new WeakSet(), M = function({
for (const [i, a] of F(this, E).entries())
i.startsWith(`${r}-${e ?? ""}`) && (y("Closing subscription", i), a.unsubscribe(), F(this, E).delete(i));
}, N = new WeakSet(), Z = function(r) {
+ const webContentsId = r.webContents.id;
r.webContents.on("did-start-navigation", ({ frame: e }) => {
y(
"Handling webContents `did-start-navigation` event",
@@ -558,7 +566,7 @@ w = new WeakMap(), E = new WeakMap(), R = new WeakSet(), M = function({
frameRoutingId: e.routingId
});
}), r.webContents.on("destroyed", () => {
- y("Handling webContents `destroyed` event"), this.detachWindow(r);
+ y("Handling webContents `destroyed` event"), this.detachWindow(r, webContentsId);
});
};
const me = ({

View File

@@ -0,0 +1,46 @@
diff --git a/node_modules/node-gyp-build/bin.js b/node_modules/node-gyp-build/bin.js
index 3fbcdf0..7ca3ab5 100644
--- a/node_modules/node-gyp-build/bin.js
+++ b/node_modules/node-gyp-build/bin.js
@@ -16,7 +16,8 @@ if (!buildFromSource()) {
}
function build () {
- var args = [os.platform() === 'win32' ? 'node-gyp.cmd' : 'node-gyp', 'rebuild']
+ var win32 = os.platform() === 'win32'
+ var args = [win32 ? 'node-gyp.cmd' : 'node-gyp', 'rebuild']
try {
var pkg = require('node-gyp/package.json')
@@ -27,7 +28,7 @@ function build () {
]
} catch (_) {}
- proc.spawn(args[0], args.slice(1), { stdio: 'inherit' }).on('exit', function (code) {
+ proc.spawn(args[0], args.slice(1), { stdio: 'inherit', shell: win32, windowsHide: true }).on('exit', function (code) {
if (code || !process.argv[3]) process.exit(code)
exec(process.argv[3]).on('exit', function (code) {
process.exit(code)
@@ -45,15 +46,18 @@ function preinstall () {
function exec (cmd) {
if (process.platform !== 'win32') {
- var shell = os.platform() === 'android' ? 'sh' : '/bin/sh'
- return proc.spawn(shell, ['-c', '--', cmd], {
+ var shell = os.platform() === 'android' ? 'sh' : true
+ return proc.spawn(cmd, [], {
+ shell,
stdio: 'inherit'
})
}
- return proc.spawn(process.env.comspec || 'cmd.exe', ['/s', '/c', '"' + cmd + '"'], {
+ return proc.spawn(cmd, [], {
windowsVerbatimArguments: true,
- stdio: 'inherit'
+ stdio: 'inherit',
+ shell: true,
+ windowsHide: true
})
}

View File

@@ -63,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`);
@@ -94,7 +94,7 @@ async function patchBetterSQLite3() {
);
const json = JSON.parse(await readFile(jsonPath, "utf-8"));
json.version = "11.5.1";
json.version = "9.5.1";
json.homepage = "https://github.com/thecodrr/better-sqlite3-multiple-ciphers";
json.repository.url =
"git://github.com/thecodrr/better-sqlite3-multiple-ciphers.git";

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

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import type { Database, Statement } from "better-sqlite3-multiple-ciphers";
import type { QueryResult } from "@streetwriters/kysely";
import type { QueryResult } from "kysely";
type SQLiteCompatibleType =
| number
@@ -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

@@ -29,37 +29,19 @@ type Progress = { percent: number };
const t = initTRPC.create();
let cancellationToken: CancellationToken | undefined = undefined;
let downloadTimeout: NodeJS.Timeout | undefined = undefined;
export const updaterRouter = t.router({
autoUpdates: t.procedure.query(() => config.automaticUpdates),
install: t.procedure.query(() => autoUpdater.quitAndInstall()),
download: t.procedure.query(async () => {
if (cancellationToken) return;
clearTimeout(downloadTimeout);
await new Promise<string[]>((resolve, reject) => {
downloadTimeout = setTimeout(async () => {
cancellationToken = new CancellationToken();
autoUpdater.isUpdaterActive();
await autoUpdater
.downloadUpdate(cancellationToken)
.then(resolve)
.catch(reject)
.finally(() => (cancellationToken = undefined));
}, 1000);
});
cancellationToken = new CancellationToken();
await autoUpdater
.downloadUpdate(cancellationToken)
.finally(() => (cancellationToken = undefined));
}),
check: t.procedure.query(async () => {
if (cancellationToken) return;
clearTimeout(downloadTimeout);
await new Promise<void>((resolve) => {
downloadTimeout = setTimeout(async () => {
await autoUpdater
.checkForUpdates()
.catch(console.error)
.finally(resolve);
}, 1000);
});
await autoUpdater.checkForUpdates().catch(console.error);
}),
toggleAutoUpdates: t.procedure

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,25 +19,26 @@ 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 { NNCrypto } from "@notesnook/crypto";
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 NativeNNCrypto: (new () => NNCrypto) | undefined;
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;
});
// globalThis.NativeNNCrypto = require("@notesnook/crypto").NNCrypto;
globalThis.NativeNNCrypto = require("@notesnook/crypto").NNCrypto;
globalThis.os = () => (MAC_APP_STORE ? "mas" : platform());

View File

@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { NativeImage, nativeImage } from "electron";
import path from "path";
import { isDevelopment } from "./index";
import { ParsedImage, parseICO } from "icojs";
import { parse, ParsedImage } from "icojs";
import { getSystemTheme } from "./theme";
import { readFile } from "fs/promises";
@@ -71,7 +71,7 @@ export class AssetManager {
`${icon}${prefix}.ico`
);
const icoBuffer = await readFile(icoPath);
const images = await parseICO(icoBuffer, "image/png");
const images = await parse(icoBuffer, "image/png");
ALL_ICONS.push({ id: icon, images, prefix });
}
}

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

@@ -38,8 +38,7 @@ DerivedData
*.ipa
*.xcuserstate
*.hprof
**/.xcode.env.local
native/cache
ios/.xcode.env.local
# 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

@@ -0,0 +1,156 @@
/*
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 { Platform } from "react-native";
import { MMKV } from "../database/mmkv";
import { useSettingStore } from "../../stores/use-setting-store";
const WEBSITE_ID = "3c6890ce-8410-49d5-8831-15fb2eb28a21";
const baseUrl = "https://analytics.streetwriters.co/api/collect";
const UA =
Platform.OS === "ios"
? "Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1"
: `
Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36`;
/**
*
* @param {Routes} route
* @param {Routes[]} conditions
* @returns
*/
async function canUpdateAnalytics(route, conditions = []) {
if (!useSettingStore?.getState()?.settings?.telemetry) return false;
let eventsList = MMKV.getString("notesnookUserEvents");
if (eventsList) {
eventsList = JSON.parse(eventsList);
}
if (eventsList && eventsList[route]) {
console.log("analytics: event already sent", route);
return false;
}
if (route !== "/welcome") {
for (let cond of conditions) {
if (!eventsList || !eventsList[cond]) {
console.log("analytics: conditions not met for event", route, cond);
return false;
}
}
}
console.log("analytics: will send event", route);
return true;
}
async function saveAnalytics(route, value = true) {
let eventsList = MMKV.getString("notesnookUserEvents");
if (eventsList) {
eventsList = JSON.parse(eventsList);
} else {
eventsList = {};
}
eventsList[route] = value;
MMKV.setString("notesnookUserEvents", JSON.stringify(eventsList));
}
/**
*@typedef {"/welcome" | "/home" | "/signup" | "/first-note" | "/account-created" | "/pro-sheet" | "/pro-plans" | "/iap-native" | "/pro-screen" | "/editor" | "/editor-toolbar" | "/properties" | "/sidemenu"} Routes
* @param {Routes} prevRoute
* @param {Routes} route
* @param {Routes[]} conditions
* @param {boolean} once
* @returns
*/
async function pageView(
route,
prevRoute = "",
conditions = ["/welcome"],
once = true
) {
if (__DEV__) return;
if (!(await canUpdateAnalytics(route, conditions)) && once) return;
let body = {
payload: {
website: WEBSITE_ID,
url: `notesnook-${Platform.OS}${prevRoute}${route}`,
referrer: `https://notesnook.com/notesnook-${Platform.OS}${prevRoute}`,
hostname: `notesnook-${Platform.OS}`,
language: "en-US",
screen: "1920x1080"
},
type: "pageview"
};
try {
let response = await fetch(baseUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": UA
},
body: JSON.stringify(body)
});
console.log("analytics: event sent", route);
await saveAnalytics(route);
return await response.text();
} catch (e) {
console.log(e);
}
}
async function sendEvent(type, value, once = true) {
if (__DEV__) return;
if (!(await canUpdateAnalytics(type)) && once) return;
let body = {
payload: {
website: WEBSITE_ID,
url: "/",
event_type: type,
event_value: value,
hostname: "notesnook-android-app",
language: "en-US",
screen: "1920x1080"
},
type: "event"
};
try {
let response = await fetch(baseUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": UA
},
body: JSON.stringify(body)
});
console.log(response.status);
return await response.text();
} catch (e) {
console.log(e);
}
}
export default {
sendEvent,
pageView,
saveAnalytics
};

View File

@@ -18,46 +18,55 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import "./logger";
import { database } from "@notesnook/common";
import { logger as dbLogger } from "@notesnook/core";
import { logger as dbLogger } from "@notesnook/core/dist/logger";
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 { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
import filesystem from "../filesystem";
import Storage from "./storage";
import { RNSqliteDriver } from "./sqlite.kysely";
import { getDatabaseKey } from "./encryption";
import SettingsService from "../../services/settings";
import { strings } from "@notesnook/intl";
database.host(
__DEV__
? {
API_HOST: "https://api.notesnook.com",
AUTH_HOST: "https://auth.streetwriters.co",
SSE_HOST: "https://events.streetwriters.co",
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
ISSUES_HOST: "https://issues.streetwriters.co"
// API_HOST: "http://192.168.43.5:5264",
// AUTH_HOST: "http://192.168.43.5:8264",
// SSE_HOST: "http://192.168.43.5:7264",
// SUBSCRIPTIONS_HOST: "http://192.168.43.5:9264",
// ISSUES_HOST: "http://192.168.43.5:2624"
}
: {
API_HOST: "https://api.notesnook.com",
AUTH_HOST: "https://auth.streetwriters.co",
SSE_HOST: "https://events.streetwriters.co",
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
ISSUES_HOST: "https://issues.streetwriters.co"
}
);
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);
database.host({
API_HOST: "https://api.notesnook.com",
AUTH_HOST: "https://auth.streetwriters.co",
SSE_HOST: "https://events.streetwriters.co",
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
ISSUES_HOST: "https://issues.streetwriters.co",
...(SettingsService.getProperty("serverUrls") || {})
});
database.setup({
storage: Storage,
eventsource: Platform.OS === "ios" ? EventSource : AndroidEventSource,
fs: filesystem,
compressor: () => ({
compressor: {
compress: Gzip.deflate,
decompress: Gzip.inflate
}),
},
batchSize: 100,
sqliteOptions: {
dialect: (name) => ({

View File

@@ -17,12 +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 { initialize } from "@notesnook/core";
import {
SqliteAdapter,
SqliteIntrospector,
SqliteQueryCompiler
} from "@streetwriters/kysely";
import { initialize } from "@notesnook/core/dist/logger";
import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
import { Platform } from "react-native";
import { setLogger } from ".";
import { RNSqliteDriver } from "./sqlite.kysely";

View File

@@ -17,14 +17,9 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import type {
DatabaseConnection,
Driver,
QueryResult
} from "@streetwriters/kysely";
import { CompiledQuery } from "@streetwriters/kysely";
import type { DatabaseConnection, Driver, QueryResult } from "kysely";
import { CompiledQuery } from "kysely";
import { QuickSQLiteConnection, open } from "react-native-quick-sqlite";
import { strings } from "@notesnook/intl";
type Config = { dbName: string; async: boolean; location: string };
@@ -98,7 +93,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

@@ -21,15 +21,15 @@ import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import {
decrypt,
decryptMulti,
deriveCryptoKey,
encrypt,
encryptMulti,
generateCryptoKey,
getCryptoKey,
getRandomBytes,
hash,
removeCryptoKey
removeCryptoKey,
decryptMulti,
encryptMulti
} from "./encryption";
import { MMKV } from "./mmkv";

View File

@@ -20,6 +20,7 @@ import { Dimensions, Image, Platform } from "react-native";
import ImageResizer from "@bam.tech/react-native-image-resizer";
import RNFetchBlob from "react-native-blob-util";
console.log("hello world");
/**
* Scale down & compress images to screen width
* for loading in editor.

View File

@@ -18,21 +18,24 @@ 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 {
getFileNameWithExtension,
isImage,
isDocument
} from "@notesnook/core/dist/utils/filename";
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 RNFetchBlob from "react-native-blob-util";
import { ShareComponent } from "../../components/sheets/export-notes/share";
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";
import { createCacheDir, exists } from "./io";
import { IOS_APPGROUPID } from "../../utils/constants";
export const FileDownloadStatus = {
Success: 1,
@@ -55,10 +58,15 @@ export async function downloadAllAttachments() {
* @param onProgress
* @returns
*/
export async function downloadAttachments(attachments) {
export async function downloadAttachments(
attachments,
onProgress,
canceled,
groupId
) {
await createCacheDir();
if (!attachments || !attachments.length) return;
const groupId = `download-all-${Date.now()}`;
const result = new Map();
let outputFolder;
if (Platform.OS === "android") {
@@ -82,115 +90,92 @@ export async function downloadAttachments(attachments) {
await RNFetchBlob.fs.mkdir(zipSourceFolder);
const isCancelled = () => {
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(),
canceled: true
});
return true;
}
};
for (let i = 0; i < attachments.length; i++) {
if (isCancelled()) return;
let attachment = await db.attachments.attachment(attachments[i]);
const hash = attachment.hash;
try {
useAttachmentStore.getState().setDownloading({
groupId: groupId,
current: i + 1,
total: attachments.length,
filename: attachment.hash
});
if (canceled.current) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
return;
}
onProgress?.(
i + 1 / attachments.length,
`Downloading attachments (${i + 1}/${
attachments.length
})... Please wait`
);
// Download to cache
let uri = await downloadAttachment(hash, false, {
silent: true,
cache: true,
groupId: groupId
});
if (isCancelled()) return;
if (!uri) throw new Error(strings.failedToDownloadFile());
if (canceled.current) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
return;
}
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);
} catch (e) {
DatabaseLogger.error(e);
}
}
useAttachmentStore.getState().setDownloading({
groupId: groupId,
current: 0,
total: 0,
success: true
});
if (isCancelled()) return;
let sub;
try {
useAttachmentStore.getState().setDownloading({
current: 0,
total: 1,
message: strings.savingZipFile(),
groupId
});
// If all goes well, zip the notesnook-attachments folder in cache.
sub = subscribe(({ progress }) => {
useAttachmentStore.getState().setDownloading({
groupId,
current: progress,
total: 1,
message: strings.savingZipFile((progress * 100).toFixed(1))
result.set(hash, {
filename: attachment.filename,
status: FileDownloadStatus.Success,
attachment: attachment
});
});
await zip(zipSourceFolder, zipOutputFile);
sub?.remove();
if (Platform.OS === "android") {
// Move the zip to user selected directory.
const file = await ScopedStorage.createFile(
outputFolder,
`notesnook-attachments-${Date.now()}.zip`,
"application/zip"
);
await copyFileAsync(`file://${zipOutputFile}`, file.uri);
} catch (e) {
result.set(hash, {
filename: attachment.filename,
status: FileDownloadStatus.Fail,
reason: e
});
ToastManager.error(e, "Error downloading attachment");
}
}
if (canceled.current) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
return;
}
if (result?.size) {
let sub;
try {
onProgress?.(0, `Zipping... Please wait`);
// If all goes well, zip the notesnook-attachments folder in cache.
useAttachmentStore.getState().setDownloading({
current: 0,
total: 0,
message: undefined,
success: true,
groupId
});
releasePermissions(outputFolder);
} catch (e) {
useAttachmentStore.getState().setDownloading({
current: 0,
total: 0,
message: undefined,
success: false,
groupId
});
releasePermissions(outputFolder);
sub?.remove();
ToastManager.error(e, "Error zipping attachments");
}
// Remove source & zip file from cache.
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
if (Platform.OS === "android") {
RNFetchBlob.fs.unlink(zipOutputFile).catch(console.log);
sub = subscribe(({ progress }) => {
onProgress(
progress,
`Saving zip file (${(progress * 100).toFixed(1)}%)... Please wait`
);
});
await zip(zipSourceFolder, zipOutputFile);
sub?.remove();
onProgress(1, `Saving zip file... Please wait`);
if (Platform.OS === "android") {
// Move the zip to user selected directory.
const file = await ScopedStorage.createFile(
outputFolder,
`notesnook-attachments-${Date.now()}.zip`,
"application/zip"
);
await copyFileAsync(`file://${zipOutputFile}`, file.uri);
}
onProgress?.(1, `Done`);
releasePermissions(outputFolder);
} catch (e) {
releasePermissions(outputFolder);
sub?.remove();
ToastManager.error(e, "Error zipping attachments");
}
// Remove source & zip file from cache.
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
if (Platform.OS === "android") {
RNFetchBlob.fs.unlink(zipOutputFile).catch(console.log);
}
}
return result;
}
export default async function downloadAttachment(
@@ -224,29 +209,9 @@ export default async function downloadAttachment(
}
try {
useAttachmentStore.getState().setDownloading({
groupId: options.groupId || attachment.hash,
current: 0,
total: 1,
filename: attachment.filename
});
await db
.fs()
.downloadFile(
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
});
.downloadFile(options.groupId || attachment.hash, attachment.hash);
if (!(await exists(attachment.hash))) {
DatabaseLogger.log("Attachment does not exist after download.");
return;
@@ -260,7 +225,7 @@ export default async function downloadAttachment(
);
}
let filename = await getFileNameWithExtension(
let filename = getFileNameWithExtension(
attachment.filename,
attachment.mimeType
);
@@ -288,19 +253,32 @@ export default async function downloadAttachment(
if (!options.silent) {
ToastManager.show({
heading: strings.network.downloadSuccess(),
message: strings.network.fileDownloaded(filename),
heading: "Download successful",
message: filename + " downloaded",
type: "success"
});
}
if (
attachment.dateUploaded &&
!isImage(attachment.mimeType) &&
!isDocument(attachment.mimeType)
) {
RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
.catch(console.log);
}
if (Platform.OS === "ios" && !options.cache) {
fileUri = folder.uri + `/${filename}`;
}
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 ? null : attachment.hash,
component: <ShareComponent uri={fileUri} name={filename} padding={12} />
@@ -309,6 +287,7 @@ export default async function downloadAttachment(
return fileUri;
} catch (e) {
DatabaseLogger.error(e);
if (attachment.dateUploaded) {
RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
@@ -317,15 +296,6 @@ 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) {
throw e;

View File

@@ -17,60 +17,32 @@ 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 hosts from "@notesnook/core/dist/utils/constants";
import NetInfo from "@react-native-community/netinfo";
import RNFetchBlob from "react-native-blob-util";
import { ToastManager } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { DatabaseLogger, db } from "../database";
import { cacheDir, fileCheck } from "./utils";
import { createCacheDir, exists } from "./io";
import { ABYTES, cacheDir, getUploadedFileSize, parseS3Error } from "./utils";
export async function downloadFile(filename, requestOptions, cancelToken) {
if (!requestOptions) {
DatabaseLogger.log(
`Error downloading file: ${filename}, reason: No requestOptions`
);
export async function downloadFile(filename, data, cancelToken) {
if (!data) {
DatabaseLogger.log(`Error downloading file: ${filename}, reason: No data`);
return false;
}
DatabaseLogger.log(`Downloading ${filename}`);
await createCacheDir();
let { url, headers, chunkSize } = requestOptions;
let tempFilePath = `${cacheDir}/${filename}_temp`;
let originalFilePath = `${cacheDir}/${filename}`;
let { url, headers } = data;
let path = `${cacheDir}/${filename}`;
try {
if (await exists(filename)) {
DatabaseLogger.log(`File Exists already: ${filename}`);
return true;
}
const attachment = await db.attachments.attachment(filename);
const size = await getUploadedFileSize(filename);
if (size === -1) {
const error = `${strings.fileVerificationFailed()} (File hash: ${filename})`;
throw new Error(error);
}
if (size === 0) {
const error = `${strings.fileLengthError()} (File hash: ${filename})`;
await db.attachments.markAsFailed(attachment.id, error);
throw new Error(error);
}
const totalChunks = Math.ceil(size / chunkSize);
const decryptedLength = size - totalChunks * ABYTES;
if (attachment && attachment.size !== decryptedLength) {
const error = `${strings.fileLengthMismatch(
attachment.size,
decryptedLength
)} (File hash: ${filename})`;
await db.attachments.markAsFailed(attachment.id, error);
throw new Error(error);
}
let res = await fetch(url, {
method: "GET",
headers
@@ -80,9 +52,7 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
DatabaseLogger.log(
`Error downloading file: ${filename}, ${res.status}, ${res.statusText}, reason: Unable to resolve download url`
);
throw new Error(
`${res.status}: ${strings.failedToResolvedDownloadUrl()}`
);
throw new Error(`${res.status}: Unable to resolve download url`);
}
const downloadUrl = await res.text();
@@ -91,73 +61,52 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
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");
}
let totalSize = 0;
DatabaseLogger.log(`Download starting: ${filename}`);
let request = RNFetchBlob.config({
path: tempFilePath,
IOSBackgroundTask: true,
overwrite: true
path: path,
IOSBackgroundTask: true
})
.fetch("GET", downloadUrl, null)
.progress(async (recieved, total) => {
.progress((recieved, total) => {
useAttachmentStore
.getState()
.setProgress(0, total, filename, recieved, "download");
totalSize = total;
DatabaseLogger.log(`Downloading: ${filename}, ${recieved}/${total}`);
});
cancelToken.cancel = () => {
useAttachmentStore.getState().remove(filename);
request.cancel();
RNFetchBlob.fs.unlink(tempFilePath).catch(console.log);
DatabaseLogger.log(`Download cancelled: ${filename}`);
};
let response = await request;
console.log(response.info().headers);
const contentType =
response.info().headers?.["content-type"] ||
response.info().headers?.["Content-Type"];
if (contentType === "application/xml") {
const error = parseS3Error(await response.text());
throw new Error(`[${error.Code}] ${error.Message}`);
}
await fileCheck(response, totalSize);
let status = response.info().status;
useAttachmentStore.getState().remove(filename);
if (exists(originalFilePath)) {
await RNFetchBlob.fs.unlink(originalFilePath).catch(console.log);
}
await RNFetchBlob.fs.mv(tempFilePath, originalFilePath);
if (!(await exists(filename))) {
throw new Error("File size mismatch");
}
return status >= 200 && status < 300;
} catch (e) {
if (e.message !== "canceled" && !e.message.includes("NoSuchKey")) {
const toast = {
heading: strings.downloadError(),
if (e.message !== "canceled") {
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);
RNFetchBlob.fs.unlink(path).catch(console.log);
DatabaseLogger.error(e, {
url,
headers
@@ -166,23 +115,37 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
}
}
export async function getUploadedFileSize(hash) {
try {
const url = `${hosts.API_HOST}/s3?name=${hash}`;
const token = await db.tokenManager.getAccessToken();
const attachmentInfo = await fetch(url, {
method: "HEAD",
headers: { Authorization: `Bearer ${token}` }
});
const contentLength = parseInt(
attachmentInfo.headers?.get("content-length")
);
return isNaN(contentLength) ? 0 : contentLength;
} catch (e) {
DatabaseLogger.error(e);
return -1;
}
}
export async function checkAttachment(hash) {
const internetState = await NetInfo.fetch();
const isInternetReachable =
internetState.isConnected && internetState.isInternetReachable;
if (!isInternetReachable) return;
if (!isInternetReachable) return { success: true };
const attachment = await db.attachments.attachment(hash);
if (!attachment) return { failed: "Attachment not found." };
try {
const size = await getUploadedFileSize(hash);
console.log("File Size", size);
if (size === -1) return { success: true };
if (size === 0)
return {
failed: `File length is 0. Please upload this file again from the attachment manager. (File hash: ${hash})`
};
if (size === 0) return { failed: "File length is 0." };
} catch (e) {
return { failed: e?.message };
}

View File

@@ -17,22 +17,17 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { checkAttachment, downloadFile } from "./download";
import { checkAttachment, downloadFile, getUploadedFileSize } from "./download";
import {
clearFileStorage,
deleteFile,
exists,
readEncrypted,
writeEncryptedBase64,
hashBase64,
clearCache,
deleteCacheFileByName,
deleteCacheFileByPath,
bulkExists,
getCacheSize
hashBase64
} from "./io";
import { uploadFile } from "./upload";
import { cancelable, getUploadedFileSize } from "./utils";
import { cancelable } from "./utils";
export default {
readEncrypted,
@@ -44,10 +39,5 @@ export default {
exists,
clearFileStorage,
getUploadedFileSize,
checkAttachment,
clearCache,
deleteCacheFileByName,
deleteCacheFileByPath,
bulkExists,
getCacheSize
checkAttachment
};

View File

@@ -20,10 +20,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import Sodium from "@ammarahmed/react-native-sodium";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import { eSendEvent } from "../../services/event-manager";
import { IOS_APPGROUPID } from "../../utils/constants";
import { DatabaseLogger, db } from "../database";
import { ABYTES, cacheDir, cacheDirOld, getRandomId } from "./utils";
import { cacheDir, cacheDirOld, getRandomId } from "./utils";
export async function readEncrypted(filename, key, cipherData) {
await migrateFilesFromCache();
@@ -126,13 +125,9 @@ export async function clearFileStorage() {
}
export async function createCacheDir() {
try {
if (!(await RNFetchBlob.fs.exists(cacheDir))) {
await RNFetchBlob.fs.mkdir(cacheDir);
DatabaseLogger.log("Cache directory created");
}
} catch (e) {
DatabaseLogger.error(e);
if (!(await RNFetchBlob.fs.exists(cacheDir))) {
await RNFetchBlob.fs.mkdir(cacheDir);
DatabaseLogger.log("Cache directory created");
}
}
@@ -163,35 +158,7 @@ export async function migrateFilesFromCache() {
}
}
export async function clearCache() {
await RNFetchBlob.fs.unlink(cacheDir).catch(console.log);
await createCacheDir();
eSendEvent("cache-cleared");
}
export async function deleteCacheFileByPath(path) {
await RNFetchBlob.fs.unlink(path).catch(console.log);
}
export async function deleteCacheFileByName(name) {
const iosAppGroup =
Platform.OS === "ios"
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${name}`;
await RNFetchBlob.fs.unlink(appGroupPath).catch(console.log);
await RNFetchBlob.fs.unlink(`${cacheDir}/${name}`).catch(console.log);
}
export async function deleteDCacheFiles() {
const files = await RNFetchBlob.fs.ls(cacheDir);
for (const file of files) {
if (file.includes("_dcache")) {
await RNFetchBlob.fs.unlink(file).catch(console.log);
}
}
}
const ABYTES = 17;
export async function exists(filename) {
let path = `${cacheDir}/${filename}`;
@@ -233,37 +200,3 @@ export async function exists(filename) {
}
return exists;
}
export async function bulkExists(files) {
try {
await createCacheDir();
const cacheFiles = await RNFetchBlob.fs.ls(cacheDir);
let missingFiles = files.filter((file) => !cacheFiles.includes(file));
if (Platform.OS === "ios") {
const iosAppGroup =
Platform.OS === "ios"
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupFiles = await RNFetchBlob.fs.ls(iosAppGroup);
missingFiles = missingFiles.filter(
(file) => !appGroupFiles.includes(file)
);
}
return missingFiles;
} catch (e) {
DatabaseLogger.error(e);
return [];
}
}
export async function getCacheSize() {
await createCacheDir();
const stat = await RNFetchBlob.fs.lstat(`file://` + cacheDir);
let total = 0;
console.log("Total files", stat.length);
stat.forEach((s) => {
total += parseInt(s.size);
});
return total;
}

View File

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

View File

@@ -17,13 +17,10 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import * as ScopedStorage from "react-native-scoped-storage";
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;
export const cacheDir =
@@ -37,36 +34,24 @@ export function getRandomId(prefix) {
.replace("0.", prefix || "");
}
/**
*
* @param {string | undefined} data
* @returns
*/
export function parseS3Error(data) {
const xml = typeof data === "string" ? data : null;
export function extractValueFromXmlTag(code, xml) {
if (!xml.includes(code)) return `Unknown ${code}`;
return xml.slice(
xml.indexOf(`<${code}>`) + code.length + 2,
xml.indexOf(`</${code}>`)
);
}
const error = {
Code: "UNKNOWN",
Message: xml
};
try {
if (!xml) return error;
const doc = new DOMParser().parseFromString(xml, "text/xml");
const ErrorElement = doc.getElementsByTagName("Error")[0];
if (!ErrorElement) return error;
for (const child of ErrorElement.children) {
if (
child.textContent &&
(child.tagName === "Code" || child.tagName === "Message")
)
error[child.tagName] = child.textContent;
export async function fileCheck(response, totalSize) {
if (totalSize < 1000) {
let text = await response.text();
if (text.startsWith("<?xml")) {
let errorJson = {
Code: extractValueFromXmlTag("Code", text),
Message: extractValueFromXmlTag("Message", text)
};
throw new Error(`${errorJson.Code}: ${errorJson.Message}`);
}
return error;
} catch (e) {
return error;
}
}
@@ -74,11 +59,12 @@ export function cancelable(operation) {
const cancelToken = {
cancel: () => {}
};
return (filename, requestOptions) => {
return (filename, { url, headers }) => {
return {
execute: () => operation(filename, requestOptions, cancelToken),
execute: () => operation(filename, { url, headers }, cancelToken),
cancel: async () => {
await cancelToken.cancel();
RNFetchBlob.fs.unlink(`${cacheDir}/${filename}`).catch(console.log);
}
};
};
@@ -105,36 +91,3 @@ export async function releasePermissions(path) {
}
}
}
export async function getUploadedFileSize(hash) {
try {
const url = `${hosts.API_HOST}/s3?name=${hash}`;
const token = await db.tokenManager.getAccessToken();
const attachmentInfo = await fetch(url, {
method: "HEAD",
headers: { Authorization: `Bearer ${token}` }
});
const contentLength = parseInt(
attachmentInfo.headers?.get("content-length")
);
return isNaN(contentLength) ? 0 : contentLength;
} catch (e) {
DatabaseLogger.error(e);
return -1;
}
}
export async function checkUpload(filename, chunkSize, expectedSize) {
const size = await getUploadedFileSize(filename);
const totalChunks = Math.ceil(size / chunkSize);
const decryptedLength = size - totalChunks * ABYTES;
const error =
size === 0
? `File size is 0.`
: size === -1
? `File verification check failed.`
: expectedSize !== decryptedLength
? `File size mismatch. Expected ${size} bytes but got ${decryptedLength} bytes.`
: undefined;
if (error) throw new Error(error);
}

View File

@@ -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

@@ -30,8 +30,6 @@ import filesystem from "../../common/filesystem";
import downloadAttachment from "../../common/filesystem/download-attachment";
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
import picker from "../../screens/editor/tiptap/picker";
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
import { editorController } from "../../screens/editor/tiptap/utils";
import {
ToastManager,
eSendEvent,
@@ -51,23 +49,22 @@ import { Dialog } from "../dialog";
import { presentDialog } from "../dialog/functions";
import { openNote } from "../list-items/note/wrapper";
import { DateMeta } from "../properties/date-meta";
import SheetProvider from "../sheet-provider";
import { Button } from "../ui/button";
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";
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
import { editorController } from "../../screens/editor/tiptap/utils";
import SheetProvider from "../sheet-provider";
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");
fwdRef.current?.hide();
downloadAttachment(attachment.hash, false);
eSendEvent(eCloseSheet, contextId);
},
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,19 +113,17 @@ 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;
if (result.failed) {
db.attachments.markAsFailed(attachment.id, result.failed);
setFailed(result.failed);
const res = await filesystem.checkAttachment(attachment.hash);
if (res.failed) {
db.attachments.markAsFailed(attachment.id, res.failed);
setFailed(res.failed);
ToastManager.show({
heading: strings.fileCheckFailed(result.failed),
heading: "File check failed with error: " + res.failed,
type: "error",
context: "local"
});
@@ -136,7 +132,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 +146,13 @@ const Actions = ({
icon: "file-check"
},
{
name: strings.rename(),
name: "Rename",
onPress: () => {
presentDialog({
context: contextId as any,
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 +165,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);
@@ -219,7 +217,6 @@ const Actions = ({
style={{
maxHeight: "100%"
}}
keyboardShouldPersistTaps="never"
>
<Dialog context={contextId} />
<SheetProvider context={contextId} />
@@ -257,7 +254,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 +263,7 @@ const Actions = ({
Clipboard.setString(attachment.hash);
ToastManager.show({
type: "success",
heading: strings.hashCopied(),
heading: "Attachment hash copied",
context: "local"
});
}}
@@ -295,7 +293,7 @@ const Actions = ({
}}
size={SIZE.sm}
>
{strings.listOf()} {strings.dataTypesPlural.note()}:
List of notes:
</Heading>
{notes.map((item) => (
@@ -354,7 +352,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 +374,6 @@ Actions.present = (
setAttachments={set}
close={close}
attachment={attachment}
context={context || "global"}
/>
)
});

View File

@@ -22,6 +22,7 @@ import { Attachment, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
import { useDBItem } from "../../hooks/use-db-item";
@@ -30,8 +31,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);
@@ -45,8 +44,7 @@ export const AttachmentItem = ({
setAttachments,
pressable = true,
hideWhenNotDownloading,
context,
errorOnly
context
}: {
id: string | number;
attachments?: VirtualizedGrouping<Attachment>;
@@ -55,9 +53,9 @@ export const AttachmentItem = ({
pressable?: boolean;
hideWhenNotDownloading?: boolean;
context?: string;
errorOnly?: boolean;
}) => {
const [attachment] = useDBItem(id, "attachment", attachments);
const { colors } = useThemeColors();
const [currentProgress, setCurrentProgress] = useAttachmentProgress(
attachment,
@@ -69,8 +67,10 @@ export const AttachmentItem = ({
Actions.present(attachment, setAttachments, context);
};
return errorOnly && attachment && !attachment?.failed ? null : (
<Pressable
return hideWhenNotDownloading &&
(!currentProgress || !currentProgress.value) ? null : (
<TouchableOpacity
activeOpacity={0.9}
onPress={onPress}
style={{
flexDirection: "row",
@@ -78,6 +78,8 @@ export const AttachmentItem = ({
justifyContent: "space-between",
padding: 12,
paddingVertical: 6,
borderRadius: 5,
backgroundColor: colors.secondary.background,
minHeight: 45
}}
>
@@ -87,46 +89,41 @@ export const AttachmentItem = ({
style={{
flexShrink: 1,
flexDirection: "row",
alignItems: "center",
gap: 10
alignItems: "center"
}}
>
<View
style={{
justifyContent: "center",
alignItems: "center",
marginLeft: -5,
borderWidth: 1,
borderColor: colors.secondary.border,
paddingHorizontal: 2,
width: 30,
height: 30,
borderRadius: 5
marginLeft: -5
}}
>
<Icon name="file" size={SIZE.xxxl} color={colors.primary.icon} />
<Paragraph
adjustsFontSizeToFit
size={8}
color={colors.secondary.paragraph}
size={6}
color={colors.static.white}
style={{
maxWidth: 30
position: "absolute"
}}
numberOfLines={1}
>
{getFileExtension(attachment.filename).toUpperCase() ||
attachment.mimeType.split("/")?.[1]?.toUpperCase()}
{getFileExtension(attachment.filename).toUpperCase()}
</Paragraph>
</View>
<View
style={{
flexShrink: 1
flexShrink: 1,
marginLeft: 10
}}
>
<Paragraph
size={SIZE.sm}
size={SIZE.sm - 1}
style={{
flexWrap: "wrap"
flexWrap: "wrap",
marginBottom: 2.5
}}
numberOfLines={1}
lineBreakMode="middle"
@@ -136,8 +133,11 @@ export const AttachmentItem = ({
</Paragraph>
{!hideWhenNotDownloading ? (
<Paragraph color={colors.secondary.paragraph} size={SIZE.xxs}>
{strings.fileSize()}: {formatBytes(attachment.size)}
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
{formatBytes(attachment.size)}{" "}
{currentProgress?.type
? "(" + currentProgress.type + "ing - tap to cancel)"
: ""}
</Paragraph>
) : null}
</View>
@@ -186,6 +186,6 @@ export const AttachmentItem = ({
)}
</>
)}
</Pressable>
</TouchableOpacity>
);
};

View File

@@ -0,0 +1,297 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useRef, useState } from "react";
import { Platform, View } from "react-native";
import { db } from "../../common/database";
import { downloadAttachments } from "../../common/filesystem/download-attachment";
import {
PresentSheetOptions,
presentSheet
} from "../../services/event-manager";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { ProgressBarComponent } from "../ui/svg/lazy";
import { useThemeColors } from "@notesnook/theme";
import { FlatList } from "react-native-actions-sheet";
import { AttachmentItem } from "./attachment-item";
import { Attachment, VirtualizedGrouping } from "@notesnook/core";
const DownloadAttachments = ({
close,
attachments,
isNote,
update
}: {
attachments: VirtualizedGrouping<Attachment>;
close?: ((ctx?: string | undefined) => void) | undefined;
isNote?: boolean;
update?: (props: PresentSheetOptions) => void;
}) => {
const { colors } = useThemeColors();
const [downloading, setDownloading] = useState(false);
const [progress, setProgress] = useState({
value: 0,
statusText: "Download started... Please wait"
});
const [result, setResult] = useState(new Map());
const canceled = useRef(false);
const groupId = useRef<string>();
const onDownload = async () => {
update?.({
disableClosing: true
} as PresentSheetOptions);
setDownloading(true);
canceled.current = false;
groupId.current = Date.now().toString();
const result = await downloadAttachments(
await attachments.ids(),
(progress: number, statusText: string) =>
setProgress({ value: progress, statusText }),
canceled,
groupId.current
);
if (canceled.current) return;
setResult(result || new Map());
setDownloading(false);
update?.({
disableClosing: false
} as PresentSheetOptions);
};
const cancel = async () => {
update?.({
disableClosing: false
} as PresentSheetOptions);
canceled.current = true;
if (!groupId.current) return;
console.log(groupId.current, "canceling groupId downloads");
await db.fs().cancel(groupId.current);
setDownloading(false);
setResult(new Map());
groupId.current = undefined;
};
const failedResults = () => {
const results = [];
for (const value of result.values()) {
if (value.status === 0) results.push(value.attachment);
}
return results;
};
function getResultText() {
const downloadedAttachmentsCount =
attachments?.placeholders?.length - failedResults().length;
if (downloadedAttachmentsCount === 0)
return "Failed to download all attachments";
return `Successfully downloaded ${downloadedAttachmentsCount}/${
attachments?.placeholders.length
} attachments as a zip file at ${
Platform.OS === "android" ? "the selected folder" : "Notesnook/downloads"
}`;
}
return (
<View
style={{
alignItems: "center",
width: "100%",
paddingVertical: 12,
paddingHorizontal: 12
}}
>
<Heading>
{downloading
? "Downloading attachments"
: result?.size
? "Downloaded attachments"
: "Download attachments"}
</Heading>
{downloading ? (
<Paragraph
style={{
textAlign: "center"
}}
>
{progress.statusText}
</Paragraph>
) : result?.size ? (
<Paragraph
style={{
textAlign: "center"
}}
>
{getResultText()}
</Paragraph>
) : (
<Paragraph
style={{
textAlign: "center"
}}
>
Are you sure you want to download all attachments
{isNote ? " of this note?" : "?"}
</Paragraph>
)}
{downloading ? (
<View
style={{
width: 200,
marginTop: 10
}}
>
<ProgressBarComponent
height={5}
width={null}
animated={true}
useNativeDriver
progress={
progress.value
? progress.value / attachments.placeholders?.length
: 0
}
unfilledColor={colors.secondary.background}
color={colors.primary.accent}
borderWidth={0}
/>
</View>
) : null}
<FlatList
style={{
maxHeight: 300,
width: "100%",
minHeight: 60,
backgroundColor: colors.secondary.background,
borderRadius: 5,
marginVertical: 12
}}
data={downloading ? attachments.placeholders : undefined}
ListEmptyComponent={
<View
style={{
width: "100%",
justifyContent: "center",
alignItems: "center",
height: 60
}}
>
<Paragraph color={colors.secondary.paragraph}>
No downloads in progress.
</Paragraph>
</View>
}
keyExtractor={(index) => "attachment_download" + index}
renderItem={({ index }) => {
return (
<AttachmentItem
id={index}
setAttachments={() => {}}
pressable={false}
hideWhenNotDownloading={true}
attachments={attachments}
/>
);
}}
/>
{result?.size ? (
<Button
style={{
width: 250,
borderRadius: 100,
marginTop: 20
}}
onPress={() => {
close?.();
}}
type="accent"
title="Done"
/>
) : !downloading ? (
<View
style={{
flexDirection: "row",
width: "100%",
marginTop: 20
}}
>
<Button
style={{
flex: 1,
borderRadius: 100,
marginRight: 5
}}
onPress={() => {
close?.();
}}
type="secondary"
title="No"
/>
<Button
style={{
flex: 1,
borderRadius: 100,
marginLeft: 5
}}
onPress={onDownload}
type="accent"
title="Yes"
/>
</View>
) : (
<Button
style={{
width: 250,
borderRadius: 100,
marginTop: 20
}}
onPress={cancel}
type="error"
title="Cancel"
/>
)}
</View>
);
};
DownloadAttachments.present = (
context: string,
attachments: VirtualizedGrouping<Attachment>,
isNote?: boolean
) => {
presentSheet({
context: context,
component: (ref, close, update) => (
<DownloadAttachments
close={close}
attachments={attachments}
isNote={isNote}
update={update}
/>
)
});
};
export default DownloadAttachments;

View File

@@ -19,29 +19,21 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import {
Attachment,
FilteredSelector,
Note,
SortOptions,
VirtualizedGrouping
} from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { FilteredSelector } from "@notesnook/core/dist/database/sql-collection";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import { ActivityIndicator, View } from "react-native";
import { ActivityIndicator, ScrollView, View } from "react-native";
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
import { ScrollView } from "react-native-gesture-handler";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import create, { State } from "zustand";
import { db } from "../../common/database";
import filesystem from "../../common/filesystem";
import { downloadAttachments } from "../../common/filesystem/download-attachment";
import { AttachmentGroupProgress } from "../../screens/settings/attachment-group-progress";
import { presentSheet, ToastManager } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { presentSheet } from "../../services/event-manager";
import { useSettingStore } from "../../stores/use-setting-store";
import { SIZE } from "../../utils/size";
import { Dialog } from "../dialog";
import { presentDialog } from "../dialog/functions";
import { Header } from "../header";
import SheetProvider from "../sheet-provider";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
@@ -50,105 +42,22 @@ import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { AttachmentItem } from "./attachment-item";
import DownloadAttachments from "./download-attachments";
const DEFAULT_SORTING: SortOptions = {
sortBy: "dateEdited",
sortDirection: "desc"
};
type RecheckerProgress = {
failed: number;
passed: number;
isWorking: boolean;
shown: boolean;
filter: string;
};
interface RecheckerState extends State {
progress: {
[key: string]: RecheckerProgress;
};
setProgress: (key: string, progress: RecheckerProgress) => void;
currentFilter: string;
}
const useRechecker = create<RecheckerState>((set) => ({
progress: {
all: {
failed: 0,
passed: 0,
isWorking: false,
shown: false,
filter: "all"
}
},
currentFilter: "all",
setProgress: (key: string, progress: RecheckerProgress) => {
set((state) => ({
progress: {
...state.progress,
[key]: progress
}
}));
}
}));
const attachmentTypes = [
{
title: strings.mediaTypes.all(),
filterBy: "all"
},
{
title: strings.mediaTypes.image(),
filterBy: "images"
},
{
title: strings.mediaTypes.audio(),
filterBy: "audio"
},
{
title: strings.mediaTypes.video(),
filterBy: "video"
},
{
title: strings.mediaTypes.document(),
filterBy: "documents"
},
{
title: strings.mediaTypes.orphaned(),
filterBy: "orphaned"
},
{
title: strings.mediaTypes.errors(),
filterBy: "errors"
}
];
export const AttachmentDialog = ({
note,
isSheet
}: {
note?: Note;
isSheet: boolean;
}) => {
export const AttachmentDialog = ({ note }: { note?: Note }) => {
const { colors } = useThemeColors();
const { height } = useSettingStore((state) => state.dimensions);
const [attachments, setAttachments] =
useState<VirtualizedGrouping<Attachment>>();
const attachmentSearchValue = useRef<string>();
const [loading, setLoading] = useState(true);
const searchTimer = useRef<NodeJS.Timeout>();
const [loading, setLoading] = useState(false);
const [currentFilter, setCurrentFilter] = useState("all");
const rechecker = useRechecker((state) =>
note ? state.progress[note.id] || {} : state.progress.all
);
const currentFilterRef = useRef(currentFilter);
currentFilterRef.current = currentFilter;
useEffect(() => {
useRechecker.setState({
currentFilter: currentFilter
});
}, [currentFilter]);
const refresh = React.useCallback(() => {
if (note) {
@@ -159,7 +68,6 @@ export const AttachmentDialog = ({
sortBy: "dateModified"
})
.then((attachments) => {
setLoading(false);
setAttachments(attachments);
});
} else {
@@ -168,10 +76,7 @@ export const AttachmentDialog = ({
...DEFAULT_SORTING,
sortBy: "dateModified"
})
.then((attachments) => {
setAttachments(attachments);
setLoading(false);
});
.then((attachments) => setAttachments(attachments));
}
}, [note]);
@@ -203,71 +108,53 @@ export const AttachmentDialog = ({
setAttachments={async () => {
setAttachments(await filterAttachments(currentFilter));
}}
errorOnly={currentFilter === "errors"}
attachments={attachments}
id={index}
context={!isSheet ? "global" : "attachments-list"}
context="attachments-list"
/>
);
const onCheck = async () => {
const getState = () =>
useRechecker.getState().progress[note?.id || "all"] || {};
const setState = (state: Partial<RecheckerProgress>) =>
useRechecker.getState().setProgress(note?.id || "all", {
...getState(),
...state
});
if (!attachments) return;
setLoading(true);
if (!attachments || getState().isWorking) return;
setState({
isWorking: true,
failed: 0,
passed: 0,
shown: true,
filter: currentFilterRef.current
});
const filter = currentFilterRef.current;
const filteredAttachments = await filterAttachments(
currentFilterRef.current
);
for (let i = 0; i < filteredAttachments.placeholders.length; i++) {
if (!getState().isWorking) {
ToastManager.show({
message: strings.attachmentRecheckCancelled(),
type: "info",
context: isSheet ? "local" : "global"
});
return;
}
for (let i = 0; i < attachments.placeholders.length; i++) {
const attachment = (await attachments.item(i))?.item;
if (currentFilter == "errors" && !attachment?.failed) continue;
if (!attachment) continue;
const result = await filesystem.checkAttachment(attachment.hash);
if (!result) return;
if (result.failed) {
setState({
failed: getState().failed + 1
});
await db.attachments.markAsFailed(attachment.hash, result.failed);
} else {
setState({
passed: getState().passed + 1
});
await db.attachments.markAsFailed(attachment.id);
await db.attachments.markAsFailed(attachment.id, undefined);
}
}
if (filter === useRechecker.getState().currentFilter) {
setAttachments(await filterAttachments(currentFilter));
}
setState({
isWorking: false
});
refresh();
setLoading(false);
};
const attachmentTypes = [
{
title: "All",
filterBy: "all"
},
{
title: "Images",
filterBy: "images"
},
{
title: "Documents",
filterBy: "documents"
},
{
title: "Video",
filterBy: "video"
},
{
title: "Audio",
filterBy: "audio"
}
];
const filterAttachments = async (type: string) => {
let items: FilteredSelector<Attachment> = db.attachments.all;
@@ -296,14 +183,6 @@ export const AttachmentDialog = ({
items = note
? db.attachments.ofNote(note.id, "documents")
: db.attachments.documents;
break;
case "orphaned":
items = db.attachments.orphaned;
break;
case "errors":
items = items = note
? db.attachments.ofNote(note.id, "all")
: db.attachments.all;
}
return await items.sorted({
@@ -313,59 +192,39 @@ export const AttachmentDialog = ({
};
return (
<>
{isSheet ? (
<>
<SheetProvider context="attachments-list" />
<Dialog context="local" />
</>
) : null}
{!isSheet ? (
<Header
title={strings.manageAttachments()}
renderedInRoute="SettingsGroup"
canGoBack
headerRightButtons={[
{
onPress() {
onCheck();
},
title: strings.recheckAll()
},
{
onPress() {
if (!attachments) return;
presentDialog({
title: strings.doActions.download.attachment(
attachments.placeholders.length
),
positiveText: strings.network.download(),
positivePress: async () => {
downloadAttachments(await attachments.ids());
},
negativeText: strings.cancel()
});
},
title: strings.downloadAllAttachments()
}
]}
/>
) : (
<View
style={{
width: "100%",
alignSelf: "center",
paddingHorizontal: 12,
height: height * 0.85
}}
>
<SheetProvider context="attachments-list" />
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center"
}}
>
<Heading>Attachments</Heading>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 12
flexDirection: "row"
}}
>
<Heading>{strings.dataTypesPluralCamelCase.attachment()}</Heading>
<View
style={{
flexDirection: "row"
}}
>
{loading ? (
<ActivityIndicator
style={{
height: 40,
width: 40,
marginRight: 10
}}
size={SIZE.lg}
/>
) : (
<IconButton
name="check-all"
style={{
@@ -377,242 +236,126 @@ export const AttachmentDialog = ({
size={SIZE.lg}
onPress={onCheck}
/>
)}
<IconButton
name="download"
style={{
height: 40,
width: 40
}}
color={colors.primary.paragraph}
onPress={() => {
if (!attachments) return;
presentDialog({
title: strings.doActions.download.attachment(
attachments.placeholders.length
),
positiveText: strings.network.download(),
positivePress: async () => {
downloadAttachments(await attachments.ids());
},
negativeText: strings.cancel()
});
}}
size={SIZE.lg}
/>
</View>
<IconButton
name="download"
style={{
height: 40,
width: 40
}}
color={colors.primary.paragraph}
onPress={() => {
if (!attachments) return;
DownloadAttachments.present(
"attachments-list",
attachments,
!!note
);
}}
size={SIZE.lg}
/>
</View>
)}
</View>
<View
style={{
width: "100%",
alignSelf: "center",
paddingHorizontal: 12,
height: "100%"
<Seperator />
<Input
placeholder="Filter attachments by filename, type or hash"
onChangeText={onChangeText}
onSubmit={() => {
onChangeText(attachmentSearchValue.current as string);
}}
>
<Seperator />
<Input
placeholder={strings.filterAttachments()}
onChangeText={onChangeText}
onSubmit={() => {
onChangeText(attachmentSearchValue.current as string);
}}
/>
/>
{rechecker.shown ? (
<View>
<ScrollView
style={{
width: "100%",
height: 50,
flexDirection: "row",
backgroundColor: colors.primary.background
}}
contentContainerStyle={{
minWidth: "100%",
height: 50
}}
horizontal
>
{attachmentTypes.map((item) => (
<Button
type={
currentFilter === item.filterBy ? "secondaryAccented" : "plain"
}
key={item.title}
title={item.title}
style={{
borderRadius: 0,
borderBottomWidth: 1,
flexGrow: 1,
borderBottomColor:
currentFilter !== item.filterBy
? "transparent"
: colors.primary.accent
}}
onPress={async () => {
setCurrentFilter(item.filterBy);
setAttachments(await filterAttachments(item.filterBy));
}}
/>
))}
</ScrollView>
</View>
<FlashList
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
ListEmptyComponent={
<View
style={{
flexDirection: "row",
alignItems: "center",
width: "100%",
borderRadius: 10,
padding: 10,
borderWidth: 1,
borderColor: colors.primary.border,
gap: 12,
justifyContent: "space-between"
}}
>
<View
style={{
flexDirection: "row",
gap: 12,
alignItems: "center"
}}
>
{rechecker.isWorking ? (
<ActivityIndicator color={colors.primary.accent} />
) : (
<Icon
name="file-check"
size={30}
color={colors.primary.accent}
/>
)}
<View>
<Paragraph>
{rechecker.isWorking
? note
? strings.checkingNoteAttachments()
: strings.checkingAllAttachments()
: strings.attachmentRecheckComplete()}
</Paragraph>
<Paragraph>
{`${
rechecker.isWorking ? `${strings.pleaseWait()} ` : ""
}${strings.passed()}: ${
rechecker.passed
}, ${strings.failed()}: ${rechecker.failed}`}
</Paragraph>
</View>
</View>
<IconButton
type={rechecker.isWorking ? "errorShade" : "plain"}
name={rechecker.isWorking ? "close" : "check"}
size={SIZE.lg}
color={
rechecker.isWorking ? colors.error.icon : colors.primary.accent
}
onPress={() => {
useRechecker.getState().setProgress(note?.id || "all", {
...useRechecker.getState().progress[note?.id || "all"],
shown: false,
isWorking: false
});
}}
/>
</View>
) : null}
<View>
<ScrollView
style={{
backgroundColor: colors.primary.background,
flexWrap: "wrap",
flexDirection: "row",
paddingVertical: 12
}}
contentContainerStyle={{
height: 150,
justifyContent: "center",
alignItems: "center"
}}
horizontal
>
{attachmentTypes.map((item) =>
item.filterBy === "orphaned" && note ? null : (
<Button
type={
currentFilter === item.filterBy
? "secondaryAccented"
: "plain"
}
key={item.title}
title={item.title}
fontSize={SIZE.sm}
style={{
borderRadius: 100,
paddingHorizontal: 12,
height: 40,
minWidth: 80
}}
onPress={async () => {
const filterBy = item.filterBy;
setCurrentFilter(filterBy);
setLoading(true);
filterAttachments(filterBy)
.then((results) => {
if (filterBy !== currentFilterRef.current) return;
setAttachments(results);
setLoading(false);
})
.catch(console.log);
}}
/>
)
)}
</ScrollView>
</View>
<FlashList
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
ListEmptyComponent={
<View
style={{
height: "100%",
justifyContent: "center",
alignItems: "center"
}}
>
{loading ? (
<ActivityIndicator size={40} color={colors.primary.accent} />
) : (
<>
<Icon
name="attachment"
size={60}
color={colors.secondary.icon}
/>
<Paragraph>{strings.noAttachments()}</Paragraph>
</>
)}
</View>
}
ListHeaderComponent={<AllProgress note={note} />}
ListFooterComponent={
<View
style={{
height: 350
}}
/>
}
estimatedItemSize={50}
data={loading ? [] : attachments?.placeholders}
renderItem={renderItem}
/>
</View>
</>
);
};
const AllProgress = ({ note }: { note?: Note }) => {
const progress = useAttachmentStore((state) =>
note
? {
downloading: {
[note.id]: state.downloading?.[note.id]
},
uploading: {
[note.id]: state.uploading?.[note.id]
}
<Icon name="attachment" size={60} color={colors.secondary.icon} />
<Paragraph>
{note ? "No attachments on this note" : "No attachments"}
</Paragraph>
</View>
}
: state
);
ListFooterComponent={
<View
style={{
height: 350
}}
/>
}
estimatedItemSize={50}
data={attachments?.placeholders}
renderItem={renderItem}
/>
return (
<View
style={{
gap: 10,
width: "100%"
}}
>
{Object.keys(progress.downloading || {}).map((groupId) => (
<AttachmentGroupProgress key={groupId} groupId={groupId} />
))}
{Object.keys(progress.uploading || {}).map((groupId) => (
<AttachmentGroupProgress key={groupId} groupId={groupId} />
))}
<Paragraph
color={colors.secondary.paragraph}
size={SIZE.xs}
style={{
textAlign: "center",
marginTop: 10
}}
>
<Icon
name="shield-key-outline"
size={SIZE.xs}
color={colors.primary.icon}
/>
{" "}All attachments are end-to-end encrypted.
</Paragraph>
</View>
);
};
AttachmentDialog.present = (note?: Note) => {
presentSheet({
component: () => <AttachmentDialog note={note} isSheet={true} />,
component: () => <AttachmentDialog note={note} />,
keyboardHandlerDisabled: true
});
};

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"
});
@@ -69,13 +68,12 @@ export const ChangePassword = () => {
setLoading(true);
try {
const result = await BackupService.run(false, "change-password-dialog");
if (!result.error)
throw new Error(strings.backupFailed() + `: ${result.error}`);
if (!result) throw new Error("Failed to create backup");
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"
});
@@ -86,7 +84,7 @@ export const ChangePassword = () => {
} catch (e) {
setLoading(false);
ToastManager.show({
heading: strings.passwordChangeFailed(),
heading: "Failed to change password",
message: e.message,
type: "error",
context: "local"
@@ -103,7 +101,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
@@ -117,7 +118,7 @@ export const ChangePassword = () => {
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder={strings.oldPassword()}
placeholder="Old Password"
/>
<Input
@@ -126,21 +127,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={{
@@ -150,7 +157,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

@@ -35,8 +35,6 @@ import Heading from "../ui/typography/heading";
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 +53,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"
});
@@ -79,7 +77,6 @@ export const Signup = ({ changeMode, trial }) => {
clearMessage();
setEmailVerifyMessage();
hideAuth();
SettingsService.setProperty("encryptedBackup", true);
await sleep(300);
if (trial) {
PremiumService.sheet(null, null, true);
@@ -89,7 +86,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 +154,7 @@ export const Signup = ({ changeMode, trial }) => {
}}
size={SIZE.xxl}
>
{strings.createYourAccount()}
Create your {"\n"}account
</Heading>
</View>
@@ -176,14 +173,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 +193,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 +213,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 +221,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 +233,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 +244,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 +257,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 +289,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();
}, 1);
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

@@ -23,7 +23,6 @@ import { AnnouncementDialog } from "../announcements";
import AuthModal from "../auth/auth-modal";
import { SessionExpired } from "../auth/session-expired";
import { Dialog } from "../dialog";
import { AppLockPassword } from "../dialogs/applock-password";
import JumpToSectionDialog from "../dialogs/jump-to-section";
import { LoadingDialog } from "../dialogs/loading";
import PDFPreview from "../dialogs/pdf-preview";
@@ -36,7 +35,8 @@ import { Expiring } from "../premium/expiring";
import SheetProvider from "../sheet-provider";
import RateAppSheet from "../sheets/rate-app";
import RecoveryKeySheet from "../sheets/recovery-key";
import Progress from "../dialogs/progress";
import RestoreDataSheet from "../sheets/restore-data";
import { AppLockPassword } from "../dialogs/applock-password";
const DialogProvider = () => {
const { colors } = useThemeColors();
@@ -52,6 +52,7 @@ const DialogProvider = () => {
<RecoveryKeySheet colors={colors} />
<SheetProvider />
<SheetProvider context="sync_progress" />
<RestoreDataSheet />
<ResultDialog />
<VaultDialog colors={colors} />
<RateAppSheet />
@@ -61,7 +62,6 @@ const DialogProvider = () => {
<SessionExpired />
<PDFPreview />
<JumpToSectionDialog />
<Progress />
</>
);
};

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

@@ -32,29 +32,21 @@ const DialogContainer = ({
}: ViewProps & {
width?: any;
height?: any;
noBorder?: boolean;
}) => {
const { colors } = useThemeColors();
return (
<View
{...restProps}
style={[
style,
{
width: width || DDS.isTab ? 500 : "85%",
maxHeight: height || 450,
borderRadius: 10,
backgroundColor: colors.primary.background,
paddingTop: 12
},
restProps?.noBorder
? {}
: {
...getElevationStyle(5),
...getContainerBorder(colors.secondary.background, 0.8, 0.05)
}
]}
style={{
...getElevationStyle(5),
width: width || DDS.isTab ? 500 : "85%",
maxHeight: height || 450,
borderRadius: 10,
backgroundColor: colors.primary.background,
paddingTop: 12,
...getContainerBorder(colors.secondary.background, 0.8, 0.05)
}}
/>
);
};

View File

@@ -25,7 +25,7 @@ type DialogInfo = {
paragraph?: string;
positiveText?: string;
negativeText?: string;
positivePress?: (...args: any[]) => void;
positivePress?: (value: any) => void;
onClose?: () => void;
positiveType?:
| "transparent"
@@ -45,15 +45,6 @@ type DialogInfo = {
context: "global" | "local" | (string & {});
secureTextEntry?: boolean;
keyboardType?: string;
check?: {
info: string;
type?: string;
defaultValue?: boolean;
};
notice: {
text: string;
type: "alert" | "information";
};
};
export function presentDialog(data: Partial<DialogInfo>): void {

View File

@@ -37,8 +37,6 @@ import DialogHeader from "./dialog-header";
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 +49,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",
@@ -64,8 +62,7 @@ export const Dialog = ({ context = "global" }) => {
disableBackdropClosing: false,
check: {
info: "Check",
type: "transparent",
defaultValue: false
type: "transparent"
}
});
@@ -89,7 +86,7 @@ export const Dialog = ({ context = "global" }) => {
if (!data.context) data.context = "global";
if (data.context !== context) return;
setDialogInfo(data);
setChecked(data.check?.defaultValue);
setChecked(false);
values.current.inputValue = data.defaultValue;
setVisible(true);
},
@@ -183,19 +180,6 @@ export const Dialog = ({ context = "global" }) => {
</View>
) : null}
{dialogInfo?.notice ? (
<View
style={{
paddingHorizontal: 12
}}
>
<Notice
type={dialogInfo.notice.type || "information"}
text={dialogInfo.notice.text}
/>
</View>
) : null}
{dialogInfo.check ? (
<>
<Button
@@ -214,7 +198,7 @@ export const Dialog = ({ context = "global" }) => {
iconSize={20}
width="100%"
title={dialogInfo.check.info}
type={checked ? dialogInfo.check.type || "plain" : "plain"}
type={checked ? dialogInfo.check.type || "selected" : "plain"}
/>
</>
) : null}

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,31 +148,31 @@ 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,

View File

@@ -44,8 +44,6 @@ import { ProgressBarComponent } from "../../ui/svg/lazy";
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;
@@ -126,7 +124,6 @@ const PDFPreview = () => {
);
const close = () => {
deleteCacheFileByPath(pdfSource);
setPDFSource(null);
setVisible(false);
setPassword("");
@@ -138,10 +135,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 +185,8 @@ const PDFPreview = () => {
}}
color={colors.static.white}
>
{strings.loadingWithProgress(progress?.percent)}
Loading {`${progress?.percent ? `(${progress?.percent})` : ""}`}
... Please wait
</Paragraph>
</Animated.View>
) : (

View File

@@ -1,198 +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 { useThemeColors } from "@notesnook/theme";
import React, { useEffect } from "react";
import { View } from "react-native";
import { eSendEvent, eSubscribeEvent } from "../../../services/event-manager";
import { SIZE } from "../../../utils/size";
import { Dialog } from "../../dialog";
import BaseDialog from "../../dialog/base-dialog";
import DialogContainer from "../../dialog/dialog-container";
import { Button } from "../../ui/button";
import { ProgressBarComponent } from "../../ui/svg/lazy";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
export type ProgressOptions = {
progress?: string;
cancelCallback?: () => void;
title?: string;
paragraph?: string;
fillBackground?: boolean;
canHideProgress?: boolean;
};
export const PROGRESS_EVENTS = {
start: "startProgress",
end: "endProgress",
update: "updateProgress"
};
export default function Progress() {
const { colors } = useThemeColors();
const [progress, setProgress] = React.useState<string | undefined>();
const [visible, setVisible] = React.useState(false);
const cancelCallback = React.useRef<() => void>();
const [data, setData] = React.useState<{
title?: string;
paragraph?: string;
fillBackground?: boolean;
canHideProgress?: boolean;
}>();
useEffect(() => {
const events = [
eSubscribeEvent(PROGRESS_EVENTS.start, (options: ProgressOptions) => {
setProgress(options.progress);
cancelCallback.current = options.cancelCallback;
console.log("options", options.fillBackground);
setData({
title: options.title,
paragraph: options.paragraph,
fillBackground: options.fillBackground,
canHideProgress: options.canHideProgress
});
setVisible(true);
}),
eSubscribeEvent(PROGRESS_EVENTS.end, () => {
setProgress(undefined);
setVisible(false);
setData(undefined);
cancelCallback.current?.();
cancelCallback.current = undefined;
}),
eSubscribeEvent(PROGRESS_EVENTS.update, (options: ProgressOptions) => {
setProgress(options.progress);
if (options.cancelCallback) {
cancelCallback.current = options.cancelCallback;
}
const data: ProgressOptions = {};
if (options.title) data.title = options.title;
if (options.paragraph) data.paragraph = options.paragraph;
if (options.fillBackground)
data.fillBackground = options.fillBackground;
setData((current) => {
return {
...current,
...data
};
});
})
];
return () => {
events.forEach((event) => event?.unsubscribe());
cancelCallback.current = undefined;
};
}, []);
return !visible ? null : (
<BaseDialog
background={data?.fillBackground ? colors.primary.background : undefined}
visible
>
<DialogContainer
style={{
paddingHorizontal: 12,
paddingBottom: 10
}}
noBorder={data?.fillBackground ? true : false}
>
<Dialog context="local" />
<View
style={{
justifyContent: "center",
alignItems: "center",
paddingHorizontal: 12,
gap: 10,
paddingBottom: 20
}}
>
<Heading
style={{
textAlign: "center"
}}
color={colors.primary.paragraph}
size={SIZE.lg}
>
{data?.title}
</Heading>
<Paragraph
style={{
textAlign: "center"
}}
color={colors.secondary.paragraph}
>
{progress ? progress : data?.paragraph}
</Paragraph>
<View
style={{
flexDirection: "row",
width: 100,
paddingVertical: 12
}}
>
<ProgressBarComponent
height={5}
width={100}
animated={true}
useNativeDriver
indeterminate
indeterminateAnimationDuration={2000}
unfilledColor={colors.secondary.background}
color={colors.primary.accent}
borderWidth={0}
/>
</View>
{!data?.canHideProgress ? null : (
<Button
title={cancelCallback.current ? "Cancel" : "Hide"}
type="secondaryAccented"
onPress={() => {
if (cancelCallback.current) {
cancelCallback.current?.();
}
setVisible(false);
setProgress(undefined);
setData(undefined);
}}
width="100%"
/>
)}
</View>
</DialogContainer>
</BaseDialog>
);
}
export function startProgress(options: ProgressOptions) {
eSendEvent(PROGRESS_EVENTS.start, options);
}
export function endProgress() {
eSendEvent(PROGRESS_EVENTS.end);
}
export function updateProgress(options: ProgressOptions) {
eSendEvent(PROGRESS_EVENTS.update, options);
}

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"
});
@@ -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;
@@ -66,6 +67,8 @@ export const Header = ({
hasSearch?: boolean;
onSearch?: () => void;
}) => {
const navigation = useNavigation();
const { colors } = useThemeColors();
const insets = useGlobalSafeAreaInsets();
const [borderHidden, setBorderHidden] = useState(true);

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,
@@ -95,17 +96,14 @@ export const RightMenus = ({
animationDuration={200}
style={{
borderRadius: 5,
backgroundColor: contextMenuColors.primary.background,
marginTop: 35
backgroundColor: contextMenuColors.primary.background
}}
onRequestClose={() => {
//@ts-ignore
menuRef.current?.hide();
}}
anchor={
<IconButton
onPress={() => {
//@ts-ignore
menuRef.current?.show();
}}
name="dots-vertical"
@@ -117,10 +115,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 +126,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

@@ -18,8 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Sodium from "@ammarahmed/react-native-sodium";
import { DataURL } from "@notesnook/core";
import type { ImageAttributes } from "@notesnook/editor";
import dataurl from "@notesnook/core/dist/utils/dataurl";
import type { ImageAttributes } from "@notesnook/editor/dist/extensions/image/index";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import { Platform, View } from "react-native";
@@ -55,8 +55,8 @@ const ImagePreview = () => {
setLoading(true);
setTimeout(async () => {
let hash = image.hash;
if (!hash && image.src && DataURL.toObject(image.src)) {
const data = DataURL.toObject(image.src);
if (!hash && image.src && dataurl.toObject(image.src)) {
const data = dataurl.toObject(image.src);
if (!data) return;
hash = await Sodium.hashFile({
data: data.data,

View File

@@ -17,26 +17,35 @@ 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();
const isTelemetryEnabled = useSettingStore(
(state) => state.settings.telemetry
);
const { width, height } = useWindowDimensions();
const deviceMode = useSettingStore((state) => state.deviceMode);
const insets = useGlobalSafeAreaInsets();
@@ -83,20 +92,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 +115,7 @@ const Intro = ({ navigation }) => {
Linking.openURL(item.link);
}}
>
{item.tesimonial()} {item.user}
{item.tesimonial} {item.user}
</Paragraph>
) : null}
</View>
@@ -148,7 +155,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,
@@ -189,8 +215,46 @@ const Intro = ({ navigation }) => {
}}
fontSize={SIZE.md}
type="accent"
title={strings.getStarted()}
title="Get started"
/>
<TouchableOpacity
activeOpacity={1}
style={{
flexDirection: "row",
alignSelf: "center",
width: "90%",
marginBottom: 12,
paddingHorizontal: 12,
justifyContent: "center",
padding: 12,
maxWidth: 500
}}
onPress={() => {
SettingsService.set({ telemetry: !isTelemetryEnabled });
}}
>
<Icon
size={SIZE.md}
name={
isTelemetryEnabled ? "checkbox-marked" : "checkbox-blank-outline"
}
color={
isTelemetryEnabled ? colors.primary.accent : colors.primary.icon
}
/>
<Paragraph
style={{
flexShrink: 1,
marginLeft: 6
}}
size={SIZE.xs}
>
Help improve Notesnook by sending completely anonymized{" "}
<Heading size={SIZE.xs}>private analytics and bug reports.</Heading>
</Paragraph>
</TouchableOpacity>
</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,14 +57,17 @@ export const SectionHeader = React.memo<
}: SectionHeaderProps) {
const { colors } = useThemeColors();
const { fontScale } = useWindowDimensions();
const groupBy =
strings.groupByStrings[
groupOptions.groupBy as keyof typeof strings.groupByStrings
]?.();
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={{
@@ -100,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>
@@ -114,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"
@@ -151,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/dist/utils/grouping";
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;

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

@@ -167,7 +167,7 @@ export const Component = ({ close, promo }) => {
>
(
{Platform.OS === "android"
? pricing.product?.subscriptionOfferDetails[0]?.pricingPhases
? pricing.product?.subscriptionOfferDetails[0].pricingPhases
.pricingPhaseList?.[0].formattedPrice
: pricing.product?.localizedPrice}{" "}
/ mo)

View File

@@ -85,7 +85,7 @@ export const PricingItem = ({
>
{Platform.OS === "android"
? (product.data as RNIap.SubscriptionAndroid | undefined)
?.subscriptionOfferDetails?.[0]?.pricingPhases
?.subscriptionOfferDetails[0].pricingPhases
.pricingPhaseList?.[0]?.formattedPrice
: (product.data as RNIap.SubscriptionIOS | undefined)
?.localizedPrice}

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";
@@ -394,7 +393,7 @@ export const PricingPlans = ({
>
{Platform.OS === "android"
? (product.data as RNIap.SubscriptionAndroid)
?.subscriptionOfferDetails[0]?.pricingPhases
?.subscriptionOfferDetails[0].pricingPhases
.pricingPhaseList?.[0]?.formattedPrice
: (product.data as RNIap.SubscriptionIOS)
?.introductoryPrice ||
@@ -517,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

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Color, Note } from "@notesnook/core";
import { Color, Note } from "@notesnook/core/dist/types";
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useState } from "react";
import { View } from "react-native";
@@ -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>
);
}

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