Compare commits

..

2 Commits

Author SHA1 Message Date
Abdullah Atta
5962f059ce editor: remove table view 2025-11-27 12:03:43 +05:00
01zulfi
4991bdf689 editor: readonly table fixes
* fix tables not hiding under collapsed headings in readonly view
* allow column resizing in readonly view
* fix scrollbar not showing in readonly view

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-11-27 11:56:52 +05:00
334 changed files with 7251 additions and 15751 deletions

View File

@@ -55,13 +55,13 @@ jobs:
- name: Detox build
run: |
yarn build:android
ls apps/mobile/android/app/build/outputs/apk
ls apps/mobile/android/app/build/outputs/apk/release
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/.detoxrc.js').devices.emulator.device.avdName")
AVD_NAME=$(node -p "require('./apps/mobile/native/.detoxrc.js').devices.emulator.device.avdName")
echo "AVD_NAME=$AVD_NAME" >> $GITHUB_OUTPUT
- name: AVD cache

View File

@@ -113,7 +113,7 @@ jobs:
keyStorePassword: ${{ secrets.KEY_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
env:
BUILD_TOOLS_VERSION: "36.0.0"
BUILD_TOOLS_VERSION: "33.0.0"
- name: Build apks for Github release
run: yarn release:android
@@ -128,7 +128,7 @@ jobs:
keyStorePassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
keyPassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
env:
BUILD_TOOLS_VERSION: "36.0.0"
BUILD_TOOLS_VERSION: "33.0.0"
- name: Rename apk files
run: |

View File

@@ -24,22 +24,6 @@ jobs:
java-version: "17"
architecture: "x64"
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: false
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: false
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
- name: Install node modules
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
@@ -113,7 +97,7 @@ jobs:
keyStorePassword: ${{ secrets.KEY_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
env:
BUILD_TOOLS_VERSION: "36.0.0"
BUILD_TOOLS_VERSION: "33.0.0"
- name: Build apks for Github release
run: yarn release:android
@@ -128,7 +112,7 @@ jobs:
keyStorePassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
keyPassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
env:
BUILD_TOOLS_VERSION: "36.0.0"
BUILD_TOOLS_VERSION: "33.0.0"
- name: Rename apk files
run: |
@@ -140,7 +124,7 @@ jobs:
- name: Get app version
id: package-version
uses: saionaro/extract-package-version@master
uses: martinbeentjes/npm-get-version-action@master
with:
path: apps/mobile
@@ -159,8 +143,8 @@ jobs:
uses: softprops/action-gh-release@v1
with:
draft: true
tag_name: ${{ steps.package-version.outputs.version}}-android
name: Notesnook Android v${{ steps.package-version.outputs.version}}
tag_name: ${{ steps.package-version.outputs.current-version}}-android
name: Notesnook Android v${{ steps.package-version.outputs.current-version}}
repository: streetwriters/notesnook
token: ${{ secrets.GITHUB_TOKEN }}
files: |

View File

@@ -45,7 +45,7 @@ jobs:
test-macos-x64:
name: Test macOS x64
needs: build
runs-on: macos-15-intel
runs-on: macos-13
steps:
- name: Check out Git repository

View File

@@ -4,7 +4,7 @@ on: workflow_dispatch
jobs:
build:
runs-on: macos-26
runs-on: macos-14
timeout-minutes: 60
steps:
@@ -17,7 +17,7 @@ jobs:
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: "26.1.1"
xcode-version: "16.1"
- name: Install node modules
run: |
@@ -30,7 +30,7 @@ jobs:
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2.11
with:
key: ${{ runner.os }}-ccache-${{ hashFiles(format('apps/mobile/ios/Podfile.lock')) }}
key: ${{ runner.os }}-ccache-${{ hashFiles(format('apps/mobile/native/ios/Podfile.lock')) }}
max-size: 1500M
restore-keys: |
${{ runner.os }}-ccache-
@@ -55,13 +55,14 @@ jobs:
uses: actions/cache@v3
id: pods-cache
with:
path: apps/mobile/ios/Pods
key: ${{ runner.os }}-pods-${{ hashFiles('apps/mobile/ios/Podfile.lock') }}
path: apps/mobile/native/ios/Pods
key: ${{ runner.os }}-pods-${{ hashFiles('apps/mobile/native/ios/Podfile.lock') }}
- name: Install Pods
run: |
cd apps/mobile/ios
pod install
cd apps/mobile/native/ios
bundle install
RCT_NEW_ARCH_ENABLED=0 bundle exec pod install
- name: Check for typescript errors
run: |
@@ -78,9 +79,9 @@ jobs:
bundle-identifier: org.streetwriters.notesnook
scheme: Notesnook
configuration: "Release"
export-options: apps/mobile/ios/ExportOptions.plist
project-path: apps/mobile/ios/Notesnook.xcodeproj
workspace-path: apps/mobile/ios/Notesnook.xcworkspace
export-options: apps/mobile/native/ios/ExportOptions.plist
project-path: apps/mobile/native/ios/Notesnook.xcodeproj
workspace-path: apps/mobile/native/ios/Notesnook.xcworkspace
update-targets: |
Notesnook
Make Note
@@ -116,5 +117,5 @@ jobs:
name: Notesnook.zip
path: |
Notesnook.ipa
apps/mobile/ios/**/*.map
apps/mobile/native/ios/**/*.map
packages/editor-mobile/sourcemaps/*.map

View File

@@ -1,78 +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/themes-server
on: workflow_dispatch
jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-22.04
permissions:
packages: write
contents: read
attestations: write
id-token: write
steps:
- name: Check out the repo
uses: actions/checkout@v4
- name: Collect package metadata
id: package_metadata
working-directory: ./servers/themes
run: |
echo ::set-output name=app_version::$(cat package.json | jq -r .version)
# Setup Buildx
- name: Docker Setup Buildx
uses: docker/setup-buildx-action@v3
with:
platforms: linux/amd64,linux/arm64
- 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/themes-server: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/themes-server
- name: Build and push Docker image
id: push
uses: docker/build-push-action@v6
with:
context: .
file: servers/themes/Dockerfile
push: true
platforms: linux/amd64,linux/arm64
tags: streetwriters/themes-server:${{ steps.package_metadata.outputs.app_version }},streetwriters/themes-server:latest
cache-from: streetwriters/themes-server:latest
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v1
with:
subject-name: index.docker.io/streetwriters/themes-server
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true

View File

@@ -11,5 +11,5 @@
"cache": true,
"cacheStrategy": "content"
},
"git.rebaseWhenSync": false
"git.rebaseWhenSync": true
}

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/desktop",
"version": "3.3.8",
"version": "3.3.6-beta.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/desktop",
"version": "3.3.8",
"version": "3.3.6-beta.0",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
@@ -19,8 +19,8 @@
"electron-trpc": "0.7.1",
"electron-updater": "^6.6.2",
"icojs": "^0.19.5",
"sqlite-better-trigram": "0.0.5",
"sqlite3-fts5-html": "^0.0.6",
"sqlite-better-trigram": "0.0.3",
"sqlite3-fts5-html": "^0.0.4",
"typed-emitter": "^2.1.0",
"yargs": "^17.7.2",
"zod": "3.24.3"
@@ -6316,23 +6316,23 @@
"optional": true
},
"node_modules/sqlite-better-trigram": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/sqlite-better-trigram/-/sqlite-better-trigram-0.0.5.tgz",
"integrity": "sha512-LDAuMzU2u9rhJyNmSxZyJiL3lWb8FjUqX/JZ3dwmo16Cl0zmkzFvGS94yfjLhwEce1yg1UE851Mz5UplWNhhpg==",
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/sqlite-better-trigram/-/sqlite-better-trigram-0.0.3.tgz",
"integrity": "sha512-r+SDeNk+K1VotrUx/w0TMu4FdjoDDPCqxhtfU50dSeTzfCFtkMBq1/E3gvsaiHi3UYtOa6siA8CK/gYlj75eCA==",
"license": "Public Domain",
"optionalDependencies": {
"sqlite-better-trigram-darwin-arm64": "0.0.5",
"sqlite-better-trigram-darwin-x64": "0.0.5",
"sqlite-better-trigram-linux-arm64": "0.0.5",
"sqlite-better-trigram-linux-x64": "0.0.5",
"sqlite-better-trigram-windows-arm64": "0.0.5",
"sqlite-better-trigram-windows-x64": "0.0.5"
"sqlite-better-trigram-darwin-arm64": "0.0.3",
"sqlite-better-trigram-darwin-x64": "0.0.3",
"sqlite-better-trigram-linux-arm64": "0.0.3",
"sqlite-better-trigram-linux-x64": "0.0.3",
"sqlite-better-trigram-windows-arm64": "0.0.3",
"sqlite-better-trigram-windows-x64": "0.0.3"
}
},
"node_modules/sqlite-better-trigram-darwin-arm64": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/sqlite-better-trigram-darwin-arm64/-/sqlite-better-trigram-darwin-arm64-0.0.5.tgz",
"integrity": "sha512-AVSfFHLDAuttkKtlXCMHbhtV6Dv8M48e4ucTfjMOpGenJTEWmXx3Goj/ZCHe3uLEW57qaely15AAl/xFOsGSLA==",
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/sqlite-better-trigram-darwin-arm64/-/sqlite-better-trigram-darwin-arm64-0.0.3.tgz",
"integrity": "sha512-ddL0K3v10bvqjuUgPSRfP1qFZAhO/43ruaWtWS5wu4DgikldGz1tBY+nQtdc+Xt0arrEMUyiEvOz7MuKHQNA0Q==",
"cpu": [
"arm64"
],
@@ -6343,9 +6343,9 @@
]
},
"node_modules/sqlite-better-trigram-darwin-x64": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/sqlite-better-trigram-darwin-x64/-/sqlite-better-trigram-darwin-x64-0.0.5.tgz",
"integrity": "sha512-SxrQxI8r42+WN8DZxKuTQtSPmY7TLwtk5VoKbRN6HuNkb4rx8ZmmKeVmVA8u+hLkO4bzh2GtydOrlTdIgg5g5Q==",
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/sqlite-better-trigram-darwin-x64/-/sqlite-better-trigram-darwin-x64-0.0.3.tgz",
"integrity": "sha512-IQzFzXt0ib1MZ/UQm71X7RKv8G0vwepNqcmncWbK1R7LAWwpS/qaJ1NyrMOhL2AnfeQ33x1IkQE0GlamxFVC9A==",
"cpu": [
"x64"
],
@@ -6356,9 +6356,9 @@
]
},
"node_modules/sqlite-better-trigram-linux-arm64": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/sqlite-better-trigram-linux-arm64/-/sqlite-better-trigram-linux-arm64-0.0.5.tgz",
"integrity": "sha512-jrm5jDUfozFEU3ErN2NosQHTi53XouXBrrhzoigAYWYhfk+oeMbN7MWdzv5y5eZNH5uUSRc8/Kj6XO8rx147mw==",
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/sqlite-better-trigram-linux-arm64/-/sqlite-better-trigram-linux-arm64-0.0.3.tgz",
"integrity": "sha512-NTykERCSxAutAtJ9ITwOQBitC9zDg61tTDoLOGdMoDO2iAQNLuGApNS8qFvnCNl1wZy8yowI24rHYMCWLDIEIA==",
"cpu": [
"arm64"
],
@@ -6369,9 +6369,9 @@
]
},
"node_modules/sqlite-better-trigram-linux-x64": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/sqlite-better-trigram-linux-x64/-/sqlite-better-trigram-linux-x64-0.0.5.tgz",
"integrity": "sha512-AqiEOF7fK8MPcJKxWkkrF9DWSs5Ma/AlJfuWDrdxKzlLPaV4OWucL2zm/ShdEMGZ+Uz5EgTShGxnvLRRMmZYsQ==",
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/sqlite-better-trigram-linux-x64/-/sqlite-better-trigram-linux-x64-0.0.3.tgz",
"integrity": "sha512-WoB2qMeFX6XGOWdTYCrF4imEBL5/a/UKqZpzWvPhtV92Ys47z4AIuwiWvekO8/5FRLYCyT9rJLXLSY2eUwOqLg==",
"cpu": [
"x64"
],
@@ -6382,9 +6382,9 @@
]
},
"node_modules/sqlite-better-trigram-windows-arm64": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/sqlite-better-trigram-windows-arm64/-/sqlite-better-trigram-windows-arm64-0.0.5.tgz",
"integrity": "sha512-HEY3GbhtINKyM2ztcBJ7GqpgHNDA0aBUFNnKsvAkorF08+0v0sRs3oZsoDBqDQY0tm90iUZVLoTqvtN3sLhaGw==",
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/sqlite-better-trigram-windows-arm64/-/sqlite-better-trigram-windows-arm64-0.0.3.tgz",
"integrity": "sha512-hXIT+PkkJQ3vJ0+E8Zh2/BoOCQ2dRj6LmchjP7T5afpbKXI/lcI77Cgk3tPwe8s/Ywci6OyeJwcaFuBtf1CLhw==",
"cpu": [
"arm64"
],
@@ -6395,9 +6395,9 @@
]
},
"node_modules/sqlite-better-trigram-windows-x64": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/sqlite-better-trigram-windows-x64/-/sqlite-better-trigram-windows-x64-0.0.5.tgz",
"integrity": "sha512-JeLPlGc5yL7KL/uqAgcRb9wwS51xN1aezP18k2dvuxpXWmfGDc8e8/bpKYVVzUbnMF63MdpSa9PBch2NhT6CPA==",
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/sqlite-better-trigram-windows-x64/-/sqlite-better-trigram-windows-x64-0.0.3.tgz",
"integrity": "sha512-dli7MaDld9k02Vx74Mrovxxy+SQ72GwaObvIOcbI7ATiKUvhRvjHZgmuIvgxF2DM8aRejFHgPIC4/GUJcjauEg==",
"cpu": [
"x64"
],
@@ -6408,23 +6408,23 @@
]
},
"node_modules/sqlite3-fts5-html": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/sqlite3-fts5-html/-/sqlite3-fts5-html-0.0.6.tgz",
"integrity": "sha512-+15ntK7L68G/yuM+vyKAFxzLM/TRwjNoWAkFr0q+jXcdwfTUOrU2painloOIWnqN/meT0N1kdOltaHgyGS9MaA==",
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/sqlite3-fts5-html/-/sqlite3-fts5-html-0.0.4.tgz",
"integrity": "sha512-2d7G3QAgNtndTYRFDXVzzsjlDkjXLo9YB4xxVFIxHIotKnVTNmd0/iSQMMjOMFY5XEpW8yemFkD0EyuN0wFuSQ==",
"license": "Public Domain",
"optionalDependencies": {
"sqlite3-fts5-html-darwin-arm64": "0.0.6",
"sqlite3-fts5-html-darwin-x64": "0.0.6",
"sqlite3-fts5-html-linux-arm64": "0.0.6",
"sqlite3-fts5-html-linux-x64": "0.0.6",
"sqlite3-fts5-html-windows-arm64": "0.0.6",
"sqlite3-fts5-html-windows-x64": "0.0.6"
"sqlite3-fts5-html-darwin-arm64": "0.0.4",
"sqlite3-fts5-html-darwin-x64": "0.0.4",
"sqlite3-fts5-html-linux-arm64": "0.0.4",
"sqlite3-fts5-html-linux-x64": "0.0.4",
"sqlite3-fts5-html-windows-arm64": "0.0.4",
"sqlite3-fts5-html-windows-x64": "0.0.4"
}
},
"node_modules/sqlite3-fts5-html-darwin-arm64": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/sqlite3-fts5-html-darwin-arm64/-/sqlite3-fts5-html-darwin-arm64-0.0.6.tgz",
"integrity": "sha512-qMSUJs9py8X5VK96YuiRrORCSxINoIaK/lYOQt7dGwOGnfP8qpdvj5FoQMkNnb5s2kd2xqko1o5io3iJFq2sZA==",
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/sqlite3-fts5-html-darwin-arm64/-/sqlite3-fts5-html-darwin-arm64-0.0.4.tgz",
"integrity": "sha512-zkg+LxOtq7ONz82C4+1mxsmasOIt9tbUReEMlmu29pJosfgDsMftp9k1Jy46FEk3VDFODlg9uDX7f0O0rspayg==",
"cpu": [
"arm64"
],
@@ -6435,9 +6435,9 @@
]
},
"node_modules/sqlite3-fts5-html-darwin-x64": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/sqlite3-fts5-html-darwin-x64/-/sqlite3-fts5-html-darwin-x64-0.0.6.tgz",
"integrity": "sha512-T7ouo1HZffrfuVKy0GkPR6dJvGeiaaWlqCDrQdNvpL7F6DUyaZIGd9FJSwxBjTWaOTOW7glg+sedBGntokpZqw==",
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/sqlite3-fts5-html-darwin-x64/-/sqlite3-fts5-html-darwin-x64-0.0.4.tgz",
"integrity": "sha512-LtBRTVfB3eNkupKJEobZVOyf4TbmWw4XgdBGvi4wFHHcv96gqhVlHdUGlavul561AOpab+T7DOgaPgph1y5M5A==",
"cpu": [
"x64"
],
@@ -6448,9 +6448,9 @@
]
},
"node_modules/sqlite3-fts5-html-linux-arm64": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/sqlite3-fts5-html-linux-arm64/-/sqlite3-fts5-html-linux-arm64-0.0.6.tgz",
"integrity": "sha512-Xl4pokUU4OsyroHYAoCFh7R03s6V2ApcQZImmNsVpWlAviVhlAQ3NxSBhXtJ2HNyyVJjnkJZvQnXG0zRBLZDXw==",
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/sqlite3-fts5-html-linux-arm64/-/sqlite3-fts5-html-linux-arm64-0.0.4.tgz",
"integrity": "sha512-+xgRPpD7fdXNJPKaggHE5BkSF00aI8rlHHZ+pXZR2GmFyRr++jt34MA5avO4G6oI4mrZ82wUV+sWUqd8FErRBw==",
"cpu": [
"arm64"
],
@@ -6461,9 +6461,9 @@
]
},
"node_modules/sqlite3-fts5-html-linux-x64": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/sqlite3-fts5-html-linux-x64/-/sqlite3-fts5-html-linux-x64-0.0.6.tgz",
"integrity": "sha512-v+h7nP4BliFcqVfbWE7aCN9MUqyqzPNOzl/8eautN5dZCHEvQErhVgIzZfCFDnJf0Xp73eXJLu5mnyJYFf3x3A==",
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/sqlite3-fts5-html-linux-x64/-/sqlite3-fts5-html-linux-x64-0.0.4.tgz",
"integrity": "sha512-4ah7IiDUH9kpCPoqT4lW6FcWcNiVF0wtpywW82UVfkawbmgLFPGqSQFqHyChmp1b2dsrkO5X/bhq9E2BR0/xmQ==",
"cpu": [
"x64"
],
@@ -6474,9 +6474,9 @@
]
},
"node_modules/sqlite3-fts5-html-windows-arm64": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/sqlite3-fts5-html-windows-arm64/-/sqlite3-fts5-html-windows-arm64-0.0.6.tgz",
"integrity": "sha512-460bqs+vA4I0GTyviFPD53I4ivPjrhJNr3tDhWEmd2p+aNDfxvz1z0BAw4Su1sVxhCe/up1oMDCxC1z7dEyfmA==",
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/sqlite3-fts5-html-windows-arm64/-/sqlite3-fts5-html-windows-arm64-0.0.4.tgz",
"integrity": "sha512-rQKLrX90TKIMgD7zSrOk/U6q0KEBiTaB2nXDONcK4d/CYpnG9g/N/i6Ady98j5+v/PgqymchgtRwvgBHOZzCfw==",
"cpu": [
"arm64"
],
@@ -6487,9 +6487,9 @@
]
},
"node_modules/sqlite3-fts5-html-windows-x64": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/sqlite3-fts5-html-windows-x64/-/sqlite3-fts5-html-windows-x64-0.0.6.tgz",
"integrity": "sha512-rMjO3yNcqvfVeI5cDWcLYut43s9n4vHVvFL129AJIDarFz/r99VXC34kIdPupW6GIlh2v/c3Jj3TtuXlOyulrQ==",
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/sqlite3-fts5-html-windows-x64/-/sqlite3-fts5-html-windows-x64-0.0.4.tgz",
"integrity": "sha512-FxLloBxWtsIGUMxbvOLNY9+FXJYEcku1xVqJarLfGKcXX7zvS3yXMP0dYeld2lQ/evNiLgvLMNLhUFwhEln1vA==",
"cpu": [
"x64"
],

View File

@@ -2,7 +2,7 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "3.3.8",
"version": "3.3.6-beta.0",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/cjs/index.js",
@@ -36,8 +36,8 @@
"electron-trpc": "0.7.1",
"electron-updater": "^6.6.2",
"icojs": "^0.19.5",
"sqlite-better-trigram": "0.0.5",
"sqlite3-fts5-html": "^0.0.6",
"sqlite-better-trigram": "0.0.3",
"sqlite3-fts5-html": "^0.0.4",
"typed-emitter": "^2.1.0",
"yargs": "^17.7.2",
"zod": "3.24.3"

View File

@@ -39,7 +39,7 @@ const LINUX_AUTOSTART_DIRECTORY_PATH = path.join(
"autostart"
);
const HIDDEN_ARG = "--hidden";
const STARTUP_ARGS = ["--hidden"];
export class AutoLaunch {
static enable(hidden: boolean) {
@@ -54,13 +54,13 @@ export class AutoLaunch {
);
} else {
const loginItemSettings = app.getLoginItemSettings({
args: hidden ? [HIDDEN_ARG] : undefined
args: STARTUP_ARGS
});
if (loginItemSettings.openAtLogin) return;
app.setLoginItemSettings({
openAtLogin: true,
openAsHidden: hidden,
args: hidden ? [HIDDEN_ARG] : undefined
args: STARTUP_ARGS
});
}
}

View File

@@ -47,7 +47,7 @@ module.exports = {
simulator: {
type: "ios.simulator",
device: {
type: "iPhone 17 Pro Max"
type: "iPhone 12"
}
},
attached: {
@@ -59,7 +59,7 @@ module.exports = {
emulator: {
type: "android.emulator",
device: {
avdName: "Pixel_5_API_36"
avdName: "Pixel_5_API_34"
}
}
},

View File

@@ -1,7 +1,6 @@
apply plugin: "com.android.application"
apply plugin: "org.jetbrains.kotlin.android"
apply plugin: "com.facebook.react"
apply plugin: 'kotlin-parcelize'
apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle"
@@ -125,7 +124,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled true
versionCode 3091
versionCode 3079
versionName getNpmVersion()
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'

View File

@@ -63,7 +63,4 @@
-keepattributes Signature
-keep class com.google.gson.reflect.TypeToken { *; }
-keep class * extends com.google.gson.reflect.TypeToken
-keep class com.streetwriters.notesnook.datatypes.* { *; }
-keep class net.gotev.uploadservice.* { *; }
-keep class kotlinx.parcelize.* { *; }
-keep class com.streetwriters.notesnook.datatypes.* { *; }

View File

@@ -20,7 +20,7 @@ import com.zoontek.rnbootsplash.RNBootSplash;
public class MainActivity extends ReactActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
protected void onCreate(Bundle savedInstanceState) {
RNBootSplash.init(this, R.style.BootTheme);
super.onCreate(null);
@@ -32,6 +32,19 @@ public class MainActivity extends ReactActivity {
startService(new Intent(getBaseContext(), OnClearFromRecentService.class));
} catch (Exception ignored) {}
if (Build.VERSION.SDK_INT >= 35) {
final View rootView = findViewById(android.R.id.content);
ViewCompat.setOnApplyWindowInsetsListener(rootView, (v, insets) -> {
Insets innerPadding = insets.getInsets(WindowInsetsCompat.Type.ime());
rootView.setPadding(
innerPadding.left,
innerPadding.top,
innerPadding.right,
innerPadding.bottom
);
return insets;
});
}
}
/**
@@ -47,20 +60,20 @@ public class MainActivity extends ReactActivity {
DefaultNewArchitectureEntryPoint.getFabricEnabled());
}
@Override
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
}
@Override
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
getReactHost().onConfigurationChanged(this);
Intent intent = new Intent("onConfigurationChanged");
intent.putExtra("newConfig", newConfig);
this.sendBroadcast(intent);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
getReactInstanceManager().onConfigurationChanged(this, newConfig);
Intent intent = new Intent("onConfigurationChanged");
intent.putExtra("newConfig", newConfig);
this.sendBroadcast(intent);
}
@Override
protected String getMainComponentName() {

View File

@@ -6,7 +6,7 @@ import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
import org.wonday.orientation.OrientationActivityLifecycle;
class MainApplication : Application(), ReactApplication {
override val reactHost: ReactHost by lazy {
getDefaultReactHost(
@@ -22,7 +22,6 @@ class MainApplication : Application(), ReactApplication {
override fun onCreate() {
super.onCreate()
registerActivityLifecycleCallbacks(OrientationActivityLifecycle.getInstance());
loadReactNative(this)
}
}

View File

@@ -6,14 +6,7 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.drawable.Icon;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
@@ -36,7 +29,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
public class RCTNNativeModule extends ReactContextBaseJavaModule {
@@ -245,221 +237,6 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule {
}
}
@ReactMethod
public void addShortcut(final String id, final String type, final String title, final String description, final String color, Promise promise) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
promise.reject("UNSUPPORTED", "Pinned launcher shortcuts require Android 8.0 or higher");
return;
}
try {
ShortcutManager shortcutManager = mContext.getSystemService(ShortcutManager.class);
if (shortcutManager == null) {
promise.reject("ERROR", "ShortcutManager not available");
return;
}
String uri = "https://app.notesnook.com/open_" + type + "?id=" + id;
Intent intent = new Intent(Intent.ACTION_VIEW, android.net.Uri.parse(uri));
intent.setPackage(mContext.getPackageName());
Icon icon = createLetterIcon(type, title, color);
ShortcutInfo shortcut = new ShortcutInfo.Builder(mContext, id)
.setShortLabel(title)
.setLongLabel(description != null && !description.isEmpty() ? description : title)
.setIcon(icon)
.setCategories(Set.of(type))
.setIntent(intent)
.build();
shortcutManager.requestPinShortcut(shortcut, null);
promise.resolve(true);
} catch (Exception e) {
promise.reject("ERROR", e.getMessage());
}
}
@ReactMethod
public void removeShortcut(final String id, Promise promise) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
promise.reject("UNSUPPORTED", "Pinned launcher shortcuts require Android 8.0 or higher");
return;
}
try {
ShortcutManager shortcutManager = mContext.getSystemService(ShortcutManager.class);
if (shortcutManager == null) {
promise.reject("ERROR", "ShortcutManager not available");
return;
}
shortcutManager.disableShortcuts(java.util.Collections.singletonList(id));
promise.resolve(true);
} catch (Exception e) {
promise.reject("ERROR", e.getMessage());
}
}
@ReactMethod
public void updateShortcut(final String id, final String type, final String title, final String description,final String color, Promise promise) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
promise.reject("UNSUPPORTED", "Pinned launcher shortcuts require Android 8.0 or higher");
return;
}
try {
ShortcutManager shortcutManager = mContext.getSystemService(ShortcutManager.class);
if (shortcutManager == null) {
promise.reject("ERROR", "ShortcutManager not available");
return;
}
// Get existing shortcut to preserve icon and intent
List<ShortcutInfo> shortcuts = shortcutManager.getPinnedShortcuts();
ShortcutInfo existingShortcut = null;
for (ShortcutInfo s : shortcuts) {
if (s.getId().equals(id)) {
existingShortcut = s;
break;
}
}
if (existingShortcut == null) {
return;
}
Icon icon = createLetterIcon(type, title, color);
ShortcutInfo updatedShortcut = new ShortcutInfo.Builder(mContext, id)
.setShortLabel(title)
.setIcon(icon)
.setLongLabel(description != null && !description.isEmpty() ? description : title)
.setIntent(existingShortcut.getIntent())
.build();
shortcutManager.updateShortcuts(java.util.Collections.singletonList(updatedShortcut));
promise.resolve(true);
} catch (Exception e) {
promise.reject("ERROR", e.getMessage());
}
}
@ReactMethod
public void removeAllShortcuts(Promise promise) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
promise.reject("UNSUPPORTED", "Pinned launcher shortcuts require Android 8.0 or higher");
return;
}
try {
ShortcutManager shortcutManager = mContext.getSystemService(ShortcutManager.class);
if (shortcutManager == null) {
promise.reject("ERROR", "ShortcutManager not available");
return;
}
List<ShortcutInfo> pinnedShortcuts = shortcutManager.getPinnedShortcuts();
if (!pinnedShortcuts.isEmpty()) {
List<String> ids = new ArrayList<>();
for (ShortcutInfo shortcut : pinnedShortcuts) {
ids.add(shortcut.getId());
}
shortcutManager.disableShortcuts(ids);
}
promise.resolve(true);
} catch (Exception e) {
promise.reject("ERROR", e.getMessage());
}
}
@ReactMethod
public void getAllShortcuts(Promise promise) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}
ShortcutManager shortcutManager = mContext.getSystemService(ShortcutManager.class);
WritableArray shortcuts = Arguments.createArray();
List<ShortcutInfo> infos = shortcutManager.getPinnedShortcuts();
for (ShortcutInfo info: infos) {
WritableMap data = Arguments.createMap();
data.putString("id", info.getId());
if (info.getShortLabel() != null) {
data.putString("title", info.getShortLabel().toString());
}
if (info.getLongLabel() != null) {
data.putString("description", info.getLongLabel().toString());
}
if (!Objects.requireNonNull(info.getCategories()).isEmpty()) {
if (info.getCategories().contains("note")) {
data.putString("type", "note");
} else if (info.getCategories().contains("notebook")) {
data.putString("type", "notebook");
} else if (info.getCategories().contains("tag")) {
data.putString("type", "tag");
} else if (info.getCategories().contains("color")) {
data.putString("type", "color");
}
}
shortcuts.pushMap(data);
}
promise.resolve(shortcuts);
}
private Icon createLetterIcon(String type, String title, String colorCode) {
String letter = type.contains("tag") ? "#" : title != null && !title.isEmpty()
? title.substring(0, 1).toUpperCase()
: "?";
int color = type.equals("color") ? Color.parseColor(colorCode) : getColorForLetter(letter);
if (type.equals("notebook")) return Icon.createWithResource(mContext, R.drawable.ic_notebook);
// Use a larger canvas and fill it completely to avoid white borders from launcher masking.
int iconSize = 256;
Bitmap bitmap = Bitmap.createBitmap(iconSize, iconSize, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
if (type.equals("color")) {
Paint backgroundPaint = new Paint();
backgroundPaint.setColor(color);
backgroundPaint.setAntiAlias(true);
canvas.drawCircle(iconSize / 2, iconSize / 2, iconSize/2, backgroundPaint);
} else {
Paint textPaint = new Paint();
textPaint.setColor(color);
textPaint.setTextSize(130);
textPaint.setAntiAlias(true);
textPaint.setTextAlign(Paint.Align.CENTER);
textPaint.setTypeface(android.graphics.Typeface.create(android.graphics.Typeface.DEFAULT, android.graphics.Typeface.BOLD));
float x = iconSize / 2f;
float y = (iconSize / 2f) - ((textPaint.descent() + textPaint.ascent()) / 2f);
canvas.drawText(letter, x, y, textPaint);
}
return Icon.createWithBitmap(bitmap);
}
private int getColorForLetter(String letter) {
int[] colors = {
0xFF1976D2, // Blue
0xFFD32F2F, // Red
0xFF388E3C, // Green
0xFFF57C00, // Orange
0xFF7B1FA2, // Purple
0xFF0097A7, // Cyan
0xFFC2185B, // Pink
0xFF455A64, // Blue Grey
0xFF6A1B9A, // Deep Purple
0xFF00796B, // Teal
0xFF512DA8, // Indigo
0xFF1565C0 // Dark Blue
};
int hash = Math.abs(letter.hashCode());
return colors[hash % colors.length];
}
}

View File

@@ -1,11 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="#008837"
android:alpha="1">
<path
android:fillColor="@android:color/white"
android:pathData="M240,880Q207,880 183.5,856.5Q160,833 160,800L160,160Q160,127 183.5,103.5Q207,80 240,80L720,80Q753,80 776.5,103.5Q800,127 800,160L800,800Q800,833 776.5,856.5Q753,880 720,880L240,880ZM240,800L720,800Q720,800 720,800Q720,800 720,800L720,160Q720,160 720,160Q720,160 720,160L640,160L640,440L540,380L440,440L440,160L240,160Q240,160 240,160Q240,160 240,160L240,800Q240,800 240,800Q240,800 240,800ZM240,800Q240,800 240,800Q240,800 240,800L240,160Q240,160 240,160Q240,160 240,160L240,160Q240,160 240,160Q240,160 240,160L240,800Q240,800 240,800Q240,800 240,800ZM440,440L540,380L640,440L640,440L540,380L440,440Z"/>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 284 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 440 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 807 B

View File

@@ -17,7 +17,7 @@
</style>
<!-- BootTheme should inherit from Theme.SplashScreen -->
<style name="BootTheme" parent="Theme.BootSplash.EdgeToEdge">
<style name="BootTheme" parent="Theme.SplashScreen">
<item name="bootSplashBackground">@color/bootsplash_background</item>
<item name="bootSplashLogo">@mipmap/bootsplash_logo_dark</item>
<item name="android:navigationBarColor">@color/bootsplash_background</item>

0
apps/mobile/android/gradlew vendored Executable file → Normal file
View File

View File

@@ -1,13 +1,3 @@
- Add notebooks, tags and colors to Home Screen Shortcuts
- Change day format and use /day in notes
- Add Setting to change default editor line height
- Set a custom title for monographs
- Add webpage title and date clipped to web clips
- Configure Week to start from Sunday or Monday
- Change Note's creation date
- Set expiry date on notes
- Temporarily disable password change and recovery options
- Note history now includes note title
- Minor bug fixes
- Bug fixes and minor improvements
Thank you for using Notesnook!
Thank you for using Notesnook!

View File

@@ -24,7 +24,7 @@ import {
useThemeEngineStore
} from "@notesnook/theme";
import React, { useEffect } from "react";
import { Appearance, I18nManager, Linking, StatusBar } from "react-native";
import { Appearance, I18nManager, StatusBar } from "react-native";
import "react-native-gesture-handler";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
@@ -43,7 +43,7 @@ import { changeSystemBarColors, useThemeStore } from "./stores/use-theme-store";
import { useUserStore } from "./stores/use-user-store";
import RNBootSplash from "react-native-bootsplash";
import AppLocked from "./components/app-lock";
import { useSettingStore } from "./stores/use-setting-store";
I18nManager.allowRTL(false);
I18nManager.forceRTL(false);
I18nManager.swapLeftAndRightInRTL(false);
@@ -51,25 +51,19 @@ const { appLockEnabled, appLockMode } = SettingsService.get();
if (appLockEnabled || appLockMode !== "none") {
useUserStore.getState().lockApp(true);
}
RNBootSplash.hide({
fade: true
});
Linking.getInitialURL().then((url) => {
useSettingStore.setState({
initialUrl: url
});
});
RNBootSplash.hide();
const App = (props: { configureMode: "note-preview" }) => {
useAppEvents();
//@ts-ignore
globalThis["IS_MAIN_APP_RUNNING"] = true;
useEffect(() => {
SettingsService.onFirstLaunch();
changeSystemBarColors();
SettingsService.onFirstLaunch();
setTimeout(async () => {
await Notifications.get();
if (SettingsService.get().notifNotes) {
Notifications.pinQuickNote();
Notifications.pinQuickNote(true);
}
TipManager.init();
}, 100);

View File

@@ -70,7 +70,7 @@ export async function downloadFile(
throw new Error(error);
}
const totalChunks = Math.ceil(size / (chunkSize + ABYTES));
const totalChunks = Math.ceil(size / chunkSize);
const decryptedLength = size - totalChunks * ABYTES;
if (attachment && attachment.size !== decryptedLength) {

View File

@@ -222,21 +222,15 @@ export async function deleteCacheFileByName(name: string) {
}
export async function deleteDCacheFiles() {
try {
await createCacheDir();
const files = await RNFetchBlob.fs.ls(cacheDir);
for (const file of files) {
if (
file.includes("_dcache") ||
file.startsWith("NN_") ||
file.endsWith(".pdf")
) {
await RNFetchBlob.fs.unlink(file).catch(() => {
/* empty */
});
}
await createCacheDir();
const files = await RNFetchBlob.fs.ls(cacheDir);
for (const file of files) {
if (file.includes("_dcache") || file.startsWith("NN_")) {
await RNFetchBlob.fs.unlink(file).catch(() => {
/* empty */
});
}
} catch (e) {}
}
}
export async function getCachePathForFile(filename: string) {

View File

@@ -17,8 +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 { isImage, RequestOptions, hosts } from "@notesnook/core";
import { PermissionsAndroid, Platform } from "react-native";
import { RequestOptions } from "@notesnook/core";
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";
@@ -31,134 +31,6 @@ import {
FileSizeResult,
getUploadedFileSize
} from "./utils";
import Upload from "@ammarahmed/react-native-upload";
import { CloudUploader } from "react-native-nitro-cloud-uploader";
// Upload constants
const CHUNK_SIZE = 10 * 1024 * 1024; // 10 MB
const MINIMUM_MULTIPART_FILE_SIZE = 25 * 1024 * 1024; // 25MB
interface InitiateMultipartResponse {
uploadId: string;
parts: string[];
error?: string;
}
async function initiateMultipartUpload(
filename: string,
fileSize: number,
headers: Record<string, string>
): Promise<InitiateMultipartResponse> {
const totalParts = Math.ceil(fileSize / CHUNK_SIZE);
const url = `${hosts.API_HOST}/s3/multipart?name=${filename}&parts=${totalParts}&uploadId=`;
const response = await fetch(url, { headers });
if (!response.ok) {
throw new Error(
`Failed to initiate multipart upload: ${response.statusText}`
);
}
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
if (!data.uploadId || !data.parts) {
throw new Error("Failed to initiate multipart upload: invalid response.");
}
DatabaseLogger.info(
`Initiated multipart upload for ${filename} with upload ID: ${data.uploadId}`
);
return data;
}
async function multipartUploadFile(
filename: string,
filePath: string,
fileSize: number,
requestOptions: RequestOptions,
cancelToken: { cancel: (reason?: string) => Promise<void> }
): Promise<Response> {
const { headers } = requestOptions;
try {
const uploadData = await initiateMultipartUpload(
filename,
fileSize,
headers
);
const { uploadId, parts } = uploadData;
DatabaseLogger.info(
`Starting upload for ${filename} with ${parts.length} parts`
);
cancelToken.cancel = async () => {
useAttachmentStore.getState().remove(filename);
await CloudUploader.cancelUpload(uploadId);
};
CloudUploader.addListener("upload-progress", (event) => {
useAttachmentStore
.getState()
.setProgress(
event.bytesUploaded || 0,
event.totalBytes || fileSize,
filename,
0,
"upload"
);
DatabaseLogger.info(
`File upload progress: ${filename}, ${event.bytesUploaded}/${
event.totalBytes || fileSize
}, chunk: ${event.chunkIndex}, progress: ${event.progress}`
);
});
// CloudUploader handles chunking and uploading all parts internally
const result = await CloudUploader.startUpload(
filename,
filePath,
parts,
3, // maxParallel
true // showNotification
);
CloudUploader.removeListener("upload-progress");
if (!result.success) {
throw new Error("Failed to upload multipart file");
}
DatabaseLogger.info(
`Multipart upload completed for ${filename} with upload ID: ${uploadId}`
);
const response = await fetch(`${hosts.API_HOST}/s3/multipart`, {
method: "POST",
body: JSON.stringify({
Key: filename,
UploadId: uploadId,
PartETags: result.etags.map((etag, index) => ({
partNumber: index + 1,
etag: etag
}))
}),
headers: { ...headers, "Content-Type": "application/json" }
});
return response;
} catch (error) {
DatabaseLogger.error(error, "Multipart upload failed", { filename });
CloudUploader.removeListener("upload-progress");
useAttachmentStore.getState().remove(filename);
throw error;
}
}
export async function uploadFile(
filename: string,
@@ -192,142 +64,63 @@ export async function uploadFile(
);
}
const fileInfo = await RNFetchBlob.fs.stat(filePath);
const fileSize = (await RNFetchBlob.fs.stat(filePath)).size;
const remoteFileSize = await getUploadedFileSize(filename);
if (remoteFileSize === FileSizeResult.Error) return false;
if (
remoteFileSize > FileSizeResult.Empty &&
remoteFileSize === fileInfo.size
) {
if (remoteFileSize > FileSizeResult.Empty && remoteFileSize === fileSize) {
DatabaseLogger.log(`File ${filename} is already uploaded.`);
return true;
}
let attachmentInfo = await db.attachments.attachment(filename);
DatabaseLogger.info(`Starting upload: ${filename}`);
DatabaseLogger.info(
`Starting upload of ${filename} at path: ${fileInfo.path} ${fileInfo.size}`
);
if (Platform.OS === "android") {
const status = await PermissionsAndroid.request(
"android.permission.POST_NOTIFICATIONS"
);
if (status !== "granted") {
ToastManager.show({
message: `The permission to show file upload notification was disallowed by the user.`,
type: "info"
});
}
}
let uploaded = false;
// Use multipart upload for files larger than MINIMUM_MULTIPART_FILE_SIZE
if (fileInfo.size >= MINIMUM_MULTIPART_FILE_SIZE) {
DatabaseLogger.info(
`Using multipart upload for large file: ${filename} (${fileInfo.size} bytes)`
);
const result = await multipartUploadFile(
filename,
filePath,
fileInfo.size,
requestOptions,
cancelToken
);
const status = result.status || 0;
uploaded = status >= 200 && status < 300;
if (!uploaded) {
const fileInfo = await RNFetchBlob.fs.stat(filePath);
throw new Error(
`${status}, name: ${fileInfo.filename}, length: ${
fileInfo.size
}, info: ${JSON.stringify(await result.text())}`
);
}
} else {
// Use single-part upload for smaller files
DatabaseLogger.info(
`Using single-part upload for file: ${filename} (${fileInfo.size} bytes)`
);
const upload = Upload.create({
customUploadId: filename,
path: Platform.OS === "ios" ? "file://" + fileInfo.path : fileInfo.path,
url: url,
method: "PUT",
headers: {
const uploadRequest = RNFetchBlob.config({
//@ts-ignore
IOSBackgroundTask: !globalThis["IS_SHARE_EXTENSION"]
})
.fetch(
"PUT",
url,
{
...headers,
"content-type": "application/octet-stream"
"content-type": ""
},
appGroup: IOS_APPGROUPID,
notification: {
filename:
attachmentInfo && isImage(attachmentInfo?.mimeType)
? "image"
: attachmentInfo?.filename || "file",
enabled: true,
enableRingTone: true,
autoClear: true
}
}).onChange((event) => {
switch (event.status) {
case "running":
case "pending":
useAttachmentStore
.getState()
.setProgress(
event.uploadedBytes || 0,
event.totalBytes || fileInfo.size,
filename,
0,
"upload"
);
DatabaseLogger.info(
`File upload progress: ${filename}, ${event.uploadedBytes}/${
event.totalBytes || fileInfo.size
}`
);
break;
case "completed":
DatabaseLogger.info("Upload completed");
break;
}
});
const result = await upload.start();
cancelToken.cancel = async () => {
useAttachmentStore.getState().remove(filename);
upload.cancel();
};
const status = result.responseCode || 0;
uploaded = status >= 200 && status < 300;
if (!uploaded) {
const fileInfo = await RNFetchBlob.fs.stat(filePath);
throw new Error(
`${status}, name: ${fileInfo.filename}, length: ${
fileInfo.size
}, info: ${JSON.stringify(result.error)}`
RNFetchBlob.wrap(filePath)
)
.uploadProgress((sent, total) => {
useAttachmentStore
.getState()
.setProgress(sent, total, filename, 0, "upload");
DatabaseLogger.info(
`File upload progress: ${filename}, ${sent}/${total}`
);
}
}
});
cancelToken.cancel = async () => {
useAttachmentStore.getState().remove(filename);
uploadRequest.cancel();
};
const uploadResponse = await uploadRequest;
const status = uploadResponse.info().status;
const uploaded = status >= 200 && status < 300;
useAttachmentStore.getState().remove(filename);
if (uploaded) {
attachmentInfo = await db.attachments.attachment(filename);
if (!attachmentInfo) return false;
await checkUpload(
filename,
requestOptions.chunkSize,
attachmentInfo.size
if (!uploaded) {
const fileInfo = await RNFetchBlob.fs.stat(filePath);
throw new Error(
`${status}, name: ${fileInfo.filename}, length: ${
fileInfo.size
}, info: ${JSON.stringify(uploadResponse.info())}`
);
DatabaseLogger.info(`File upload status: ${filename}, success`);
}
const attachment = await db.attachments.attachment(filename);
if (!attachment) return false;
await checkUpload(filename, requestOptions.chunkSize, attachment.size);
DatabaseLogger.info(`File upload status: ${filename}, ${status}`);
return uploaded;
} catch (e) {
useAttachmentStore.getState().remove(filename);

View File

@@ -155,16 +155,16 @@ export async function checkUpload(
expectedSize: number
) {
const size = await getUploadedFileSize(filename);
const totalChunks = Math.ceil(size / (chunkSize + ABYTES));
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;
? `File verification check failed.`
: expectedSize !== decryptedLength
? `File size mismatch. Expected ${size} bytes but got ${decryptedLength} bytes.`
: undefined;
if (error) throw new Error(error);
}
@@ -189,7 +189,3 @@ export async function checkAndCreateDir(path: string) {
}
return dir;
}
export const santizeUri = (uri: string) => {
return Platform.OS === "ios" ? decodeURI(uri).replace("file:///", "/") : uri;
};

View File

@@ -25,7 +25,7 @@ import { TouchableOpacity, View, useWindowDimensions } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import { DDS } from "../../services/device-detection";
import { eSendEvent, ToastManager } from "../../services/event-manager";
import { eSendEvent } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import PremiumService from "../../services/premium";
import SettingsService from "../../services/settings";
@@ -257,13 +257,8 @@ export const Login = ({
paddingHorizontal: 0
}}
onPress={() => {
ToastManager.show({
type: "info",
message:
"Password changing has been disabled temporarily to address some issues faced by users. It will be enabled again once the issues have resolved."
});
// if (loading || !email.current) return;
// SheetManager.show("forgotpassword_sheet");
if (loading || !email.current) return;
SheetManager.show("forgotpassword_sheet");
}}
textStyle={{
textDecorationLine: "underline"

View File

@@ -324,9 +324,7 @@ export const Signup = ({
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP,
width: DDS.isTab ? "50%" : "100%",
alignSelf: "center"
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<Paragraph

View File

@@ -1,66 +0,0 @@
import { useThemeColors } from "@notesnook/theme";
import { useRef } from "react";
import { View } from "react-native";
import { defaultBorderRadius } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import DatePicker from "react-native-date-picker";
import dayjs from "dayjs";
import { strings } from "@notesnook/intl";
import { Button } from "../ui/button";
export default function DatePickerComponent(props: {
onConfirm: (date: Date) => void;
onCancel: () => void;
}) {
const { colors, isDark } = useThemeColors();
const dateRef = useRef<Date>(dayjs().add(1, "day").toDate());
return (
<View
style={{
backgroundColor: colors.primary.background,
borderRadius: defaultBorderRadius,
padding: DefaultAppStyles.GAP,
borderWidth: 0.5,
borderColor: colors.primary.border,
width: "80%",
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
<DatePicker
theme={isDark ? "dark" : "light"}
mode="date"
minimumDate={dayjs().add(1, "day").toDate()}
onCancel={() => {
close?.();
}}
date={dateRef.current}
onDateChange={(date) => {
dateRef.current = date;
}}
/>
<Button
title={strings.setExpiry()}
type="accent"
style={{
width: "100%"
}}
onPress={async () => {
if (!dateRef.current) return;
props.onConfirm(dateRef.current);
}}
/>
<Button
title={strings.cancel()}
type="secondary"
style={{
width: "100%"
}}
onPress={async () => {
props.onCancel();
}}
/>
</View>
);
}

View File

@@ -42,7 +42,9 @@ export default function DelayLayout({
...props
}: IDelayLayoutProps) {
const { colors } = useThemeColors();
const loading = useDelayLayout(props.delay === undefined ? 200 : props.delay);
const loading = useDelayLayout(
!props.delay || props.delay < 300 ? 0 : props.delay
);
const Placeholder = placeholder[props.type || "default"];
return loading || props.wait ? (

View File

@@ -136,12 +136,12 @@ const BaseDialog = ({
backgroundColor: background
? background
: transparent
? "transparent"
: "rgba(0,0,0,0.3)"
? "transparent"
: "rgba(0,0,0,0.3)"
}}
>
<KeyboardAvoidingView
enabled={!floating && !avoidKeyboardResize}
enabled={!floating && Platform.OS === "ios" && !avoidKeyboardResize}
behavior="padding"
>
<BouncingView
@@ -155,8 +155,8 @@ const BaseDialog = ({
justifyContent: centered
? "center"
: bottom
? "flex-end"
: "flex-start"
? "flex-end"
: "flex-start"
}
]}
>

View File

@@ -17,6 +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 { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { TextInput, View, ViewStyle } from "react-native";

View File

@@ -23,7 +23,6 @@ import React, { useCallback, useEffect, useRef, useState } from "react";
import { Dimensions, TextInput, View } from "react-native";
import Orientation from "react-native-orientation-locker";
import Pdf from "react-native-pdf";
import FileViewer from "react-native-file-viewer";
import { MMKV } from "../../../common/database/mmkv";
import downloadAttachment from "../../../common/filesystem/download-attachment";
import { deleteCacheFileByPath, exists } from "../../../common/filesystem/io";
@@ -44,7 +43,6 @@ import SheetProvider from "../../sheet-provider";
import { IconButton } from "../../ui/icon-button";
import { ProgressBarComponent } from "../../ui/svg/lazy";
import Paragraph from "../../ui/typography/paragraph";
import ReactNativeBlobUtil from "react-native-blob-util";
const WIN_WIDTH = Dimensions.get("window").width;
const WIN_HEIGHT = Dimensions.get("window").height;
@@ -119,11 +117,8 @@ const PDFPreview = () => {
setVisible(false);
return;
}
let path = `${cacheDir}/${attachment.filename}`;
const path = `${cacheDir}/${uri}`;
snapshotValue.current = snapshot.current;
await ReactNativeBlobUtil.fs
.mv(`${cacheDir}/${uri}`, path)
.catch(console.log);
setPDFSource("file://" + path);
setLoading(false);
}, 100);
@@ -132,7 +127,7 @@ const PDFPreview = () => {
);
const close = () => {
deleteCacheFileByPath(pdfSource.replace("file://", ""));
deleteCacheFileByPath(pdfSource);
setPDFSource(null);
setVisible(false);
setPassword("");
@@ -162,12 +157,7 @@ const PDFPreview = () => {
return (
visible && (
<BaseDialog
animation="fade"
visible={true}
onRequestClose={close}
useSafeArea={false}
>
<BaseDialog animation="fade" visible={true} onRequestClose={close}>
<SheetProvider context={attachment?.hash} />
<Dialog context={attachment?.hash} />
@@ -233,48 +223,45 @@ const PDFPreview = () => {
<View
style={{
flexDirection: "row",
gap: DefaultAppStyles.GAP_SMALL
alignItems: "center",
marginRight: 12
}}
>
<View
<TextInput
ref={inputRef}
defaultValue={currentPage + ""}
style={{
flexDirection: "row",
alignItems: "center"
color: colors.primary.paragraph,
padding: 0,
paddingTop: 0,
paddingBottom: 0,
marginTop: 0,
marginBottom: 0,
paddingVertical: 0,
height: 25,
backgroundColor: colors.secondary.background,
width: 40,
textAlign: "center",
marginRight: 4,
borderRadius: 3,
fontFamily: "Inter-Regular"
}}
>
<TextInput
ref={inputRef}
defaultValue={currentPage + ""}
style={{
color: colors.primary.paragraph,
padding: 0,
paddingTop: 0,
paddingBottom: 0,
marginTop: 0,
marginBottom: 0,
paddingVertical: 0,
height: 25,
backgroundColor: colors.secondary.background,
width: 40,
textAlign: "center",
marginRight: 4,
borderRadius: 3,
fontFamily: "Inter-Regular"
}}
selectTextOnFocus
keyboardType="decimal-pad"
onSubmitEditing={(event) => {
setCurrentPage(event.nativeEvent.text);
pdfRef.current?.setPage(
parseInt(event.nativeEvent.text)
);
}}
blurOnSubmit
/>
<Paragraph color={colors.static.white}>
/{numPages}
</Paragraph>
</View>
selectTextOnFocus
keyboardType="decimal-pad"
onSubmitEditing={(event) => {
setCurrentPage(event.nativeEvent.text);
pdfRef.current?.setPage(parseInt(event.nativeEvent.text));
}}
blurOnSubmit
/>
<Paragraph color={colors.static.white}>/{numPages}</Paragraph>
</View>
<View
style={{
flexDirection: "row"
}}
>
<IconButton
color={colors.static.white}
name="download"
@@ -282,16 +269,6 @@ const PDFPreview = () => {
downloadAttachment(attachment.hash, false);
}}
/>
<IconButton
color={colors.static.white}
name="open-in-new"
onPress={() => {
FileViewer.open(pdfSource, {
showOpenWithDialog: true,
showAppsSuggestions: true
});
}}
/>
</View>
</View>
{pdfSource ? (

View File

@@ -0,0 +1,902 @@
/*
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 Clipboard from "@react-native-clipboard/clipboard";
import React, { Component, createRef } from "react";
import { InteractionManager, View } from "react-native";
import Share from "react-native-share";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import BiometricService from "../../../services/biometrics";
import { DDS } from "../../../services/device-detection";
import {
ToastManager,
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { getElevationStyle } from "../../../utils/elevation";
import {
eCloseActionSheet,
eCloseVaultDialog,
eOnLoadNote,
eOpenVaultDialog,
eUpdateNoteInEditor
} from "../../../utils/events";
import { deleteItems } from "../../../utils/functions";
import { fluidTabsRef } from "../../../utils/global-refs";
import { convertNoteToText } from "../../../utils/note-to-text";
import { sleep } from "../../../utils/time";
import BaseDialog from "../../dialog/base-dialog";
import DialogButtons from "../../dialog/dialog-buttons";
import DialogHeader from "../../dialog/dialog-header";
import { Toast } from "../../toast";
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";
import { DefaultAppStyles } from "../../../utils/styles";
export class VaultDialog extends Component {
constructor(props) {
super(props);
this.state = {
visible: false,
wrongPassword: false,
loading: false,
note: {},
vault: false,
locked: true,
permanant: false,
goToEditor: false,
share: false,
passwordsDontMatch: false,
deleteNote: false,
focusIndex: null,
biometricUnlock: false,
isBiometryEnrolled: false,
isBiometryAvailable: false,
fingerprintAccess: false,
changePassword: false,
copyNote: false,
revokeFingerprintAccess: false,
title: strings.goToEditor(),
description: null,
clearVault: false,
deleteVault: false,
deleteAll: false,
noteLocked: false
};
this.passInputRef = createRef();
this.confirmPassRef = createRef();
this.changePassInputRef = createRef();
this.password = null;
this.confirmPassword = null;
this.newPassword = null;
this.title = !this.state.novault
? strings.createVault()
: this.state.fingerprintAccess
? strings.vaultFingerprintUnlock()
: this.state.revokeFingerprintAccess
? strings.revokeVaultFingerprintUnlock()
: this.state.changePassword
? strings.changeVaultPassword()
: this.state.noteLocked
? this.state.deleteNote
? strings.deleteNote()
: this.state.share
? strings.shareNote()
: this.state.copyNote
? strings.copyNote()
: this.state.goToEditor
? strings.goToEditor()
: strings.goToEditor()
: strings.lockNote();
}
componentDidMount() {
eSubscribeEvent(eOpenVaultDialog, this.open);
eSubscribeEvent(eCloseVaultDialog, this.close);
}
componentWillUnmount() {
eUnSubscribeEvent(eOpenVaultDialog, this.open);
eUnSubscribeEvent(eCloseVaultDialog, this.close);
}
/**
*
* @param {import('../../../services/event-manager').vaultType} data
*/
open = async (data) => {
let biometry = await BiometricService.isBiometryAvailable();
let available = false;
let fingerprint = await BiometricService.hasInternetCredentials("nn_vault");
if (biometry) {
available = true;
}
this.setState({
note: data.item,
novault: data.novault,
locked: data.locked,
permanant: data.permanant,
goToEditor: data.goToEditor,
share: data.share,
deleteNote: data.deleteNote,
copyNote: data.copyNote,
isBiometryAvailable: available,
biometricUnlock: fingerprint,
isBiometryEnrolled: fingerprint,
fingerprintAccess: data.fingerprintAccess,
changePassword: data.changePassword,
revokeFingerprintAccess: data.revokeFingerprintAccess,
title: data.title,
description: data.description,
clearVault: data.clearVault,
deleteVault: data.deleteVault,
noteLocked: data.item && (await db.vaults.itemExists(data.item))
});
if (
fingerprint &&
data.novault &&
!data.fingerprintAccess &&
!data.revokeFingerprintAccess &&
!data.changePassword &&
!data.clearVault &&
!data.deleteVault
) {
await this._onPressFingerprintAuth(data.title, data.description);
} else {
this.setState({
visible: true
});
}
};
close = () => {
if (this.state.loading) {
ToastManager.show({
heading: this.state.title,
message: strings.pleaseWait() + "...",
type: "success",
context: "local"
});
return;
}
Navigation.queueRoutesForUpdate();
this.password = null;
this.confirmPassword = null;
this.setState({
visible: false,
note: {},
locked: false,
permanant: false,
goToEditor: false,
share: false,
novault: false,
deleteNote: false,
passwordsDontMatch: false
});
};
onPress = async () => {
if (this.state.revokeFingerprintAccess) {
await this._revokeFingerprintAccess();
this.close();
return;
}
if (this.state.loading) return;
if (!this.password) {
ToastManager.show({
heading: strings.passwordNotEntered(),
type: "error",
context: "local"
});
return;
}
if (!this.state.novault) {
if (this.password !== this.confirmPassword) {
ToastManager.show({
heading: strings.passwordNotMatched(),
type: "error",
context: "local"
});
this.setState({
passwordsDontMatch: true
});
return;
}
this._createVault();
} else if (this.state.changePassword) {
this.setState({
loading: true
});
db.vault
.changePassword(this.password, this.newPassword)
.then(() => {
this.setState({
loading: false
});
if (this.state.biometricUnlock) {
this._enrollFingerprint(this.newPassword);
}
ToastManager.show({
heading: strings.passwordUpdated(),
type: "success",
context: "global"
});
this.close();
})
.catch((e) => {
this.setState({
loading: false
});
if (e.message === db.vault.ERRORS.wrongPassword) {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
} else {
ToastManager.error(e);
}
});
} else if (this.state.locked) {
if (!this.password || this.password.trim() === 0) {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
this.setState({
wrongPassword: true
});
return;
}
if (this.state.noteLocked) {
await this._unlockNote();
} else {
db.vault
.unlock(this.password)
.then(async () => {
this.setState({
wrongPassword: false
});
await this._lockNote();
})
.catch((e) => {
this._takeErrorAction(e);
});
}
} else if (this.state.fingerprintAccess) {
this._enrollFingerprint(this.password);
} else if (this.state.clearVault) {
await this.clearVault();
} else if (this.state.deleteVault) {
await this.deleteVault();
}
};
deleteVault = async () => {
this.setState({
loading: true
});
try {
let verified = true;
if (await db.user.getUser()) {
verified = await db.user.verifyPassword(this.password);
}
if (verified) {
let noteIds = [];
if (this.state.deleteAll) {
const vault = await db.vaults.default();
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
);
});
}
eSendEvent("vaultUpdated");
this.setState({
loading: false
});
this.close();
} else {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
}
} catch (e) {
console.error(e);
}
this.setState({
loading: false
});
};
clearVault = async () => {
this.setState({
loading: true
});
try {
const vault = await db.vaults.default();
const relations = await db.relations.from(vault, "note").get();
const noteIds = relations.map((item) => item.toId);
await db.vault.clear(this.password);
noteIds.forEach((id) => {
eSendEvent(
eUpdateNoteInEditor,
{
id: id,
deleted: true
},
true
);
});
this.setState({
loading: false
});
this.close();
eSendEvent("vaultUpdated");
} catch (e) {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
}
this.setState({
loading: false
});
};
async _lockNote() {
if (!this.password || this.password.trim() === 0) {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
return;
} else {
await db.vault.add(this.state.note.id);
eSendEvent(eUpdateNoteInEditor, this.state.note, true);
this.close();
ToastManager.show({
message: strings.noteLocked(),
type: "error",
context: "local"
});
this.setState({
loading: false
});
}
}
async _unlockNote() {
if (!this.password || this.password.trim() === 0) {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
return;
}
if (this.state.permanant) {
this._permanantUnlock();
} else {
await this._openNote();
}
}
_openNote = async () => {
try {
let note = await db.vault.open(this.state.note.id, this.password);
if (this.state.biometricUnlock && !this.state.isBiometryEnrolled) {
await this._enrollFingerprint(this.password);
}
if (this.state.goToEditor) {
this._openInEditor(note);
} else if (this.state.share) {
await this._shareNote(note);
} else if (this.state.deleteNote) {
await this._deleteNote();
} else if (this.state.copyNote) {
await this._copyNote(note);
}
} catch (e) {
this._takeErrorAction(e);
}
};
async _deleteNote() {
try {
await db.vault.remove(this.state.note.id, this.password);
await deleteItems("note", [this.state.note.id]);
this.close();
} catch (e) {
this._takeErrorAction(e);
}
}
async _enrollFingerprint(password) {
this.setState(
{
loading: true
},
async () => {
try {
await db.vault.unlock(password);
await BiometricService.storeCredentials(password);
this.setState({
loading: false
});
eSendEvent("vaultUpdated");
ToastManager.show({
heading: strings.biometricUnlockEnabled(),
type: "success",
context: "global"
});
this.close();
} catch (e) {
this.close();
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
this.setState({
loading: false
});
}
}
);
}
async _createVault() {
await db.vault.create(this.password);
if (this.state.biometricUnlock) {
await this._enrollFingerprint(this.password);
}
if (this.state.note?.id) {
await db.vault.add(this.state.note.id);
eSendEvent(eUpdateNoteInEditor, this.state.note, true);
this.setState({
loading: false
});
ToastManager.show({
heading: strings.noteLocked(),
type: "success",
context: "global"
});
this.close();
} else {
ToastManager.show({
heading: strings.vaultCreated(),
type: "success",
context: "global"
});
this.close();
}
eSendEvent("vaultUpdated");
}
_permanantUnlock() {
db.vault
.remove(this.state.note.id, this.password)
.then(() => {
ToastManager.show({
heading: strings.noteUnlocked(),
type: "success",
context: "global"
});
eSendEvent(eUpdateNoteInEditor, this.state.note, true);
this.close();
})
.catch((e) => {
this._takeErrorAction(e);
});
}
_openInEditor(note) {
this.close();
InteractionManager.runAfterInteractions(async () => {
eSendEvent(eOnLoadNote, {
item: note
});
if (!DDS.isTab) {
fluidTabsRef.current?.goToPage("editor");
}
});
}
async _copyNote(note) {
Clipboard.setString((await convertNoteToText(note, true)) || "");
ToastManager.show({
heading: strings.noteCopied(),
type: "success",
context: "global"
});
this.close();
}
async _shareNote(note) {
this.close();
try {
await Share.open({
heading: note.title,
failOnCancel: false,
message: (await convertNoteToText(note)) || ""
});
} catch (e) {
console.error(e);
}
}
_takeErrorAction(e) {
this.setState({
wrongPassword: true,
visible: true
});
setTimeout(() => {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
}, 500);
}
_revokeFingerprintAccess = async () => {
try {
await BiometricService.resetCredentials();
eSendEvent("vaultUpdated");
ToastManager.show({
heading: strings.biometricUnlockDisabled(),
type: "success",
context: "global"
});
} catch (e) {
ToastManager.show({
heading: e.message,
type: "success",
context: "global"
});
}
};
_onPressFingerprintAuth = async (title, description) => {
try {
let credentials = await BiometricService.getCredentials(
title || this.state.title,
description || this.state.description
);
if (credentials?.password) {
this.password = credentials.password;
this.onPress();
} else {
eSendEvent(eCloseActionSheet);
await sleep(300);
this.setState({
visible: true
});
}
} catch (e) {
console.error(e);
}
};
render() {
const { colors } = this.props;
const {
note,
visible,
novault,
deleteNote,
share,
goToEditor,
fingerprintAccess,
changePassword,
loading,
deleteVault,
clearVault
} = this.state;
if (!visible) return null;
return (
<BaseDialog
onShow={async () => {
await sleep(100);
this.passInputRef.current?.focus();
}}
statusBarTranslucent={false}
onRequestClose={this.close}
visible={true}
>
<View
style={{
...getElevationStyle(5),
width: DDS.isTab ? 350 : "85%",
borderRadius: 10,
backgroundColor: colors.primary.background,
paddingTop: 12
}}
>
<DialogHeader title={this.state.title} icon="shield" padding={12} />
<Seperator half />
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
}}
>
{(novault ||
changePassword ||
this.state.clearVault ||
this.state.deleteVault) &&
!this.state.revokeFingerprintAccess ? (
<>
<Input
fwdRef={this.passInputRef}
editable={!loading}
autoCapitalize="none"
testID={notesnook.ids.dialogs.vault.pwd}
onChangeText={(value) => {
this.password = value;
}}
marginBottom={
!this.state.biometricUnlock ||
!this.state.isBiometryEnrolled ||
!novault ||
changePassword
? 0
: 10
}
onSubmit={() => {
changePassword
? this.confirmPassRef.current?.focus()
: this.onPress;
}}
autoComplete="password"
returnKeyLabel={
changePassword ? strings.next() : this.state.title
}
returnKeyType={changePassword ? "next" : "done"}
secureTextEntry
placeholder={
changePassword
? strings.currentPassword()
: strings.password()
}
/>
{!this.state.biometricUnlock ||
!this.state.isBiometryEnrolled ||
!novault ||
changePassword ? null : (
<Button
onPress={() =>
this._onPressFingerprintAuth(strings.unlockNote(), "")
}
icon="fingerprint"
width="100%"
title={strings.unlockWithBiometrics()}
type="transparent"
/>
)}
</>
) : null}
{this.state.deleteVault && (
<Button
onPress={() =>
this.setState({
deleteAll: !this.state.deleteAll
})
}
icon={
this.state.deleteAll
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
width="100%"
title={strings.deleteAllNotes()}
type="errorShade"
/>
)}
{changePassword ? (
<>
<Seperator half />
<Input
ref={this.confirmPassRef}
editable={!loading}
testID={notesnook.ids.dialogs.vault.changePwd}
autoCapitalize="none"
onChangeText={(value) => {
this.newPassword = value;
}}
autoComplete="password"
onSubmit={this.onPress}
returnKeyLabel="Change"
returnKeyType="done"
secureTextEntry
placeholder={strings.newPassword()}
/>
</>
) : null}
{!novault ? (
<View>
<Input
fwdRef={this.passInputRef}
autoCapitalize="none"
testID={notesnook.ids.dialogs.vault.pwd}
onChangeText={(value) => {
this.password = value;
}}
autoComplete="password"
returnKeyLabel={strings.next()}
returnKeyType="next"
secureTextEntry
onSubmit={() => {
this.confirmPassRef.current?.focus();
}}
placeholder={strings.password()}
/>
<Input
fwdRef={this.confirmPassRef}
autoCapitalize="none"
testID={notesnook.ids.dialogs.vault.pwdAlt}
secureTextEntry
validationType="confirmPassword"
customValidator={() => this.password}
errorMessage="Passwords do not match."
onErrorCheck={() => null}
marginBottom={0}
autoComplete="password"
returnKeyLabel="Create"
returnKeyType="done"
onChangeText={(value) => {
this.confirmPassword = value;
if (value !== this.password) {
this.setState({
passwordsDontMatch: true
});
} else {
this.setState({
passwordsDontMatch: false
});
}
}}
onSubmit={this.onPress}
placeholder={strings.confirmPassword()}
/>
</View>
) : null}
{this.state.biometricUnlock &&
!this.state.isBiometryEnrolled &&
novault ? (
<Paragraph>{strings.vaultEnableBiometrics()}</Paragraph>
) : null}
{this.state.isBiometryAvailable &&
!this.state.fingerprintAccess &&
!this.state.clearVault &&
!this.state.deleteVault &&
((!this.state.biometricUnlock && !changePassword) || !novault) ? (
<Button
onPress={() => {
this.setState({
biometricUnlock: !this.state.biometricUnlock
});
}}
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
icon="fingerprint"
width="100%"
title={strings.unlockWithBiometrics()}
iconColor={
this.state.biometricUnlock
? colors.selected.accent
: colors.primary.icon
}
type={this.state.biometricUnlock ? "transparent" : "plain"}
/>
) : null}
</View>
<DialogButtons
onPressNegative={this.close}
onPressPositive={this.onPress}
loading={loading}
positiveType={
deleteVault || clearVault ? "errorShade" : "transparent"
}
positiveTitle={
deleteVault
? strings.delete()
: clearVault
? strings.clear()
: fingerprintAccess
? strings.enable()
: this.state.revokeFingerprintAccess
? strings.revoke()
: changePassword
? strings.change()
: this.state.noteLocked
? deleteNote
? strings.delete()
: share
? strings.share()
: goToEditor
? strings.open()
: strings.unlock()
: !note.id
? strings.create()
: strings.lock()
}
/>
</View>
<Toast context="local" />
</BaseDialog>
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -31,7 +31,6 @@ import { Gesture, GestureDetector } from "react-native-gesture-handler";
import Animated, {
runOnJS,
SharedValue,
SnappySpringConfig,
useAnimatedReaction,
useAnimatedStyle,
useSharedValue,
@@ -111,6 +110,7 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
const containerWidth = widths
? widths.sidebar + widths.list + widths.editor
: dimensions.width;
const drawerPosition = 0;
const homePosition = widths.sidebar;
const editorPosition = widths.sidebar + widths.list;
@@ -354,10 +354,13 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
const finalValue = isSwipeLeft
? translateX.value - velocityX / 40.0
: translateX.value + velocityX / 40.0;
const velocity = velocityX / 5000;
const animationConfig: WithSpringConfig = {
velocity: velocity,
...SnappySpringConfig
velocity: velocityX,
mass: 0.5,
overshootClamping: true,
damping: 800,
stiffness: 800
};
if (finalValue < homePosition) {
@@ -440,7 +443,15 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
width: containerWidth,
flexDirection: "row"
},
animatedStyles
deviceMode === "tablet"
? {
transform: [
{
translateX: 0
}
]
}
: animatedStyles
]}
>
{children}

View File

@@ -50,7 +50,7 @@ export const Header = ({
onLeftMenuButtonPress?: () => void;
renderedInRoute?: RouteName;
id?: string;
title?: string;
title: string;
canGoBack?: boolean;
onPressDefaultRightButton?: () => void;
hasSearch?: boolean;
@@ -114,16 +114,7 @@ export const Header = ({
onLeftButtonPress={onLeftMenuButtonPress}
/>
{!title ? (
<View
style={{
width: 100,
backgroundColor: colors.primary.hover,
height: 10,
borderRadius: 100
}}
/>
) : hasSearch ? (
{hasSearch ? (
<Paragraph>
{selectionMode
? `${selectedItemsList.length} selected`

View File

@@ -131,7 +131,7 @@ function ReorderableList<T extends { id: string }>({
]
);
const getOrderedItems = React.useCallback(() => {
function getOrderedItems() {
if (!customizableSidebarFeature?.isAllowed) return data;
const items: T[] = [];
itemOrderState.forEach((id) => {
@@ -142,7 +142,7 @@ function ReorderableList<T extends { id: string }>({
items.push(...data.filter((i) => !itemOrderState.includes(i.id)));
return items;
}, [data, customizableSidebarFeature?.isAllowed]);
}
return (
<View style={styles.container}>

View File

@@ -18,16 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { getFormattedDate } from "@notesnook/common";
import {
EncryptedContentItem,
isEncryptedContent,
Note,
UnencryptedContentItem
} from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import KeepAwake from "@sayem314/react-native-keep-awake";
import { diff } from "diffblazer";
import React, { useEffect, useRef, useState } from "react";
import { SafeAreaView, Text, View } from "react-native";
import Animated from "react-native-reanimated";
@@ -40,16 +32,13 @@ import { DDS } from "../../services/device-detection";
import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent,
openVault
eUnSubscribeEvent
} from "../../services/event-manager";
import Navigation from "../../services/navigation";
import Sync from "../../services/sync";
import { useSettingStore } from "../../stores/use-setting-store";
import { eOnLoadNote, eShowMergeDialog } from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { Dialog } from "../dialog";
import BaseDialog from "../dialog/base-dialog";
import DialogButtons from "../dialog/dialog-buttons";
import DialogContainer from "../dialog/dialog-container";
@@ -58,55 +47,46 @@ import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Seperator from "../ui/seperator";
import Paragraph from "../ui/typography/paragraph";
import { presentDialog } from "../dialog/functions";
import { diff } from "diffblazer";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../utils/styles";
const MergeConflicts = () => {
const { colors } = useThemeColors();
const [visible, setVisible] = useState(false);
const [selectedContent, setSelectedContent] =
useState<UnencryptedContentItem>();
const [copyOfDiscardedContent, setCopyOfDiscardedContent] =
useState<UnencryptedContentItem>();
const [keep, setKeep] = useState(null);
const [copy, setCopy] = useState(null);
const [dialogVisible, setDialogVisible] = useState(false);
const insets = useGlobalSafeAreaInsets();
const content = useRef<UnencryptedContentItem>(null);
const content = useRef(null);
const isKeepingConflicted = !keep?.conflicted;
const isKeeping = !!keep;
const { height } = useSettingStore((state) => state.dimensions);
const applyChanges = async () => {
let contentToSave = selectedContent;
if (!contentToSave) return;
let note = await db.notes.note(contentToSave.noteId);
if (!note) return;
let _content = keep;
let note = await db.notes.note(_content.noteId);
await db.notes.add({
id: note.id,
conflicted: false,
dateEdited: contentToSave.dateEdited
dateEdited: _content.dateEdited
});
const noteLocked = await db.vaults.itemExists(note);
await db.content.add({
id: note.contentId,
data: _content.data,
type: _content.type,
dateResolved: content.current?.conflicted?.dateModified || Date.now(),
sessionId: Date.now(),
conflicted: false
});
if (noteLocked) {
await db.vault.save({
...contentToSave,
sessionId: `${Date.now()}`
});
} else {
await db.content.add({
id: note.contentId,
data: contentToSave.data,
type: contentToSave.type,
dateResolved: content.current?.conflicted?.dateModified || Date.now(),
sessionId: `${Date.now()}`,
conflicted: undefined
});
}
if (copyOfDiscardedContent) {
if (copy) {
await db.notes.add({
title: note.title + " (Copy)",
content: {
data: copyOfDiscardedContent.data,
type: copyOfDiscardedContent.type
data: copy.data,
type: copy.type
}
});
}
@@ -123,86 +103,15 @@ const MergeConflicts = () => {
Sync.run();
};
const show = async (item: Note) => {
const isLocked = await db.vaults.itemExists(item);
let noteContent: UnencryptedContentItem;
if (isLocked) {
openVault({
item: item,
novault: true,
customActionTitle: "Unlock note",
customActionParagraph: "Unlock note to merge conflicts",
onUnlock: async (item, password) => {
if (!item || !password) return;
const currentContent = await db.content.get(item.contentId!);
try {
noteContent = {
...(await db.content.get(item.contentId!)),
...item.content,
conflicted: currentContent?.conflicted
? await db.vault.decryptContent(
currentContent?.conflicted as EncryptedContentItem,
password
)
: undefined
} as UnencryptedContentItem;
content.current = noteContent;
if (__DEV__) {
if (!noteContent?.conflicted) {
content.current.conflicted = noteContent;
}
}
setVisible(true);
} catch (e) {
presentDialog({
input: true,
inputPlaceholder: strings.enterPassword(),
title: strings.unlockIncomingNote(),
paragraph: strings.unlockIncomingNoteDesc(),
positiveText: "Unlock",
positivePress: async (password) => {
try {
noteContent = {
...(await db.content.get(item.contentId!)),
...item.content,
conflicted: currentContent?.conflicted
? await db.vault.decryptContent(
currentContent?.conflicted as EncryptedContentItem,
password
)
: undefined
} as UnencryptedContentItem;
content.current = noteContent;
if (__DEV__) {
if (!noteContent?.conflicted) {
content.current.conflicted = noteContent;
}
}
setVisible(true);
return true;
} catch (e) {
return false;
}
}
});
}
}
});
} else {
noteContent = (await db.content.get(
item.contentId!
)) as UnencryptedContentItem;
content.current = noteContent;
if (__DEV__) {
if (!noteContent?.conflicted) {
content.current.conflicted = noteContent;
}
const show = async (item) => {
let noteContent = await db.content.get(item.contentId);
content.current = { ...noteContent };
if (__DEV__) {
if (!noteContent.conflicted) {
content.current.conflicted = { ...noteContent };
}
setVisible(true);
}
setVisible(true);
};
useEffect(() => {
@@ -214,8 +123,8 @@ const MergeConflicts = () => {
const close = () => {
setVisible(false);
setCopyOfDiscardedContent(undefined);
setSelectedContent(undefined);
setCopy(null);
setKeep(null);
setDialogVisible(false);
};
@@ -225,12 +134,6 @@ const MergeConflicts = () => {
back,
isCurrent,
contentToKeep
}: {
isDiscarded: boolean;
keeping: boolean;
back: boolean;
isCurrent: boolean;
contentToKeep: UnencryptedContentItem;
}) => {
return (
<View
@@ -291,7 +194,7 @@ const MergeConflicts = () => {
{isDiscarded ? (
<Button
onPress={() => {
setCopyOfDiscardedContent(contentToKeep);
setCopy(contentToKeep);
setDialogVisible(true);
}}
title={strings.saveACopy()}
@@ -319,6 +222,7 @@ const MergeConflicts = () => {
paddingHorizontal: DefaultAppStyles.GAP
}}
fontSize={AppFontSize.xs}
color={colors.error.paragraph}
onPress={() => {
setDialogVisible(true);
}}
@@ -340,9 +244,7 @@ const MergeConflicts = () => {
keeping && !isDiscarded ? strings.undo() : strings.keep()
}
onPress={() => {
setSelectedContent(
keeping && !isDiscarded ? undefined : contentToKeep
);
setKeep(keeping && !isDiscarded ? null : contentToKeep);
}}
/>
</>
@@ -356,6 +258,7 @@ const MergeConflicts = () => {
<BaseDialog
statusBarTranslucent
transparent={false}
animationType="slide"
animated={false}
bounce={false}
onRequestClose={() => {
@@ -363,9 +266,15 @@ const MergeConflicts = () => {
}}
centered={false}
background={colors?.primary.background}
supportedOrientations={[
"portrait",
"portrait-upside-down",
"landscape",
"landscape-left",
"landscape-right"
]}
visible={true}
>
<Dialog context="merge-conflicts" />
<SafeAreaView
style={{
backgroundColor: colors.primary.background,
@@ -392,15 +301,15 @@ const MergeConflicts = () => {
style={{
height: "100%",
width: "100%",
backgroundColor: DDS.isLargeTablet() ? "rgba(0,0,0,0.3)" : undefined
backgroundColor: DDS.isLargeTablet() ? "rgba(0,0,0,0.3)" : null
}}
>
<ConfigBar
back={true}
isCurrent={true}
isDiscarded={!!selectedContent && !selectedContent.conflicted}
keeping={!!selectedContent}
contentToKeep={content.current!}
isDiscarded={isKeeping && isKeepingConflicted}
keeping={isKeeping}
contentToKeep={content.current}
/>
<Animated.View
@@ -414,18 +323,15 @@ const MergeConflicts = () => {
<ReadonlyEditor
editorId="conflictPrimary"
onLoad={async (loadContent) => {
const note = await db.notes.note(content.current!.noteId);
const note = await db.notes.note(content.current?.noteId);
if (!note) return;
if (content.current && content.current.conflicted) {
loadContent({
id: note.id,
data: diff(
(content.current.conflicted as UnencryptedContentItem)
.data,
content.current.data
)
});
}
loadContent({
id: note.id,
data: diff(
content.current.conflicted.data,
content.current.data
)
});
}}
/>
</Animated.View>
@@ -433,11 +339,9 @@ const MergeConflicts = () => {
<ConfigBar
back={false}
isCurrent={false}
isDiscarded={!!selectedContent && !!selectedContent.conflicted}
keeping={!!selectedContent}
contentToKeep={
content.current!.conflicted! as UnencryptedContentItem
}
isDiscarded={isKeeping && !isKeepingConflicted}
keeping={isKeeping}
contentToKeep={content.current.conflicted}
/>
<Animated.View
@@ -450,12 +354,11 @@ const MergeConflicts = () => {
<ReadonlyEditor
editorId="conflictSecondary"
onLoad={async (loadContent) => {
const note = await db.notes.note(content.current?.noteId!);
const note = await db.notes.note(content.current?.noteId);
if (!note) return;
loadContent({
id: note.id,
data: (content.current!.conflicted as UnencryptedContentItem)
.data
data: content.current.conflicted.data
});
}}
/>

View File

@@ -47,7 +47,7 @@ const HistoryItem = ({
}: {
index: number;
items?: VirtualizedGrouping<HistorySession>;
note: Note;
note?: Note;
}) => {
const [item] = useDBItem(index, "noteHistory", items);
const { colors } = useThemeColors();
@@ -73,7 +73,6 @@ const HistoryItem = ({
session: getDate(item.dateCreated, item.dateModified)
}}
content={content}
note={note}
/>
),
context: "note_history"
@@ -136,7 +135,7 @@ export default function NoteHistory({
const renderItem = useCallback(
({ index }: { index: number }) => (
<HistoryItem index={index} items={history} note={note} />
<HistoryItem index={index} items={history} />
),
[history]
);

View File

@@ -37,43 +37,21 @@ import Paragraph from "../ui/typography/paragraph";
import { diff } from "diffblazer";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../utils/styles";
import {
HistorySession,
isEncryptedContent,
Note,
NoteContent,
SessionContentItem,
TrashOrItem
} from "@notesnook/core";
/**
*
* @param {any} param0
* @returns
*/
export default function NotePreview({
session,
content,
note
}: {
session?: HistorySession & { session: string };
content:
| Partial<
NoteContent<boolean> & {
title: string;
}
>
| undefined;
note: TrashOrItem<Note>;
}) {
export default function NotePreview({ session, content, note }) {
const { colors } = useThemeColors();
const [locked, setLocked] = useState(false);
async function restore() {
if (note && note.type === "trash") {
await db.trash.restore(note.id);
if ((await db.trash.restore(note.id)) === false) return;
Navigation.queueRoutesForUpdate();
useSelectionStore.getState().setSelectionMode();
useSelectionStore.getState().setSelectionMode(false);
ToastManager.show({
heading: strings.noteRestored(),
type: "success"
@@ -81,26 +59,24 @@ export default function NotePreview({
eSendEvent(eCloseSheet);
return;
}
if (session) {
await db.noteHistory.restore(session.id);
if (useTabStore.getState().hasTabForNote(session?.noteId)) {
const note = editorController.current.note.current[session?.noteId];
if (note) {
eSendEvent(eOnLoadNote, {
item: note,
forced: true
});
}
await db.noteHistory.restore(session.id);
if (useTabStore.getState().hasTabForNote(session?.noteId)) {
const note = editorController.current.note.current[session?.noteId];
if (note) {
eSendEvent(eOnLoadNote, {
item: note,
forced: true
});
}
eSendEvent(eCloseSheet, "note_history");
eSendEvent(eCloseSheet);
Navigation.queueRoutesForUpdate();
ToastManager.show({
heading: strings.noteRestoredFromHistory(),
type: "success"
});
}
eSendEvent(eCloseSheet, "note_history");
eSendEvent(eCloseSheet);
Navigation.queueRoutesForUpdate();
ToastManager.show({
heading: strings.noteRestoredFromHistory(),
type: "success"
});
}
useEffect(() => {
@@ -115,17 +91,15 @@ export default function NotePreview({
negativeText: strings.cancel(),
context: "local",
positivePress: async () => {
if (note) {
await db.trash.delete(note.id);
useTrashStore.getState().refresh();
useSelectionStore.getState().setSelectionMode();
ToastManager.show({
heading: strings.noteDeleted(),
type: "success",
context: "local"
});
eSendEvent(eCloseSheet);
}
await db.trash.delete(note.id);
useTrashStore.getState().refresh();
useSelectionStore.getState().setSelectionMode(false);
ToastManager.show({
heading: strings.noteDeleted(),
type: "success",
context: "local"
});
eSendEvent(eCloseSheet);
},
positiveType: "error"
});
@@ -139,11 +113,8 @@ export default function NotePreview({
}}
>
<Dialog context="local" />
<DialogHeader
padding={12}
title={content?.title || note.title || session?.session}
/>
{!session?.locked && !locked && content?.data ? (
<DialogHeader padding={12} title={note?.title || session?.session} />
{!session?.locked && !locked ? (
<View
style={{
flex: 1,
@@ -154,27 +125,17 @@ export default function NotePreview({
editorId="historyPreview"
onLoad={async (loadContent) => {
try {
if (content?.data) {
const currentContent = note?.contentId
? await db.content.get(note.contentId)
: undefined;
if (
currentContent?.data &&
!isEncryptedContent(currentContent)
) {
loadContent({
data: diff(
currentContent?.data || "<p></p>",
content.data as string
),
id: session?.noteId || note.id
});
}
if (content.data) {
const _note = note || (await db.notes.note(session?.noteId));
const currentContent = await db.content.get(_note.contentId);
loadContent({
data: diff(currentContent.data, content.data),
id: _note.id
});
}
} catch (e) {
ToastManager.error(
e as Error,
e,
"Failed to load history preview",
"local"
);
@@ -192,9 +153,7 @@ export default function NotePreview({
}}
>
<Paragraph color={colors.secondary.paragraph}>
{!content?.data
? strings.noContent()
: strings.encryptedNoteHistoryNotice()}
{strings.encryptedNoteHistoryNotice()}
</Paragraph>
</View>
)}

View File

@@ -994,7 +994,7 @@ const PricingPlanCard = ({
PremiumService.get() &&
(pricingPlans?.user?.subscription?.productId ===
(product as RNIap.Subscription)?.productId ||
pricingPlans?.user?.subscription?.productId?.startsWith(
pricingPlans?.user?.subscription?.productId.startsWith(
(product as RNIap.Subscription)?.productId
));
pricingPlans?.selectPlan(

View File

@@ -0,0 +1,72 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { View } from "react-native";
import { useThemeColors } from "@notesnook/theme";
import { AppFontSize } from "../../utils/size";
import Paragraph from "../ui/typography/paragraph";
import { getFormattedDate } from "@notesnook/common";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../utils/styles";
export const DateMeta = ({ item }) => {
const { colors } = useThemeColors();
function getDateMeta() {
let keys = Object.keys(item);
if (keys.includes("dateEdited"))
keys.splice(
keys.findIndex((k) => k === "dateModified"),
1
);
return keys.filter((key) => key.startsWith("date") && key !== "date");
}
const renderItem = (key) =>
!item[key] ? null : (
<View
key={key}
style={{
flexDirection: "row",
justifyContent: "space-between",
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL / 2
}}
>
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
{strings.dateDescFromKey(key)}
</Paragraph>
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
{getFormattedDate(item[key], "date-time")}
</Paragraph>
</View>
);
return (
<View
style={{
borderTopWidth: 1,
borderTopColor: colors.primary.border,
paddingHorizontal: DefaultAppStyles.GAP,
paddingTop: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
>
{getDateMeta().map(renderItem)}
</View>
);
};

View File

@@ -1,130 +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 React, { useState } from "react";
import { View } from "react-native";
import { useThemeColors } from "@notesnook/theme";
import { AppFontSize } from "../../utils/size";
import Paragraph from "../ui/typography/paragraph";
import { getFormattedDate } from "@notesnook/common";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../utils/styles";
import DateTimePickerModal from "react-native-modal-datetime-picker";
import { db } from "../../common/database";
import { Item, Note } from "@notesnook/core";
import AppIcon from "../ui/AppIcon";
export const DateMeta = ({ item }: { item: Item }) => {
const { colors, isDark } = useThemeColors();
const [isDatePickerVisible, setIsDatePickerVisible] = useState(false);
const [dateCreated, setDateCreated] = useState(item.dateCreated);
function getDateMeta() {
let keys = Object.keys(item);
if (keys.includes("dateEdited"))
keys.splice(
keys.findIndex((k) => k === "dateModified"),
1
);
return keys.filter((key) => key.startsWith("date") && key !== "date");
}
const renderItem = (key: string) =>
!item[key as keyof Item] ? null : (
<View
key={key}
style={{
flexDirection: "row",
justifyContent: "space-between",
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL / 2
}}
>
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
{strings.dateDescFromKey(
key as
| "dateDeleted"
| "dateEdited"
| "dateModified"
| "dateCreated"
| "dateUploaded"
)}
</Paragraph>
<Paragraph
size={AppFontSize.xs}
color={colors.secondary.paragraph}
onPress={
item.type !== "note"
? undefined
: () => {
setIsDatePickerVisible(true);
}
}
>
{getFormattedDate(
key === "dateCreated"
? dateCreated
: (item[key as keyof Item] as string),
"date-time"
)}
{key === "dateCreated" && item.type === "note" ? (
<>
{" "}
<AppIcon name="pencil" size={AppFontSize.md} />
</>
) : null}
</Paragraph>
</View>
);
return (
<>
{item.type === "note" ? (
<DateTimePickerModal
isVisible={isDatePickerVisible}
mode="datetime"
onConfirm={async (date: Date) => {
await db.notes.add({
id: item.id,
dateCreated: date.getTime()
});
setDateCreated(date.getTime());
setIsDatePickerVisible(false);
}}
onCancel={() => {
setIsDatePickerVisible(false);
}}
maximumDate={new Date((item as Note).dateEdited)}
isDarkModeEnabled={isDark}
is24Hour={db.settings.getTimeFormat() === "24-hour"}
date={new Date(dateCreated)}
/>
) : null}
<View
style={{
borderTopWidth: 1,
borderTopColor: colors.primary.border,
paddingHorizontal: DefaultAppStyles.GAP,
paddingTop: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
>
{getDateMeta().map(renderItem)}
</View>
</>
);
};

View File

@@ -39,7 +39,6 @@ import { DateMeta } from "./date-meta";
import { Items } from "./items";
import Notebooks from "./notebooks";
import { TagStrip, Tags } from "./tags";
import { Dialog } from "../dialog";
const Line = ({ top = 6, bottom = 6 }) => {
const { colors } = useThemeColors();
@@ -204,7 +203,6 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
/>
) : null}
<SheetProvider context="properties" />
<Dialog context="properties" />
</View>
)}
/>

View File

@@ -33,7 +33,6 @@ import AppIcon from "../ui/AppIcon";
import { Button } from "../ui/button";
import { Pressable } from "../ui/pressable";
import Paragraph from "../ui/typography/paragraph";
import { Dialog } from "../dialog";
const TOP_BAR_ITEMS: ActionId[] = [
"pin",
@@ -58,8 +57,6 @@ const BOTTOM_BAR_ITEMS: ActionId[] = [
"export",
"copy-link",
"duplicate",
"launcher-shortcut",
"expiry-date",
"trash"
];
@@ -79,7 +76,6 @@ const COLUMN_BAR_ITEMS: ActionId[] = [
"reorder",
"rename-color",
"rename-tag",
"launcher-shortcut",
"restore",
"trash",
"delete"
@@ -174,15 +170,15 @@ export const Items = ({
DDS.isTab
? AppFontSize.xxl
: shouldShrink
? AppFontSize.xxl
: AppFontSize.lg
? AppFontSize.xxl
: AppFontSize.lg
}
color={
item.checked
? item.activeColor || colors.primary.accent
: item.id.match(/(delete|trash)/g)
? colors.error.icon
: colors.secondary.icon
? colors.error.icon
: colors.secondary.icon
}
/>
</Pressable>
@@ -213,8 +209,8 @@ export const Items = ({
text: item.checked
? item.activeColor || colors.primary.accent
: item.id === "delete" || item.id === "trash"
? colors.error.paragraph
: colors.primary.paragraph
? colors.error.paragraph
: colors.primary.paragraph
}}
testID={"icon-" + item.id}
onPress={item.onPress}
@@ -278,8 +274,8 @@ export const Items = ({
item.checked
? item.activeColor || colors.primary.accent
: item.id === "delete" || item.id === "trash"
? colors.error.icon
: colors.secondary.icon
? colors.error.icon
: colors.secondary.icon
}
/>

View File

@@ -352,7 +352,7 @@ const ProductItem = (props: {
props.pricingPlans.isSubscribed() &&
(props.pricingPlans.user?.subscription?.productId ===
(product as RNIap.Subscription)?.productId ||
props.pricingPlans.user?.subscription?.productId?.startsWith(
props.pricingPlans.user?.subscription?.productId.startsWith(
(product as RNIap.Subscription)?.productId
) ||
props.pricingPlans.user?.subscription?.productId ===

View File

@@ -103,7 +103,7 @@ export function PlanLimits() {
if (
user?.subscription.plan !== SubscriptionPlan.FREE &&
user?.subscription.productId?.includes("5year")
user?.subscription.productId.includes("5year")
) {
ToastManager.show({
message:

View File

@@ -78,15 +78,12 @@ const PublishNoteSheet = ({
const isFeatureAvailable = useIsFeatureAvailable("monographAnalytics");
const [isLocked, setIsLocked] = useState(false);
const [publishing, setPublishing] = useState(false);
const customTitle = useRef<string>("");
const pwdInput = useRef<TextInput>(null);
const titleInput = useRef<TextInput>(null);
const passwordValue = useRef<string>(undefined);
const monographData = useAsync(async () => {
return fetchMonographData(note?.id);
}, []);
const monograph = monographData.result?.monograph;
customTitle.current = monograph?.title || note.title || "";
const publishUrl = monograph && `${hosts.MONOGRAPH_HOST}/${monograph?.id}`;
const isPublished = db.monographs.monograph(note?.id);
@@ -111,7 +108,7 @@ const PublishNoteSheet = ({
try {
if (note?.id) {
if (isLocked && !passwordValue.current) return;
await db.monographs.publish(note.id, customTitle.current, {
await db.monographs.publish(note.id, {
selfDestruct: selfDestruct,
password: isLocked ? passwordValue.current : undefined
});
@@ -132,6 +129,7 @@ const PublishNoteSheet = ({
setPublishLoading(false);
};
const setPublishLoading = (value: boolean) => {
setPublishing(value);
};
@@ -247,13 +245,6 @@ const PublishNoteSheet = ({
</TouchableOpacity>
) : null}
<Input
fwdRef={titleInput}
onChangeText={(value) => (customTitle.current = value)}
defaultValue={customTitle.current}
placeholder={strings.noteTitle()}
/>
<TouchableOpacity
onPress={() => {
if (publishing) return;

View File

@@ -30,7 +30,6 @@ import ReorderableList from "../list/reorderable-list";
import { Properties } from "../properties";
import { useSideBarDraggingStore } from "./dragging-store";
import { MenuItem } from "./menu-item";
import { Default_Drag_Action } from "../../hooks/use-actions";
export const ColorSection = React.memo(
function ColorSection() {
@@ -60,7 +59,7 @@ export const ColorSection = React.memo(
const onLongPress = React.useCallback((item: SideMenuItem) => {
if (useSideBarDraggingStore.getState().dragging) return;
Properties.present(item.data as Color, false, [Default_Drag_Action]);
Properties.present(item.data as Color);
}, []);
const renderIcon = React.useCallback((item: SideMenuItem, size: number) => {

View File

@@ -21,6 +21,15 @@ import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import {
NavigationState,
Route,
SceneMap,
SceneRendererProps,
TabDescriptor,
TabView
} from "react-native-tab-view";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import { useGroupOptions } from "../../hooks/use-group-options";
@@ -49,105 +58,7 @@ import { Button } from "../ui/button";
import SettingsService from "../../services/settings";
import { isFeatureAvailable } from "@notesnook/common";
import PaywallSheet from "../sheets/paywall";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
/**
* Simple Tab View Implementation for the Side bar
*/
type SimpleRoute = {
key: string;
title?: string;
};
type SimpleNavigationState = {
index: number;
routes: SimpleRoute[];
};
type SimpleTabBarProps = {
navigationState: SimpleNavigationState;
jumpTo: (key: string) => void;
};
type SimpleTabViewProps = {
navigationState: SimpleNavigationState;
renderScene: ({ route }: { route: SimpleRoute }) => React.ReactNode;
renderTabBar?: (props: SimpleTabBarProps) => React.ReactNode;
onIndexChange?: (index: number) => void;
};
const createSceneMap = (
scenes: Record<string, React.ComponentType<any>>
): ((props: { route: SimpleRoute }) => React.ReactNode) => {
return ({ route }: { route: SimpleRoute }) => {
const SceneComponent = scenes[route.key];
if (!SceneComponent) return null;
return <SceneComponent />;
};
};
const SimpleTabView = ({
navigationState,
renderScene,
renderTabBar,
onIndexChange
}: SimpleTabViewProps) => {
const loadedKeysRef = React.useRef(new Set<string>());
const scenesRef = React.useRef(new Map<string, React.ReactNode>());
const jumpTo = React.useCallback(
(key: string) => {
const nextIndex = navigationState.routes.findIndex(
(route) => route.key === key
);
if (nextIndex !== -1 && nextIndex !== navigationState.index) {
onIndexChange?.(nextIndex);
}
},
[navigationState.index, navigationState.routes, onIndexChange]
);
const activeKey = navigationState.routes[navigationState.index]?.key;
if (activeKey && !loadedKeysRef.current.has(activeKey)) {
loadedKeysRef.current.add(activeKey);
}
const getSceneForRoute = React.useCallback(
(route: SimpleRoute) => {
const cached = scenesRef.current.get(route.key);
if (cached) return cached;
const created = renderScene({ route });
scenesRef.current.set(route.key, created);
return created;
},
[renderScene]
);
return (
<View style={{ flex: 1 }}>
<View style={{ flex: 1 }}>
{navigationState.routes.map((route, routeIndex) => (
<View
key={route.key}
style={{
flex: 1,
display: navigationState.index === routeIndex ? "flex" : "none"
}}
>
{loadedKeysRef.current.has(route.key)
? getSceneForRoute(route)
: navigationState.index === routeIndex
? getSceneForRoute(route)
: null}
</View>
))}
</View>
{renderTabBar ? renderTabBar({ navigationState, jumpTo }) : null}
</View>
);
};
const renderScene = createSceneMap({
const renderScene = SceneMap({
home: SideMenuHome,
notebooks: SideMenuNotebooks,
tags: SideMenuTags,
@@ -157,11 +68,10 @@ const renderScene = createSceneMap({
export const SideMenu = React.memo(
function SideMenu() {
const { colors } = useThemeColors();
const insets = useGlobalSafeAreaInsets();
const [index, setIndex] = React.useState(
SettingsService.getProperty("defaultSidebarTab")
);
const [routes] = React.useState<SimpleRoute[]>([
const [routes] = React.useState<Route[]>([
{
key: "home",
title: "Home"
@@ -177,28 +87,34 @@ export const SideMenu = React.memo(
]);
return (
<View
<SafeAreaView
style={{
flex: 1,
backgroundColor: colors.primary.background,
paddingTop: insets.top,
paddingBottom: insets.bottom,
paddingLeft: insets.left
backgroundColor: colors.primary.background
}}
>
<SimpleTabView
<TabView
navigationState={{ index, routes }}
renderTabBar={(props) => <TabBar {...props} />}
tabBarPosition="bottom"
renderScene={renderScene}
onIndexChange={setIndex}
swipeEnabled={false}
animationEnabled={false}
lazy
/>
</View>
</SafeAreaView>
);
},
() => true
);
const TabBar = (props: SimpleTabBarProps) => {
const TabBar = (
props: SceneRendererProps & {
navigationState: NavigationState<Route>;
options: Record<string, TabDescriptor<Route>> | undefined;
}
) => {
const dragging = useSideBarDraggingStore((state) => state.dragging);
const { colors, isDark } = useThemeColors();
const groupOptions = useGroupOptions(
@@ -435,8 +351,9 @@ const TabBar = (props: SimpleTabBarProps) => {
color={colors.primary.icon}
onPress={async () => {
if (props.navigationState.index === 1) {
const notebooksFeature =
await isFeatureAvailable("notebooks");
const notebooksFeature = await isFeatureAvailable(
"notebooks"
);
if (!notebooksFeature.isAllowed) {
PaywallSheet.present(notebooksFeature);
return;

View File

@@ -37,7 +37,7 @@ import { Pressable } from "../ui/pressable";
import Paragraph from "../ui/typography/paragraph";
import { useSideBarDraggingStore } from "./dragging-store";
export function MenuItem({
function _MenuItem({
item,
index,
testID,
@@ -104,12 +104,15 @@ export function MenuItem({
const _onPress = () => {
if (useSideBarDraggingStore.getState().dragging) return;
if (item.onPress) return item.onPress(item);
Navigation.closeDrawer();
if (useNavigationStore.getState().currentRoute !== item.id) {
Navigation.navigate(item.id as keyof RouteParams, {
canGoBack: false
});
}
setImmediate(() => {
Navigation.closeDrawer();
});
};
return (
@@ -151,8 +154,8 @@ export function MenuItem({
item.icon === "crown"
? colors.static.yellow
: isFocused
? colors.selected.icon
: colors.secondary.icon
? colors.selected.icon
: colors.secondary.icon
}
size={AppFontSize.md}
/>
@@ -179,3 +182,12 @@ export function MenuItem({
</Pressable>
);
}
export const MenuItem = React.memo(_MenuItem, (prev, next) => {
if (
prev.item.id !== next.item.id &&
prev.item.data?.dateModified !== next.item.data?.dateModified
)
return false;
return true;
});

View File

@@ -31,9 +31,6 @@ import ReorderableList from "../list/reorderable-list";
import { MenuItem } from "./menu-item";
import { useThemeColors } from "@notesnook/theme";
import { DefaultAppStyles } from "../../utils/styles";
import { useSideBarDraggingStore } from "./dragging-store";
import { Properties } from "../properties";
import { Default_Drag_Action } from "../../hooks/use-actions";
export const PinnedSection = React.memo(
function PinnedSection() {
@@ -60,11 +57,6 @@ export const PinnedSection = React.memo(
});
}, []);
const onLongPress = React.useCallback((item: SideMenuItem) => {
if (useSideBarDraggingStore.getState().dragging) return;
Properties.present(item.data as Notebook, false, [Default_Drag_Action]);
}, []);
const menuItems = useMemo(
() =>
menuPins.map((item) => ({
@@ -73,8 +65,7 @@ export const PinnedSection = React.memo(
icon: item.type === "notebook" ? "notebook-outline" : "pound",
dataType: item.type,
data: item,
onPress: onPress,
onLongPress: onLongPress
onPress: onPress
})) as SideMenuItem[],
[menuPins, onPress]
);
@@ -107,7 +98,6 @@ export const PinnedSection = React.memo(
flexGrow: 1,
width: "100%"
}}
disableDefaultDrag
contentContainerStyle={{
flexGrow: 1
}}

View File

@@ -19,7 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { SubscriptionPlan } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import dayjs from "dayjs";
import React from "react";
import { FlatList, View } from "react-native";
import { DraxProvider, DraxScrollView } from "react-native-drax";
@@ -85,7 +84,6 @@ export function SideMenuHome() {
keyboardDismissMode="interactive"
keyboardShouldPersistTaps="handled"
keyExtractor={() => "scroll-items"}
bounces={false}
renderItem={() => (
<>
<ReorderableList
@@ -142,35 +140,19 @@ export function SideMenuHome() {
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
>
{dayjs().month() !== 11 ? (
<>
{(subscriptionType === SubscriptionPlan.FREE ||
!subscriptionType ||
!user) &&
!SettingsService.getProperty("serverUrls") ? (
<Button
title={pro.title}
style={{
width: "100%"
}}
type="accent"
onPress={pro.onPress}
/>
) : null}
</>
) : (
{(subscriptionType === SubscriptionPlan.FREE ||
!subscriptionType ||
!user) &&
!SettingsService.getProperty("serverUrls") ? (
<Button
title={`Wrapped ${dayjs().year()} 🎉`}
title={pro.title}
style={{
width: "100%"
}}
bold
type="secondaryAccented"
onPress={() => {
Navigation.navigate("Wrapped");
}}
type="accent"
onPress={pro.onPress}
/>
)}
) : null}
</View>
</View>
);

View File

@@ -42,7 +42,7 @@ const SheetWrapper = ({
closeOnTouchBackdrop = true,
onHasReachedTop,
overlay,
overlayOpacity = 0.7,
overlayOpacity = 0.3,
enableGesturesInScrollView = false,
bottomPadding = true,
keyboardHandlerDisabled

View File

@@ -17,14 +17,12 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable no-inner-declarations */
import { isFeatureAvailable, useAreFeaturesAvailable } from "@notesnook/common";
import { useAreFeaturesAvailable } from "@notesnook/common";
import {
Color,
createInternalLink,
Item,
ItemReference,
Note,
Notebook,
VAULT_ERRORS
} from "@notesnook/core";
import { strings } from "@notesnook/intl";
@@ -32,7 +30,7 @@ import { useThemeColors } from "@notesnook/theme";
import { DisplayedNotification } from "@notifee/react-native";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { useEffect, useRef, useState } from "react";
import { InteractionManager, Platform, View } from "react-native";
import { InteractionManager, Platform } from "react-native";
import Share from "react-native-share";
import { DatabaseLogger, db } from "../common/database";
import { AttachmentDialog } from "../components/attachments";
@@ -67,13 +65,11 @@ import { useSelectionStore } from "../stores/use-selection-store";
import { useSettingStore } from "../stores/use-setting-store";
import { useTagStore } from "../stores/use-tag-store";
import { useUserStore } from "../stores/use-user-store";
import { eCloseSheet, eUpdateNoteInEditor } from "../utils/events";
import { eUpdateNoteInEditor } from "../utils/events";
import { deleteItems } from "../utils/functions";
import { convertNoteToText } from "../utils/note-to-text";
import { sleep } from "../utils/time";
import { NotesnookModule } from "../utils/notesnook-module";
import DatePickerComponent from "../components/date-picker";
import { resetStoredState } from "./use-stored-state";
export type ActionId =
| "select"
@@ -118,9 +114,7 @@ export type ActionId =
| "remove-from-notebook"
| "trash"
| "default-homepage"
| "default-tag"
| "launcher-shortcut"
| "expiry-date";
| "default-tag";
export type Action = {
id: ActionId;
@@ -136,31 +130,6 @@ export type Action = {
locked?: boolean;
};
export const Default_Drag_Action: Action = {
id: "reorder",
title: strings.reorder(),
icon: "sort-ascending",
onPress: async () => {
const feature = await isFeatureAvailable("customizableSidebar");
if (feature && !feature.isAllowed) {
ToastManager.show({
message: feature.error,
type: "info",
context: "local",
actionText: strings.upgrade(),
func: () => {
PaywallSheet.present(feature);
}
});
return;
}
useSideBarDraggingStore.setState({
dragging: true
});
eSendEvent(eCloseSheet);
}
};
function isNotePinnedInNotifications(item: Item) {
const pinned = Notifications.getPinnedNotes();
if (!pinned || pinned.length === 0) {
@@ -189,12 +158,10 @@ export const useActions = ({
"shortcuts",
"notebooks",
"customizableSidebar",
"customHomepage",
"androidLauncherShortcuts",
"expiringNotes"
"customHomepage"
]);
const [item, setItem] = useState(propItem);
const { colors, isDark } = useThemeColors();
const { colors } = useThemeColors();
const setMenuPins = useMenuStore((state) => state.setMenuPins);
const [isPinnedToMenu, setIsPinnedToMenu] = useState(
db.shortcuts.exists(item.id)
@@ -330,6 +297,7 @@ export const useActions = ({
id: item.id,
title: value
});
eSendEvent(Navigation.routeNames.TaggedNotes);
InteractionManager.runAfterInteractions(() => {
useTagStore.getState().refresh();
@@ -467,6 +435,31 @@ export const useActions = ({
icon: "square-edit-outline",
onPress: renameColor
});
actions.push({
id: "reorder",
title: strings.reorder(),
icon: "sort-ascending",
onPress: async () => {
if (features && !features.customizableSidebar.isAllowed) {
ToastManager.show({
message: features.customizableSidebar.error,
type: "info",
context: "local",
actionText: strings.upgrade(),
func: () => {
PaywallSheet.present(features.customizableSidebar);
}
});
return;
}
useSideBarDraggingStore.setState({
dragging: true
});
close();
},
locked: !features?.customizableSidebar.isAllowed
});
}
if (item.type === "reminder") {
@@ -713,6 +706,26 @@ export const useActions = ({
type: item.type
}
);
resetStoredState(
"app-home-navigtion-key",
isHomepage
? undefined
: {
name:
item.type === "notebook"
? "Notebook"
: item.type === "tag"
? "TaggedNotes"
: item.type === "color"
? "ColorNotes"
: undefined,
params: {
item: item,
id: item.id
}
}
);
}
});
}
@@ -952,7 +965,7 @@ export const useActions = ({
copyNote: true,
novault: true,
locked: true,
item: item as Note,
item: item,
title: strings.copyNote()
});
} else {
@@ -1164,45 +1177,6 @@ export const useActions = ({
},
checked: item.archived,
isToggle: true
},
{
id: "expiry-date",
title: item.expiryDate ? strings.unsetExpiry() : strings.setExpiry(),
icon: item.expiryDate ? "bomb-off" : "bomb",
locked: !features?.expiringNotes?.isAllowed,
onPress: async () => {
if (item.expiryDate) {
await db.notes.setExpiryDate(null, item.id);
setItem((await db.notes.note(item.id)) as Item);
} else {
if (features && !features?.expiringNotes.isAllowed) {
ToastManager.show({
message: features?.expiringNotes.error,
type: "info",
actionText: strings.upgrade(),
context: "local",
func: () => {
PaywallSheet.present(features?.expiringNotes);
}
});
return;
}
presentDialog({
context: "properties",
component: (close) => (
<DatePickerComponent
onCancel={() => close?.()}
onConfirm={async (date) => {
close?.();
await db.notes.setExpiryDate(date.getTime(), item.id);
setItem((await db.notes.note(item.id)) as Item);
}}
/>
)
});
}
}
}
);
@@ -1233,44 +1207,5 @@ export const useActions = ({
});
}
if (
Platform.OS === "android" &&
(item.type === "tag" ||
item.type === "note" ||
item.type === "notebook" ||
item.type === "color")
) {
actions.push({
id: "launcher-shortcut",
title: strings.addToHome(),
icon: "cellphone-arrow-down",
locked: !features?.androidLauncherShortcuts.isAllowed,
onPress: async () => {
if (features && !features?.androidLauncherShortcuts.isAllowed) {
ToastManager.show({
message: features?.androidLauncherShortcuts.error,
type: "info",
actionText: strings.upgrade(),
context: "local",
func: () => {
PaywallSheet.present(features?.androidLauncherShortcuts);
}
});
return;
}
try {
await NotesnookModule.addShortcut(
item.id,
item.type,
item.title,
(item as Note).headline || (item as Notebook).description || "",
(item as Color).colorCode
);
} catch (e) {}
}
});
}
return actions;
};

View File

@@ -85,7 +85,7 @@ import Notifications from "../services/notifications";
import PremiumService from "../services/premium";
import SettingsService from "../services/settings";
import Sync from "../services/sync";
import { clearAllStores, initAfterSync } from "../stores";
import { clearAllStores, initAfterSync, initialize } from "../stores";
import { refreshAllStores } from "../stores/create-db-collection-store";
import { useAttachmentStore } from "../stores/use-attachment-store";
import { useMessageStore } from "../stores/use-message-store";
@@ -107,9 +107,6 @@ import { fluidTabsRef } from "../utils/global-refs";
import { NotesnookModule } from "../utils/notesnook-module";
import { sleep } from "../utils/time";
import useFeatureManager from "./use-feature-manager";
import { deleteDCacheFiles } from "../common/filesystem/io";
import dayjs from "dayjs";
import { useRelationStore } from "../stores/use-relation-store";
const onCheckSyncStatus = async (type: SyncStatusEvent) => {
const { disableSync, disableAutoSync } = SettingsService.get();
@@ -162,10 +159,7 @@ const onUserSessionExpired = async () => {
eSendEvent(eLoginSessionExpired);
};
const onAppOpenedFromURL = async (event: {
url: string;
isInitialUrl?: boolean;
}) => {
const onAppOpenedFromURL = async (event: { url: string }) => {
const url = event.url;
try {
@@ -177,65 +171,17 @@ const onAppOpenedFromURL = async (event: {
eSendEvent(eOnLoadNote, { newNote: true });
fluidTabsRef.current?.goToPage("editor", false);
return;
} else if (url.startsWith("https://app.notesnook.com/open_note?")) {
} else if (url.startsWith("https://app.notesnook.com/open_note")) {
const id = new URL(url).searchParams.get("id");
if (id) {
const note = await db.notes.note(id);
if (note) {
editorState().initialLoadCalled = true;
eSendEvent(eOnLoadNote, {
item: note
});
fluidTabsRef.current?.goToPage("editor", false);
}
}
} else if (
url.startsWith("https://app.notesnook.com/open_notebook?") &&
!event.isInitialUrl
) {
const id = new URL(url).searchParams.get("id");
if (id) {
const notebook = await db.notebooks.notebook(id);
if (notebook) {
Navigation.navigate("Notebook", {
id: notebook.id,
canGoBack: true,
item: notebook
});
}
}
} else if (
url.startsWith("https://app.notesnook.com/open_tag?") &&
!event.isInitialUrl
) {
const id = new URL(url).searchParams.get("id");
if (id) {
const tag = await db.tags.tag(id);
if (tag) {
Navigation.navigate("TaggedNotes", {
type: "tag",
id: tag.id,
item: tag,
canGoBack: true
});
}
}
} else if (
url.startsWith("https://app.notesnook.com/open_color?") &&
!event.isInitialUrl
) {
const id = new URL(url).searchParams.get("id");
if (id) {
const color = await db.colors.color(id);
if (color) {
Navigation.navigate("ColoredNotes", {
type: "color",
id: color.id,
item: color,
canGoBack: true
});
}
}
} else if (url.startsWith("https://app.notesnook.com/open_reminder")) {
const id = new URL(url).searchParams.get("id");
if (id) {
@@ -488,28 +434,6 @@ const IsDatabaseMigrationRequired = () => {
return true;
};
let timer: NodeJS.Timeout | null = null;
let initialDate = -1;
async function expiringNotesTimer() {
if (timer != null) clearInterval(timer);
if (initialDate === -1) {
DatabaseLogger.info("Deleting expired notes at startup");
db.notes.deleteExpiredNotes();
initialDate = dayjs().date();
}
timer = setInterval(() => {
if (dayjs().date() != initialDate) {
DatabaseLogger.info("Deleting expired notes");
db.notes.deleteExpiredNotes();
Navigation.queueRoutesForUpdate();
useRelationStore.getState().update();
initialDate = dayjs().date();
}
}, 1000 * 60);
}
const initializeDatabase = async (password?: string) => {
if (useUserStore.getState().appLocked) return;
if (!db.isInitialized) {
@@ -529,21 +453,18 @@ const initializeDatabase = async (password?: string) => {
if (db.isInitialized) {
await setAppMessage();
useSettingStore.getState().setAppLoading(false);
useSettingStore.getState().refresh();
Notifications.setupReminders(true);
if (SettingsService.get().notifNotes) {
Notifications.pinQuickNote(false);
}
DatabaseLogger.info("Database initialized");
Notifications.restorePinnedNotes();
expiringNotesTimer();
deleteDCacheFiles();
}
Walkthrough.init();
};
export const useAppEvents = () => {
const [isAppLoading, initialUrl] = useSettingStore((state) => [
state.isAppLoading,
state.initialUrl
]);
const isAppLoading = useSettingStore((state) => state.isAppLoading);
const [setLastSynced, setUser, appLocked, syncing] = useUserStore((state) => [
state.setLastSynced,
state.setUser,
@@ -588,15 +509,6 @@ export const useAppEvents = () => {
};
}, [isAppLoading, onSyncComplete]);
useEffect(() => {
if (initialUrl) {
onAppOpenedFromURL({
url: initialUrl!,
isInitialUrl: true
});
}
}, [initialUrl]);
const subscribeToPurchaseListeners = useCallback(async () => {
if (Platform.OS === "android") {
try {
@@ -725,10 +637,10 @@ export const useAppEvents = () => {
EV.subscribe(EVENTS.syncCheckStatus, onCheckSyncStatus),
EV.subscribe(EVENTS.syncAborted, onSyncAborted),
EV.subscribe(EVENTS.appRefreshRequested, onSyncComplete),
db.eventManager.subscribe(EVENTS.userLoggedOut, onLogout),
db.eventManager.subscribe(EVENTS.userEmailConfirmed, onUserEmailVerified),
EV.subscribe(EVENTS.userLoggedOut, onLogout),
EV.subscribe(EVENTS.userEmailConfirmed, onUserEmailVerified),
EV.subscribe(EVENTS.userSessionExpired, onUserSessionExpired),
db.eventManager.subscribe(
EV.subscribe(
EVENTS.userSubscriptionUpdated,
onUserSubscriptionStatusChanged
),
@@ -812,7 +724,6 @@ export const useAppEvents = () => {
if (state === "active") {
notifee.setBadgeCount(0);
updateStatusBarColor();
expiringNotesTimer();
checkAutoBackup();
Sync.run("global", false, "full");
reconnectSSE();
@@ -863,10 +774,23 @@ export const useAppEvents = () => {
}
};
if (!refValues.current.initialUrl) {
Linking.getInitialURL().then((url) => {
if (url) {
refValues.current.initialUrl = url;
}
});
}
let sub: NativeEventSubscription;
if (!isAppLoading && !appLocked) {
setTimeout(() => {
sub = AppState.addEventListener("change", onAppStateChanged);
if (refValues.current.initialUrl) {
onAppOpenedFromURL({
url: refValues.current.initialUrl!
});
refValues.current.initialUrl = undefined;
}
}, 1000);
refValues.current.removeInternetStateListener = NetInfo.addEventListener(

View File

@@ -660,25 +660,9 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
}
}
function isSubscribedToPlan(planId: string) {
if (!PremiumService.get()) return false;
return user?.subscription?.productId?.includes(planId);
}
function isSubscribedToProduct(productId: string) {
if (!PremiumService.get()) return false;
return (
user?.subscription?.productId &&
(user?.subscription?.productId === productId ||
user?.subscription?.productId?.startsWith(productId))
);
}
return {
currentPlan: pricingPlans.find((p) => p.id === currentPlan),
pricingPlans: plans,
isSubscribedToPlan,
isSubscribedToProduct,
getStandardPrice: getLocalizedPrice,
loadingPlans,
loading,

View File

@@ -29,7 +29,7 @@ import React, {
useRef,
useState
} from "react";
import { LayoutChangeEvent, View } from "react-native";
import { Dimensions, LayoutChangeEvent, Platform, View } from "react-native";
import Orientation, {
OrientationType,
useDeviceOrientationChange
@@ -39,6 +39,7 @@ import Animated, {
useSharedValue,
withTiming
} from "react-native-reanimated";
import { SafeAreaView } from "react-native-safe-area-context";
import { notesnook } from "../../e2e/test.ids";
import { db } from "../common/database";
import { FluidPanels } from "../components/fluid-panels";
@@ -68,13 +69,15 @@ import {
eOpenFullscreenEditor,
eUnlockNote
} from "../utils/events";
import { valueLimiter } from "../utils/functions";
import { fluidTabsRef } from "../utils/global-refs";
import { editorRef, fluidTabsRef } from "../utils/global-refs";
import { AppNavigationStack } from "./navigation-stack";
import type { PaneWidths } from "../screens/editor/wrapper";
const MOBILE_SIDEBAR_SIZE = 0.85;
const valueLimiter = (value: number, min: number, max: number) => {
return value < min ? min : value > max ? max : value;
};
let SideMenu: any = null;
let EditorWrapper: any = null;
@@ -90,6 +93,7 @@ export const FluidPanelsView = React.memo(
const insets = useGlobalSafeAreaInsets();
const animatedOpacity = useSharedValue(0);
const animatedTranslateY = useSharedValue(-9999);
const overlayRef = useRef<Animated.View>(null);
const [orientation, setOrientation] = useState<OrientationType>(
Orientation.getInitialOrientation()
@@ -98,21 +102,16 @@ export const FluidPanelsView = React.memo(
const [isLoading, setIsLoading] = useState(false);
useDeviceOrientationChange((o) => {
if (
o !== OrientationType.UNKNOWN &&
o !== OrientationType["FACE-UP"] &&
o !== OrientationType["FACE-DOWN"] &&
o !== OrientationType["PORTRAIT-UPSIDEDOWN"]
) {
setOrientation(o);
}
setOrientation(o);
});
const isLandscape = orientation.includes("LANDSCAPE");
useEffect(() => {
if (!appLoading) {
setTimeout(() => {
setIsLoading(false);
}, 200);
}, 500);
}
}, [appLoading]);
@@ -148,6 +147,16 @@ export const FluidPanelsView = React.memo(
if (deviceMode === "smallTablet") {
fluidTabsRef.current?.openDrawer(false);
}
editorRef.current?.setNativeProps({
style: {
width: dimensions.width,
zIndex: 999,
paddingHorizontal:
deviceMode === "smallTablet"
? dimensions.width * 0
: dimensions.width * 0.15
}
});
}, [deviceMode, dimensions.width, setFullscreen]);
const closeFullScreenEditor = useCallback(
@@ -160,6 +169,17 @@ export const FluidPanelsView = React.memo(
editorController.current?.commands.updateSettings({
fullscreen: false
});
editorRef.current?.setNativeProps({
style: {
width:
_deviceMode === "smallTablet"
? dimensions.width -
valueLimiter(dimensions.width * 0.4, 300, 450)
: dimensions.width * 0.48,
zIndex: null,
paddingHorizontal: 0
}
});
if (_deviceMode === "smallTablet") {
fluidTabsRef.current?.goToIndex(1, false);
}
@@ -201,8 +221,38 @@ export const FluidPanelsView = React.memo(
(current: string | null, size: { width: number; height: number }) => {
setDeviceModeState(current);
if (fullscreen && current === "mobile") {
eSendEvent(eCloseFullscreenEditor, current);
const needsLayout = current !== deviceMode;
if (fullscreen && current !== "mobile") {
// Runs after size is set via state.
setTimeout(() => {
editorRef.current?.setNativeProps({
style: {
width: size.width,
zIndex: 999,
paddingHorizontal:
current === "smallTablet" ? size.width * 0 : size.width * 0.15
}
});
}, 1);
} else {
if (fullscreen) eSendEvent(eCloseFullscreenEditor, current);
editorRef.current?.setNativeProps({
style: {
position: "relative",
width:
current === "tablet"
? size.width * 0.48
: current === "smallTablet"
? size.width - valueLimiter(size.width * 0.4, 300, 450)
: size.width,
zIndex: null,
paddingHorizontal: 0
}
});
}
if (!needsLayout) {
return;
}
const state = getAppState();
@@ -214,6 +264,8 @@ export const FluidPanelsView = React.memo(
case "smallTablet":
if (!fullscreen) {
fluidTabsRef.current?.closeDrawer(false);
} else {
fluidTabsRef.current?.openDrawer(false);
}
break;
case "mobile":
@@ -222,52 +274,46 @@ export const FluidPanelsView = React.memo(
editorState().movedAway === false &&
useTabStore.getState().getCurrentNoteId()
) {
fluidTabsRef.current?.goToPage("editor", false);
fluidTabsRef.current?.goToIndex(2, false);
} else {
fluidTabsRef.current?.goToPage(
fluidTabsRef.current?.page(),
false
);
fluidTabsRef.current?.goToIndex(1, false);
}
break;
}
}, 0);
}, 32);
},
[deviceMode, fullscreen, setDeviceModeState]
);
const checkDeviceType = React.useCallback(
(size: { width: number; height: number }) => {
if (DDS.width === size.width && orientation === DDS.orientation) return;
setDimensions({
width: size.width,
height: size.height
});
DDS.setSize(size, orientation);
const nextDeviceMode = DDS.isLargeTablet()
? "tablet"
: DDS.isSmallTab
? "smallTablet"
: "mobile";
setDeviceMode(nextDeviceMode, size);
},
[orientation, setDeviceMode, setDimensions]
);
useEffect(() => {
if (orientation !== "UNKNOWN") {
checkDeviceType(dimensions);
}
}, [orientation, dimensions]);
const _onLayout = React.useCallback(
(event: LayoutChangeEvent) => {
const size = event?.nativeEvent?.layout;
setDimensions({
width: size.width,
height: size.height
});
if (size.width > size.height) {
setOrientation(OrientationType["LANDSCAPE-RIGHT"]);
} else {
setOrientation(OrientationType["PORTRAIT"]);
if (!size || (size.width === dimensions.width && deviceMode !== null)) {
DDS.setSize(size, orientation);
setDeviceMode(deviceMode, size);
checkDeviceType(size);
return;
}
checkDeviceType(size);
},
[
checkDeviceType,
@@ -278,6 +324,11 @@ export const FluidPanelsView = React.memo(
]
);
if (!deviceMode) {
const size = Dimensions.get("window");
checkDeviceType(size);
}
const PANE_OFFSET = useMemo(
() => ({
mobile: {
@@ -305,7 +356,7 @@ export const FluidPanelsView = React.memo(
[dimensions.width, fullscreen]
);
const PANE_WIDTHS: PaneWidths = useMemo(
const PANE_WIDTHS = useMemo(
() => ({
mobile: {
sidebar: dimensions.width * MOBILE_SIDEBAR_SIZE,
@@ -331,7 +382,6 @@ export const FluidPanelsView = React.memo(
(scrollOffset: number) => {
if (!deviceMode) return;
hideAllTooltips();
if (
scrollOffset >
PANE_OFFSET[deviceMode as keyof typeof PANE_OFFSET].sidebar - 10
@@ -359,7 +409,7 @@ export const FluidPanelsView = React.memo(
};
}, []);
if (!isLoading && !SideMenu && !EditorWrapper) {
if (!isLoading) {
SideMenu = require("../components/side-menu").SideMenu;
EditorWrapper = require("../screens/editor/wrapper").EditorWrapper;
}
@@ -371,7 +421,15 @@ export const FluidPanelsView = React.memo(
style={{
height: "100%",
width: "100%",
backgroundColor: colors.primary.background
backgroundColor: colors.primary.background,
marginRight:
orientation === "LANDSCAPE-RIGHT" && Platform.OS === "ios"
? insets.right
: 0,
marginLeft:
orientation === "LANDSCAPE-LEFT" && Platform.OS === "ios"
? insets.left
: 0
}}
>
{deviceMode && PANE_WIDTHS[deviceMode as keyof typeof PANE_WIDTHS] ? (
@@ -445,7 +503,9 @@ export const FluidPanelsView = React.memo(
style={{
flex: 1,
paddingTop: insets.top,
paddingBottom: insets.bottom
paddingBottom: insets.bottom,
paddingLeft: insets.left,
paddingRight: insets.right
}}
>
<AppNavigationStack />

View File

@@ -31,6 +31,8 @@ import { useSettingStore } from "../stores/use-setting-store";
import { rootNavigatorRef } from "../utils/global-refs";
import Navigation from "../services/navigation";
import { isFeatureAvailable } from "@notesnook/common";
import { useStoredValue } from "../hooks/use-stored-state";
import { db } from "../common/database";
const RootStack = createNativeStackNavigator();
const AppStack = createNativeStackNavigator();
@@ -56,117 +58,105 @@ const AppNavigation = React.memo(
() => {
const { colors } = useThemeColors();
const homepageV2 = useSettingStore((state) => state.settings.homepageV2);
const home = useStoredValue(
"app-home-navigtion-key",
!homepageV2 || homepageV2.id === DEFAULT_HOME.name
? DEFAULT_HOME
: undefined
);
const loading = useSettingStore((state) => state.isAppLoading);
const [home, setHome] = React.useState<
{ name: string; params: any } | undefined
>(undefined);
React.useEffect(() => {
if (!home) {
if (useSettingStore.getState().initialUrl) {
const url = useSettingStore.getState().initialUrl;
if (url?.startsWith("https://app.notesnook.com/open_notebook?")) {
const id = new URL(url).searchParams.get("id");
if (id) {
setHome({
name: "Notebook",
params: {
id: id
}
});
return;
}
} else if (url?.startsWith("https://app.notesnook.com/open_tag?")) {
const id = new URL(url).searchParams.get("id");
if (id) {
setHome({
name: "TaggedNotes",
params: {
type: "tag",
id: id
}
});
return;
}
} else if (url?.startsWith("https://app.notesnook.com/open_color?")) {
const id = new URL(url).searchParams.get("id");
if (id) {
setHome({
name: "ColoredNotes",
params: {
type: "color",
id: id
}
});
return;
}
}
}
if (homepageV2) {
switch (homepageV2.type) {
case "notebook": {
setHome({
name: "Notebook",
params: {
id: homepageV2.id
}
});
return;
}
case "color": {
setHome({
name: "ColoredNotes",
params: {
type: "color",
id: homepageV2.id
}
});
return;
}
case "tag": {
setHome({
name: "TaggedNotes",
params: {
type: "tag",
id: homepageV2.id
}
});
return;
}
case "default":
setHome({
name: homepageV2.id,
params: undefined
});
return;
}
} else {
setHome(DEFAULT_HOME);
}
}
}, []);
if (!home.value && !homepageV2) {
home.value = DEFAULT_HOME;
}
React.useEffect(() => {
if (!homepageV2 || loading) return;
isFeatureAvailable("customHomepage").then((value) => {
if (!value.isAllowed) {
SettingsService.setProperty("homepageV2", undefined);
(async () => {
isFeatureAvailable("customHomepage").then((value) => {
if (!value.isAllowed) {
home.value = DEFAULT_HOME;
SettingsService.setProperty("homepageV2", undefined);
}
});
if (!home.value) {
switch (homepageV2.type) {
case "notebook":
{
const notebook = await db.notebooks.notebook(homepageV2.id);
if (notebook) {
home.value = {
name: "Notebook",
params: {
item: notebook,
id: notebook.id,
title: notebook.title
}
};
return;
}
}
break;
case "color": {
const color = await db.colors.color(homepageV2.id);
if (color) {
home.value = {
name: "ColoredNotes",
params: {
item: color,
id: color.id,
title: color.title
}
};
return;
}
break;
}
case "tag": {
const tag = await db.tags.tag(homepageV2.id);
if (tag) {
home.value = {
name: "TaggedNotes",
params: {
item: tag,
id: tag.id,
title: tag.title
}
};
return;
}
break;
}
case "default":
{
home.value = DEFAULT_HOME;
}
return;
}
home.value = undefined;
setTimeout(() => {
home.value = DEFAULT_HOME;
});
}
});
}, [homepageV2, loading]);
})();
}, [homepageV2, loading, home.value]);
React.useEffect(() => {
if (!home) return;
useNavigationStore.getState().update(home?.name as keyof RouteParams);
useNavigationStore
.getState()
.setFocusedRouteId(home?.params?.id || home?.name);
.update(home.value?.name as keyof RouteParams);
useNavigationStore
.getState()
.setFocusedRouteId(home.value?.params?.id || home.value?.name);
}, [home]);
return !home ? null : (
return !home.value ? null : (
<AppStack.Navigator
initialRouteName={home?.name}
initialRouteName={home.value?.name}
screenOptions={{
headerShown: false,
animation: "none",
@@ -207,7 +197,7 @@ const AppNavigation = React.memo(
return TaggedNotes;
}}
initialParams={
home?.name === "TaggedNotes" ? home?.params : undefined
home.value?.name === "TaggedNotes" ? home.value?.params : undefined
}
/>
@@ -219,7 +209,7 @@ const AppNavigation = React.memo(
return ColoredNotes;
}}
initialParams={
home?.name === "ColoredNotes" ? home?.params : undefined
home.value?.name === "ColoredNotes" ? home.value?.params : undefined
}
/>
@@ -254,7 +244,9 @@ const AppNavigation = React.memo(
Notebook = Notebook || require("../screens/notebook").default;
return Notebook;
}}
initialParams={home?.name === "Notebook" ? home?.params : undefined}
initialParams={
home.value?.name === "Notebook" ? home.value?.params : undefined
}
/>
<AppStack.Screen
@@ -281,7 +273,6 @@ let Settings: any = null;
let ManageTags: any = null;
let AddReminder: any = null;
let PayWall: any = null;
let Wrapped: any = null;
export const RootNavigation = () => {
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
@@ -393,14 +384,6 @@ export const RootNavigation = () => {
return PayWall;
}}
/>
<RootStack.Screen
name="Wrapped"
getComponent={() => {
Wrapped = Wrapped || require("../screens/wrapped").default;
return Wrapped;
}}
/>
</RootStack.Navigator>
</NavigationContainer>
);

View File

@@ -42,9 +42,9 @@ import { DDS } from "../../services/device-detection";
import { ToastManager } from "../../services/event-manager";
import Navigation, { NavigationProps } from "../../services/navigation";
import Notifications from "../../services/notifications";
import PremiumService from "../../services/premium";
import SettingsService from "../../services/settings";
import { useRelationStore } from "../../stores/use-relation-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { getFormattedDate, useIsFeatureAvailable } from "@notesnook/common";
@@ -70,9 +70,17 @@ const RecurringModes = {
Year: "year"
};
const WeekDays = [0, 1, 2, 3, 4, 5, 6];
const WeekDaysMon = [1, 2, 3, 4, 5, 6, 0];
const WeekDays = new Array(7).fill(true);
const MonthDays = new Array(31).fill(true);
const WeekDayNames = {
0: "Sunday",
1: "Monday",
2: "Tuesday",
3: "Wednesday",
4: "Thursday",
5: "Friday",
6: "Saturday"
};
const ReminderNotificationModes = {
Silent: "silent",
@@ -84,7 +92,6 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
const { reminder, reference } = props.route.params;
useNavigationFocus(props.navigation, { focusOnInit: true });
const { colors, isDark } = useThemeColors();
const weekFormat = useSettingStore((state) => state.weekFormat);
const [reminderMode, setReminderMode] = useState<Reminder["mode"]>(
reminder?.mode || "once"
);
@@ -144,8 +151,7 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
const isSecondLast = index === selectedDays.length - 2;
const joinWith = isSecondLast ? " & " : isLast ? "" : ", ";
return recurringMode === RecurringModes.Week
? strings.weekDayNames[day as keyof typeof strings.weekDayNames]() +
joinWith
? WeekDayNames[day as keyof typeof WeekDayNames] + joinWith
: `${day}${nth(day)} ${joinWith}`;
})
.join("");
@@ -384,39 +390,37 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
recurringMode === RecurringModes.Year
? null
: recurringMode === RecurringModes.Week
? (weekFormat === "Mon" ? WeekDaysMon : WeekDays).map(
(item) => (
<Button
key={strings.weekDayNamesShort[
item as keyof typeof strings.weekDayNamesShort
]()}
title={strings.weekDayNamesShort[
item as keyof typeof strings.weekDayNamesShort
]()}
type={
selectedDays.indexOf(item) > -1
? "selected"
: "plain"
}
fontSize={AppFontSize.xs}
style={{
height: 40,
borderRadius: 100,
marginRight: 10
}}
onPress={() => {
setSelectedDays((days) => {
if (days.indexOf(item) > -1) {
days.splice(days.indexOf(item), 1);
return [...days];
}
days.push(item);
? WeekDays.map((item, index) => (
<Button
key={strings.weekDayNamesShort[
index as keyof typeof strings.weekDayNamesShort
]()}
title={strings.weekDayNamesShort[
index as keyof typeof strings.weekDayNamesShort
]()}
type={
selectedDays.indexOf(index) > -1
? "selected"
: "plain"
}
fontSize={AppFontSize.xs}
style={{
height: 40,
borderRadius: 100,
marginRight: 10
}}
onPress={() => {
setSelectedDays((days) => {
if (days.indexOf(index) > -1) {
days.splice(days.indexOf(index), 1);
return [...days];
});
}}
/>
)
)
}
days.push(index);
return [...days];
});
}}
/>
))
: MonthDays.map((item, index) => (
<Button
key={index + "monthday"}
@@ -464,7 +468,6 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
onConfirm={handleConfirm}
onCancel={hideDatePicker}
isDarkModeEnabled={isDark}
firstDayOfWeek={weekFormat === "Mon" ? 1 : 0}
is24Hour={db.settings.getTimeFormat() === "24-hour"}
date={date || new Date(Date.now())}
/>

View File

@@ -62,7 +62,6 @@ import { fluidTabsRef } from "../../utils/global-refs";
import { strings } from "@notesnook/intl";
import { i18n } from "@lingui/core";
import { useVaultStatus } from "../../hooks/use-vault-status";
import { useSettingStore } from "../../stores/use-setting-store";
const style: ViewStyle = {
height: "100%",
@@ -232,6 +231,7 @@ const useLockedNoteHandler = () => {
const unlockWithBiometrics = async () => {
try {
if (!tabRef.current?.session?.noteLocked || !tabRef.current) return;
console.log("Trying to unlock with biometrics...");
const credentials = await BiometricService.getCredentials(
"Unlock note",
"Unlock note to open it in editor."
@@ -320,16 +320,12 @@ const useLockedNoteHandler = () => {
}
};
const unlock = (forced?: boolean) => {
const isMovedAway =
useSettingStore.getState().deviceMode !== "mobile"
? false
: editorState().movedAway;
const unlock = () => {
if (
tabRef.current?.session?.locked &&
(tabRef.current?.session?.locked,
useTabStore.getState().biometryAvailable &&
useTabStore.getState().biometryEnrolled &&
(!isMovedAway || forced)
useTabStore.getState().biometryEnrolled &&
!editorState().movedAway)
) {
setTimeout(() => {
unlockWithBiometrics();
@@ -348,15 +344,13 @@ const useLockedNoteHandler = () => {
const subs = [
eSubscribeEvent(eUnlockNote, unlock),
eSubscribeEvent(eUnlockWithBiometrics, () => {
unlock(true);
unlock();
}),
eSubscribeEvent(eUnlockWithPassword, onSubmit)
];
if (
tabRef.current?.session?.locked &&
(fluidTabsRef.current?.page() === "editor" ||
useSettingStore.getState().deviceMode !== "mobile")
fluidTabsRef.current?.page() === "editor"
) {
unlock();
}

View File

@@ -22,10 +22,10 @@ import { isFeatureAvailable } from "@notesnook/common";
import { isImage } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import {
pick as pickFile,
DocumentPickerOptions,
keepLocalCopy,
KeepLocalCopyResponse,
pick as pickFile
DocumentPickerResponse,
keepLocalCopy
} from "@react-native-documents/picker";
import { basename } from "pathe";
import { Platform } from "react-native";
@@ -35,13 +35,31 @@ import { DatabaseLogger, db } from "../../../common/database";
import filesystem from "../../../common/filesystem";
import { compressToFile } from "../../../common/filesystem/compress";
import AttachImage from "../../../components/dialogs/attach-image-dialog";
import { ToastManager } from "../../../services/event-manager";
import {
ToastManager,
eSendEvent,
presentSheet
} from "../../../services/event-manager";
import PremiumService from "../../../services/premium";
import { useSettingStore } from "../../../stores/use-setting-store";
import { useUserStore } from "../../../stores/use-user-store";
import { eCloseSheet } from "../../../utils/events";
import { useTabStore } from "./use-tab-store";
import { editorController, editorState } from "./utils";
import { santizeUri } from "../../../common/filesystem/utils";
const showEncryptionSheet = (file: DocumentPickerResponse) => {
presentSheet({
title: strings.encryptingAttachment(),
paragraph: strings.encryptingAttachmentDesc(file.name || ""),
icon: "attachment"
});
};
const santizeUri = (uri: string) => {
uri = decodeURI(uri);
uri = Platform.OS === "ios" ? uri.replace("file:///", "/") : uri;
return uri;
};
type PickerOptions = {
noteId?: string;
@@ -62,7 +80,7 @@ const file = async (fileOptions: PickerOptions) => {
await db.attachments.generateKey();
let file;
let fileCopyUri: KeepLocalCopyResponse[0];
let fileCopyUri;
let fileName;
try {
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
@@ -79,10 +97,12 @@ const file = async (fileOptions: PickerOptions) => {
});
fileCopyUri = result[0];
} catch (e) {
DatabaseLogger.error(e as Error, "Error picking file");
return;
}
let uri =
Platform.OS === "ios" ? fileCopyUri.sourceUri || file.uri : file.uri;
const featureResult = await isFeatureAvailable("fileSize", file.size || 0);
if (!featureResult.isAllowed) {
ToastManager.show({
@@ -102,7 +122,8 @@ const file = async (fileOptions: PickerOptions) => {
return;
}
let uri = santizeUri(fileCopyUri.localUri);
uri = Platform.OS === "ios" ? santizeUri(uri) : uri;
showEncryptionSheet(file);
const hash = await Sodium.hashFile({
uri: uri,
type: "url"
@@ -118,8 +139,7 @@ const file = async (fileOptions: PickerOptions) => {
) {
throw new Error("Failed to attach file");
}
await RNFetchBlob.fs.unlink(uri);
if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri);
if (
fileOptions.tabId !== undefined &&
@@ -153,7 +173,10 @@ const file = async (fileOptions: PickerOptions) => {
} else {
throw new Error("Failed to attach file, no tabId is set");
}
eSendEvent(eCloseSheet);
} catch (e) {
eSendEvent(eCloseSheet);
ToastManager.show({
heading: (e as Error).message,
type: "error",
@@ -182,9 +205,7 @@ const camera = async (options: PickerOptions) => {
options
);
})
.catch((e) => {
console.log(e);
});
.catch((e) => {});
} catch (e) {
ToastManager.show({
heading: (e as Error).message,

View File

@@ -20,7 +20,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import type { ToolbarGroupDefinition } from "@notesnook/editor";
import { useEditor } from "./use-editor";
import { FeatureId, FeatureResult } from "@notesnook/common";
import { DayFormat } from "@notesnook/core";
export type useEditorType = ReturnType<typeof useEditor>;
export type EditorState = {
@@ -61,8 +60,6 @@ export type Settings = {
markdownShortcuts: boolean;
features: Record<any, any>;
loggedIn: boolean;
defaultLineHeight: number;
dayFormat: DayFormat;
};
export type EditorProps = {

View File

@@ -28,13 +28,11 @@ import { getDefaultPresets } from "@notesnook/editor/dist/cjs/toolbar/tool-defin
import { strings } from "@notesnook/intl";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { useCallback, useEffect, useRef } from "react";
import * as ScopedStorage from "react-native-scoped-storage";
import {
BackHandler,
Keyboard,
KeyboardEventListener,
NativeEventSubscription,
Platform,
useWindowDimensions
} from "react-native";
import { WebViewMessageEvent } from "react-native-webview";
@@ -84,9 +82,6 @@ import { useDragState } from "../../settings/editor/state";
import { EditorMessage, EditorProps, useEditorType } from "./types";
import { useTabStore } from "./use-tab-store";
import { editorState, openInternalLink } from "./utils";
import filesystem from "../../../common/filesystem";
import ReactNativeBlobUtil from "react-native-blob-util";
import { ShareComponent } from "../../../components/sheets/export-notes/share";
const publishNote = async () => {
const user = useUserStore.getState().user;
@@ -158,22 +153,16 @@ export const useEditorEvents = (
const features = useAreFeaturesAvailable([
"callout",
"outlineList",
"taskList",
"importCsvToTable",
"exportTableAsCsv"
"taskList"
]);
const deviceMode = useSettingStore((state) => state.deviceMode);
const fullscreen = useSettingStore((state) => state.fullscreen);
const corsProxy = useSettingStore((state) => state.settings.corsProxy);
const loading = useSettingStore((state) => state.isAppLoading);
const [dateFormat, timeFormat, defaultLineHeight, dayFormat] =
useSettingStore((state) => [
state.dateFormat,
state.timeFormat,
state.settings.defaultLineHeight,
state.dayFormat
]);
const [dateFormat, timeFormat] = useSettingStore((state) => [
state.dateFormat,
state.timeFormat
]);
const handleBack = useRef<NativeEventSubscription>(undefined);
const loggedIn = useUserStore((state) => !!state.user);
const { fontScale } = useWindowDimensions();
@@ -233,12 +222,10 @@ export const useEditorEvents = (
fontFamily: SettingsService.get().defaultFontFamily,
dateFormat: db.settings?.getDateFormat(),
timeFormat: db.settings?.getTimeFormat(),
dayFormat: db.settings?.getDayFormat(),
fontScale,
markdownShortcuts,
features,
loggedIn,
defaultLineHeight
loggedIn
});
}, [
fullscreen,
@@ -254,13 +241,10 @@ export const useEditorEvents = (
defaultFontFamily,
dateFormat,
timeFormat,
dayFormat,
loading,
fontScale,
markdownShortcuts,
loggedIn,
defaultLineHeight,
features
loggedIn
]);
const onBackPress = useCallback(async () => {
@@ -680,8 +664,7 @@ export const useEditorEvents = (
if (note) {
eSendEvent(eOnLoadNote, {
item: note,
tabId: editorMessage.tabId,
loadedFromEditor: true
tabId: editorMessage.tabId
});
}
} else {
@@ -692,8 +675,7 @@ export const useEditorEvents = (
if (note) {
eSendEvent(eOnLoadNote, {
item: note,
tabId: editorMessage.tabId,
loadedFromEditor: true
tabId: editorMessage.tabId
});
}
}
@@ -749,62 +731,6 @@ export const useEditorEvents = (
}
break;
}
case EditorEvents.downloadCsv: {
try {
const csv = editorMessage.value;
let filePath: string;
let fileName: string;
if (Platform.OS === "android") {
let file = await ScopedStorage.createDocument(
"table.csv",
"text/csv",
csv,
"utf8"
);
if (!file) return;
filePath = file.path || file.uri;
fileName = file.name;
} else {
const path = await filesystem.checkAndCreateDir("/");
let possibleFileName = "table.csv";
let counter = 1;
// Check if file exists and find available filename
while (
await ReactNativeBlobUtil.fs.exists(path + possibleFileName)
) {
possibleFileName = `table (${counter}).csv`;
counter++;
}
await ReactNativeBlobUtil.fs.writeFile(
path + possibleFileName,
csv,
"utf8"
);
filePath = path + possibleFileName;
fileName = possibleFileName;
}
await sleep(500);
presentSheet({
title: "Table saved to csv",
paragraph: strings.fileSaved(fileName, Platform.OS),
icon: "download",
context: "global",
component: (
<ShareComponent uri={filePath} name={fileName} padding={12} />
)
});
} catch (e) {
ToastManager.show({
type: "info",
message: "Could not save table to csv"
});
DatabaseLogger.error(e as Error);
}
break;
}
default:
break;

View File

@@ -298,7 +298,7 @@ export const useEditor = (
return;
}
if (!title && isContentInvalid(data) && id) {
if (isContentInvalid(data) && id) {
// Create a new history session if recieved empty or invalid content
// To ensure that history is preserved for correct content.
currentSessionHistoryId = editorSessionHistory.newSession(id);
@@ -508,17 +508,9 @@ export const useEditor = (
newTab?: boolean;
refresh?: boolean;
searchResultIndex?: number;
loadedFromEditor?: boolean;
}) => {
loadNoteMutex.runExclusive(async () => {
if (
!event ||
(event.loadedFromEditor &&
event.item &&
event.item?.id !== useTabStore.getState().getCurrentNoteId())
) {
return;
}
if (!event) return;
if (event.blockId) {
blockIdRef.current = event.blockId;
}
@@ -677,7 +669,7 @@ export const useEditor = (
state.current.currentlyEditing = true;
if (!tabLocked) {
await loadContent(item);
} else if (fluidTabsRef.current?.page() === "editor") {
} else {
commands.focus(tabId!);
}
@@ -727,7 +719,6 @@ export const useEditor = (
}, 300);
}
postMessage(NativeEvents.theme, theme);
console.log("load finished", event.item?.id);
});
},
[
@@ -1097,10 +1088,7 @@ export const useEditor = (
if (!state.current?.initialLoadCalled) {
const url = await Linking.getInitialURL();
let noteId =
url &&
url.startsWith("https://app.notesnook.com/open_note?") &&
new URL(url).searchParams.get("id");
let noteId = url && new URL(url).searchParams.get("id");
if (noteId) {
const note = await db.notes?.note(noteId);
fluidTabsRef.current?.goToPage("editor");

View File

@@ -23,36 +23,21 @@ import {
AppState,
AppStateStatus,
KeyboardAvoidingView,
Platform,
TextInput,
View
} from "react-native";
import Editor from ".";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import useIsFloatingKeyboard from "../../hooks/use-is-floating-keyboard";
import useKeyboard from "../../hooks/use-keyboard";
import { DDS } from "../../services/device-detection";
import { useSettingStore } from "../../stores/use-setting-store";
import { editorRef } from "../../utils/global-refs";
import { editorController, textInput } from "./tiptap/utils";
import deviceInfo from "react-native-device-info";
export type PaneWidths = {
mobile: {
sidebar: number;
list: number;
editor: number;
};
smallTablet: {
sidebar: number;
list: number;
editor: number;
};
tablet: {
sidebar: number;
list: number;
editor: number;
};
};
export const EditorWrapper = ({ widths }: { widths: PaneWidths }) => {
export const EditorWrapper = ({ widths }: { widths: any }) => {
const { colors } = useThemeColors();
const { colors: toolBarColors } = useThemeColors("editorToolbar");
const deviceMode = useSettingStore((state) => state.deviceMode);
@@ -62,9 +47,8 @@ export const EditorWrapper = ({ widths }: { widths: PaneWidths }) => {
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
const keyboard = useKeyboard();
const prevState = useRef<AppStateStatus>(undefined);
const isFullscreen = useSettingStore((state) => state.fullscreen);
const dimensions = useSettingStore((state) => state.dimensions);
const onAppStateChanged = async (state: AppStateStatus) => {
if (!prevState.current) {
@@ -88,40 +72,37 @@ export const EditorWrapper = ({ widths }: { widths: PaneWidths }) => {
};
}, [loading]);
const getMarginBottom = () => {
const bottomInsets =
Platform.OS === "android" ? 12 : insets.bottom + 16 || 14;
if (!keyboard.keyboardShown) return bottomInsets / 1.5;
if (deviceInfo.isTablet() && Platform.OS === "ios" && !floating)
return bottomInsets;
if (Platform.OS === "ios") return bottomInsets / 1.5;
return 0;
};
const KeyboardAvoidingViewIOS =
Platform.OS === "ios" ? KeyboardAvoidingView : View;
return (
<View
testID="editor-wrapper"
ref={editorRef}
style={[
{
width: isFullscreen
? dimensions.width
: widths[
!introCompleted ? "mobile" : (deviceMode as keyof PaneWidths)
]?.editor,
height: "100%",
minHeight: "100%",
backgroundColor: toolBarColors.primary.background,
paddingLeft: isFullscreen
? deviceMode === "smallTablet"
? 0
: dimensions.width * 0.15
: null,
paddingRight: isFullscreen
? deviceMode === "smallTablet"
? 0
: dimensions.width * 0.15
: insets.right,
borderLeftWidth: DDS.isTab ? 1 : 0,
borderLeftColor: DDS.isTab
? colors.secondary.background
: "transparent",
paddingBottom: insets.bottom
}
]}
style={{
width: widths[!introCompleted ? "mobile" : (deviceMode as any)]?.editor,
height: "100%",
minHeight: "100%",
backgroundColor: toolBarColors.primary.background,
borderLeftWidth: DDS.isTab ? 1 : 0,
borderLeftColor: DDS.isTab
? colors.secondary.background
: "transparent",
paddingBottom: insets.bottom
}}
>
{loading || !introCompleted ? null : (
<KeyboardAvoidingView
<KeyboardAvoidingViewIOS
behavior="padding"
style={{
backgroundColor: colors.primary.background,
@@ -137,7 +118,7 @@ export const EditorWrapper = ({ widths }: { widths: PaneWidths }) => {
blurOnSubmit={false}
/>
<Editor key="editor" withController={true} />
</KeyboardAvoidingView>
</KeyboardAvoidingViewIOS>
)}
</View>
);

View File

@@ -38,6 +38,7 @@ import { eUpdateNotebookRoute } from "../../utils/events";
import { findRootNotebookId } from "../../utils/notebooks";
import { openEditor, setOnFirstSave } from "../notes/common";
import { View } from "react-native";
import { DefaultAppStyles } from "../../utils/styles";
import { Notebooks } from "../../components/sheets/notebooks";
import { useSettingStore } from "../../stores/use-setting-store";
import { rootNavigatorRef } from "../../utils/global-refs";
@@ -46,9 +47,6 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
const [notes, setNotes] = useState<VirtualizedGrouping<Note>>();
const params = useRef<NotebookScreenParams>(route?.params);
const isAppLoading = useSettingStore((state) => state.isAppLoading);
const [notebook, setNotebook] = useState<Notebook | undefined>(
params.current.item
);
const [loading, setLoading] = useState(true);
const updateOnFocus = useRef(false);
const [breadcrumbs, setBreadcrumbs] = useState<
@@ -77,10 +75,10 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
});
const syncWithNavigation = React.useCallback(() => {
useNavigationStore.getState().setFocusedRouteId(params?.current?.id);
useNavigationStore.getState().setFocusedRouteId(params?.current?.item?.id);
setOnFirstSave({
type: "notebook",
id: params.current.id
id: params.current.item.id
});
}, []);
@@ -88,20 +86,23 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
async (data?: NotebookScreenParams) => {
if (useSettingStore.getState().isAppLoading) return;
if (
useNavigationStore.getState().focusedRouteId !== params.current.id &&
useNavigationStore.getState().focusedRouteId !==
params.current.item.id &&
!data
) {
updateOnFocus.current = true;
return;
}
if (data?.id && params.current?.id !== data?.id) {
const nextRootNotebookId = await findRootNotebookId(data?.id);
const currentNotebookRoot = await findRootNotebookId(params.current.id);
if (data?.item?.id && params.current.item?.id !== data?.item?.id) {
const nextRootNotebookId = await findRootNotebookId(data?.item?.id);
const currentNotebookRoot = await findRootNotebookId(
params.current.item.id
);
if (
nextRootNotebookId !== currentNotebookRoot ||
nextRootNotebookId === params.current?.id
nextRootNotebookId === params.current?.item?.id
) {
// Never update notebook in route if root is different or if the root is current notebook.
return;
@@ -111,13 +112,14 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
if (data) params.current = data;
try {
const notebook = await db.notebooks?.notebook(params?.current?.id);
setNotebook(notebook);
params.current.item = notebook;
const notebook = await db.notebooks?.notebook(
params?.current?.item?.id
);
if (notebook) {
const breadcrumbs = await db.notebooks.breadcrumbs(notebook.id);
setBreadcrumbs(breadcrumbs.slice(0, breadcrumbs.length - 1));
params.current.id = notebook.id;
params.current.item = notebook;
const notes = await db.relations
.from(notebook, "note")
.selector.grouped(db.settings.getGroupOptions("notes"));
@@ -158,27 +160,29 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
<>
<Header
renderedInRoute={route.name}
title={notebook?.title}
title={params.current.item?.title}
canGoBack={params?.current?.canGoBack}
rightButton={{
name: "dots-vertical",
onPress: () => {
Properties.present(notebook);
Properties.present(params.current.item);
}
}}
hasSearch={true}
onSearch={() => {
if (!notebook) return;
const selector = db.relations.from(notebook, "note").selector;
const selector = db.relations.from(
params.current.item,
"note"
).selector;
Navigation.push("Search", {
placeholder: strings.searchInRoute(notebook?.title),
placeholder: strings.searchInRoute(params.current.item?.title),
type: "note",
title: notebook?.title,
title: params.current.item?.title,
route: route.name,
items: selector
});
}}
id={notebook?.id}
id={params.current.item?.id}
/>
<DelayLayout wait={loading}>
@@ -188,21 +192,19 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
onRefresh={() => {
onRequestUpdate();
}}
id={params.current?.id}
id={params.current.item?.id}
renderedInRoute="Notebook"
headerTitle={notebook?.title}
headerTitle={params.current.item.title}
loading={loading}
CustomLisHeader={
notebook ? (
<NotebookHeader
breadcrumbs={breadcrumbs}
notebook={notebook}
totalNotes={notes?.placeholders.length || 0}
/>
) : undefined
<NotebookHeader
breadcrumbs={breadcrumbs}
notebook={params.current.item}
totalNotes={notes?.placeholders.length || 0}
/>
}
placeholder={{
title: notebook?.title!,
title: params.current.item?.title,
paragraph: strings.notesEmpty(),
button: strings.addFirstNote(),
action: openEditor,
@@ -215,22 +217,20 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
position: "absolute",
bottom: 20,
right: 20,
gap: DefaultAppStyles.GAP_VERTICAL,
alignItems: "center"
}}
>
<FloatingButton
icon="file-tree"
testID="notebookTreeSheet"
size="small"
onPress={() => {
if (!notebook) return;
Notebooks.present(notebook);
Notebooks.present(params.current.item);
}}
style={{
position: "relative",
right: 0,
bottom: 0,
paddingBottom: 10
bottom: 5
}}
/>
<FloatingButton
@@ -238,14 +238,13 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
alwaysVisible
style={{
position: "relative",
paddingTop: 10,
right: 0,
bottom: 0
}}
/>
</View>
<SelectionHeader
id={route.params?.id}
id={route.params?.item?.id}
items={notes}
type="note"
renderedInRoute="Notebook"
@@ -259,7 +258,6 @@ NotebookScreen.navigate = async (item: Notebook, canGoBack?: boolean) => {
const { currentRoute, focusedRouteId } = useNavigationStore.getState();
if (currentRoute === "Notebooks") {
Navigation.push("Notebook", {
id: item.id,
item: item,
canGoBack
});
@@ -276,24 +274,22 @@ NotebookScreen.navigate = async (item: Notebook, canGoBack?: boolean) => {
// Update the route in place instead
eSendEvent(eUpdateNotebookRoute, {
id: item.id,
canGoBack: canGoBack,
item: item
item: item,
title: item.title,
canGoBack: canGoBack
});
} else {
// Push a new route
Navigation.push("Notebook", {
id: item.id,
canGoBack,
item: item
item: item,
canGoBack
});
}
} else {
// Push a new route anyways
Navigation.push("Notebook", {
id: item.id,
canGoBack,
item: item
item: item,
canGoBack
});
}
};

View File

@@ -25,7 +25,7 @@ import Navigation, { NavigationProps } from "../../services/navigation";
import useNavigationStore, {
NotesScreenParams
} from "../../stores/use-navigation-store";
import { PLACEHOLDER_DATA, openEditor } from "./common";
import { PLACEHOLDER_DATA, openEditor, toCamelCase } from "./common";
export const ColoredNotes = ({
navigation,
route
@@ -45,13 +45,11 @@ export const ColoredNotes = ({
ColoredNotes.get = async (params: NotesScreenParams, grouped = true) => {
if (!grouped) {
return await db.relations
.from({ id: params.id, type: "color" }, "note")
.resolve();
return await db.relations.from(params.item, "note").resolve();
}
return await db.relations
.from({ id: params.id, type: "color" }, "note")
.from(params.item, "note")
.selector.grouped(db.settings.getGroupOptions("notes"));
};
@@ -65,10 +63,8 @@ ColoredNotes.navigate = (item: Color, canGoBack: boolean) => {
}
Navigation.push<"ColoredNotes">("ColoredNotes", {
type: "color",
id: item.id,
canGoBack,
item: item
item: item,
canGoBack
});
};

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { resolveItems } from "@notesnook/common";
import { Tag, VirtualizedGrouping } from "@notesnook/core";
import { VirtualizedGrouping } from "@notesnook/core";
import { Color, Note } from "@notesnook/core";
import React, { useEffect, useRef, useState } from "react";
import { db } from "../../common/database";
@@ -76,12 +76,14 @@ const NotesPage = ({
const [notes, setNotes] = useState<VirtualizedGrouping<Note>>();
const [loadingNotes, setLoadingNotes] = useState(true);
const isMonograph = route.name === "Monographs";
const [item, setItem] = useState<Tag | Color | undefined>(
params.current.item
);
const title = item?.type === "tag" ? "#" + item?.title : item?.title;
const title =
params.current?.item.type === "tag"
? "#" + params.current?.item.title
: params.current?.item.title;
const accentColor =
route.name === "ColoredNotes" ? (item as Color)?.colorCode : undefined;
route.name === "ColoredNotes"
? (params.current?.item as Color)?.colorCode
: undefined;
const updateOnFocus = useRef(false);
const isAppLoading = useSettingStore((state) => state.isAppLoading);
const isFocused = useNavigationFocus(navigation, {
@@ -105,30 +107,31 @@ const NotesPage = ({
});
const syncWithNavigation = React.useCallback(() => {
const { id } = params.current;
useNavigationStore.getState().setFocusedRouteId(id || route.name);
const { item } = params.current;
useNavigationStore
.getState()
.setFocusedRouteId(params?.current?.item?.id || route.name);
!isMonograph &&
setOnFirstSave({
type: getItemType(route.name),
id: id
id: item.id
});
}, [isMonograph, route.name]);
const onRequestUpdate = React.useCallback(
async (data?: NotesScreenParams) => {
if (useSettingStore.getState().isAppLoading) return;
if (
params.current.id &&
useNavigationStore.getState().focusedRouteId !== params.current.id &&
params.current.item.id &&
useNavigationStore.getState().focusedRouteId !==
params.current.item.id &&
!data
) {
updateOnFocus.current = false;
return;
}
const isNew = data && data?.id !== params.current?.id;
const isNew = data && data?.item?.id !== params.current?.item?.id;
if (data) params.current = data;
try {
@@ -139,9 +142,9 @@ const NotesPage = ({
)) as VirtualizedGrouping<Note>;
if (route.name === "TaggedNotes" || route.name === "ColoredNotes") {
const item = await (db as any)[params.current.type + "s"][
params.current.type
](params.current.id);
const item = await (db as any)[params.current.item.type + "s"][
params.current.item.type
](params.current.item.id);
if (!item) {
if (rootNavigatorRef.canGoBack()) {
@@ -151,7 +154,7 @@ const NotesPage = ({
}
return;
}
setItem(item);
params.current.item = item;
}
@@ -170,7 +173,15 @@ const NotesPage = ({
useEffect(() => {
if (isAppLoading) return;
if (loadingNotes) {
onRequestUpdate(params.current);
get(params.current, true)
.then(async (items) => {
setNotes(items as VirtualizedGrouping<Note>);
await (items as VirtualizedGrouping<Note>).item(0, resolveItems);
setLoadingNotes(false);
})
.catch((e) => {
setLoadingNotes(false);
});
}
}, [loadingNotes, get, isAppLoading]);
@@ -191,18 +202,19 @@ const NotesPage = ({
}
canGoBack={params?.current?.canGoBack}
hasSearch={true}
id={route.name === "Monographs" ? "Monographs" : params?.current?.id}
id={
route.name === "Monographs" ? "Monographs" : params?.current.item?.id
}
onSearch={() => {
if (!item) return;
const selector =
route.name === "Monographs"
? db.monographs.all
: db.relations.from(item, "note").selector;
: db.relations.from(params.current.item, "note").selector;
Navigation.push("Search", {
placeholder: strings.searchInRoute(title || route.name),
type: "note",
title: title!,
title: title,
route: route.name,
items: selector
});
@@ -217,7 +229,7 @@ const NotesPage = ({
onRefresh={onRequestUpdate}
loading={false}
renderedInRoute={route.name}
id={params.current?.id}
id={params.current.item?.id}
headerTitle={title || "Monographs"}
customAccentColor={accentColor}
placeholder={placeholder}
@@ -234,7 +246,7 @@ const NotesPage = ({
) : null}
</DelayLayout>
<SelectionHeader
id={route.params?.id || route.name}
id={route.params?.item?.id || route.name}
items={notes}
type="note"
renderedInRoute={route.name}

View File

@@ -62,8 +62,7 @@ Monographs.get = async (params?: NotesScreenParams, grouped = true) => {
Monographs.navigate = (canGoBack?: boolean) => {
Navigation.navigate<"Monographs">("Monographs", {
type: "monograph",
id: "monograph",
item: { type: "monograph" } as any,
canGoBack: canGoBack as boolean
});
};

View File

@@ -46,13 +46,11 @@ export const TaggedNotes = ({
TaggedNotes.get = async (params: NotesScreenParams, grouped = true) => {
if (!grouped) {
return await db.relations
.from({ id: params.id, type: "tag" }, "note")
.resolve();
return await db.relations.from(params.item, "note").resolve();
}
return await db.relations
.from({ id: params.id, type: "tag" }, "note")
.from(params.item, "note")
.selector.grouped(db.settings.getGroupOptions("notes"));
};
@@ -66,10 +64,8 @@ TaggedNotes.navigate = (item: Tag, canGoBack?: boolean) => {
}
Navigation.push<"TaggedNotes">("TaggedNotes", {
id: item.id,
type: "tag",
canGoBack,
item: item
item: item,
canGoBack
});
};

View File

@@ -23,6 +23,7 @@ import React, { useRef, useState } from "react";
import { View } from "react-native";
import { TextInput } from "react-native-gesture-handler";
import { IconButton } from "../../components/ui/icon-button";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import Navigation from "../../services/navigation";
import useNavigationStore from "../../stores/use-navigation-store";
import { useSelectionStore } from "../../stores/use-selection-store";
@@ -40,6 +41,7 @@ export const SearchBar = ({
const isFocused = useNavigationStore(
(state) => state.focusedRouteId === "Search"
);
const insets = useGlobalSafeAreaInsets();
const { colors } = useThemeColors();
const inputRef = useRef<TextInput>(null);
const _onChangeText = (value: string) => {
@@ -56,6 +58,7 @@ export const SearchBar = ({
>
<View
style={{
marginTop: DefaultAppStyles.GAP_SMALL,
flexDirection: "row",
alignItems: "center",
width: "100%",

View File

@@ -32,8 +32,6 @@ import {
BackupReminderPicker,
BackupWithAttachmentsReminderPicker,
DateFormatPicker,
DayFormatPicker,
WeekFormatPicker,
FontPicker,
HomePicker,
SidebarTabPicker,
@@ -59,8 +57,6 @@ export const components: { [name: string]: ReactElement } = {
"title-format": <TitleFormat />,
"date-format-selector": <DateFormatPicker />,
"time-format-selector": <TimeFormatPicker />,
"day-format-selector": <DayFormatPicker />,
"week-format-selector": <WeekFormatPicker />,
"theme-selector": <ThemeSelector />,
"applock-timer": <ApplockTimerPicker />,
autobackupsattachments: <BackupWithAttachmentsReminderPicker />,

View File

@@ -59,7 +59,7 @@ const Group = ({
id="Settings"
/>
)}
<DelayLayout type="settings">
<DelayLayout type="settings" delay={0}>
<View
style={{
flex: 1

View File

@@ -59,7 +59,7 @@ const Home = ({
hasSearch={false}
id="Settings"
/>
<DelayLayout type="settings">
<DelayLayout delay={0} type="settings">
<LegendList
data={settingsGroups}
keyExtractor={keyExtractor}

View File

@@ -17,29 +17,23 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { ScopedThemeProvider, useThemeColors } from "@notesnook/theme";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import React from "react";
import { View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import useNavigationStore from "../../stores/use-navigation-store";
import { ScopedThemeProvider, useThemeColors } from "@notesnook/theme";
import Group from "./group";
import Home from "./home";
import { RouteParams } from "./types";
import { SafeAreaView } from "react-native-safe-area-context";
const SettingsStack = createNativeStackNavigator<RouteParams>();
export const Settings = () => {
const { colors } = useThemeColors();
const insets = useSafeAreaInsets();
return (
<View
<SafeAreaView
style={{
flex: 1,
backgroundColor: colors.primary.background,
paddingTop: insets.top,
paddingBottom: insets.bottom,
paddingLeft: insets.left,
paddingRight: insets.right
backgroundColor: colors.primary.background
}}
>
<ScopedThemeProvider value="list">
@@ -64,7 +58,7 @@ export const Settings = () => {
<SettingsStack.Screen name="SettingsGroup" component={Group} />
</SettingsStack.Navigator>
</ScopedThemeProvider>
</View>
</SafeAreaView>
);
};

View File

@@ -32,18 +32,6 @@ import { strings } from "@notesnook/intl";
import { isFeatureAvailable } from "@notesnook/common";
import PaywallSheet from "../../../components/sheets/paywall";
const DAY_FORMATS = ["short", "long"];
const DayFormatFormats = {
short: "ddd",
long: "dddd"
};
const WEEK_FORMATS = ["Sun", "Mon"];
const WeekFormatNames = {
Sun: "Sunday",
Mon: "Monday"
};
export const FontPicker = createSettingsPicker({
getValue: () => useSettingStore.getState().settings.defaultFontFamily,
updateValue: (item) => {
@@ -123,8 +111,8 @@ export const TrashIntervalPicker = createSettingsPicker({
return item === -1
? strings.never()
: item === 1
? strings.reminderRecurringMode.day()
: strings.days(item);
? strings.reminderRecurringMode.day()
: strings.days(item);
},
getItemKey: (item) => item.toString(),
options: [-1, 1, 7, 30, 365],
@@ -164,42 +152,6 @@ export const DateFormatPicker = createSettingsPicker({
isOptionAvailable: () => true
});
export const DayFormatPicker = createSettingsPicker({
getValue: () => db.settings.getDayFormat(),
updateValue: (item) => {
db.settings.setDayFormat(item);
useSettingStore.setState({
dayFormat: item
});
},
formatValue: (item) => {
return `${strings.dayFormat()} (${dayjs().format(DayFormatFormats[item])})`;
},
getItemKey: (item) => item,
options: DAY_FORMATS,
compareValue: (current, item) => current === item,
isFeatureAvailable: () => true,
isOptionAvailable: () => true
});
export const WeekFormatPicker = createSettingsPicker({
getValue: () => db.settings.getWeekFormat(),
updateValue: (item) => {
db.settings.setWeekFormat(item);
useSettingStore.setState({
weekFormat: item
});
},
formatValue: (item) => {
return `${WeekFormatNames[item]}`;
},
getItemKey: (item) => item,
options: WEEK_FORMATS,
compareValue: (current, item) => current === item,
isFeatureAvailable: () => true,
isOptionAvailable: () => true
});
const TimeFormats = {
"12-hour": "hh:mm A",
"24-hour": "HH:mm"
@@ -276,10 +228,10 @@ export const ApplockTimerPicker = createSettingsPicker({
return item === -1
? strings.never()
: item === 0 || item === undefined
? strings.immediately()
: item === 1
? strings.minutes(1)
: strings.minutes(item);
? strings.immediately()
: item === 1
? strings.minutes(1)
: strings.minutes(item);
},
getItemKey: (item) => item.toString(),
options: [-1, 0, 1, 5, 15, 30],

View File

@@ -95,15 +95,6 @@ const restoreBackup = async (options: {
deleteFile?: boolean;
}) => {
try {
if (
!options.uri.endsWith(".nnbackup") &&
!options.uri.endsWith(".nnbackupz")
) {
throw new Error(
`Invalid backup file selected. Only .nnbackup and .nnbackupz files can be restored.`
);
}
const isLegacyBackup = options.uri.endsWith(".nnbackup");
startProgress({
@@ -376,6 +367,7 @@ export const RestoreBackup = () => {
disableAppLockRequests: true
});
const file = await pick();
const fileCopy = await keepLocalCopy({
destination: "cachesDirectory",
files: [
@@ -398,7 +390,10 @@ export const RestoreBackup = () => {
}, 1000);
restoreBackup({
uri: fileCopy[0].localUri,
uri:
Platform.OS === "android"
? (("file://" + fileCopy[0].sourceUri) as string)
: (fileCopy[0].sourceUri as string),
deleteFile: true
});
},

View File

@@ -71,7 +71,6 @@ import { verifyUser, verifyUserWithApplock } from "./functions";
import { logoutUser } from "./logout";
import { SettingSection } from "./types";
import { getTimeLeft } from "./user-section";
import { EDITOR_LINE_HEIGHT } from "../../utils/constants";
export const settingsGroups: SettingSection[] = [
{
@@ -119,8 +118,7 @@ export const settingsGroups: SettingSection[] = [
if (
(user.subscription?.provider === SubscriptionProvider.GOOGLE ||
user.subscription?.provider === SubscriptionProvider.APPLE) &&
isCurrentPlatform &&
user?.subscription?.productId
isCurrentPlatform
) {
RNIap.deepLinkToSubscriptions({
sku: user?.subscription.productId
@@ -144,20 +142,16 @@ export const settingsGroups: SettingSection[] = [
"dddd, MMMM D, YYYY h:mm A"
);
const trialEndDate = dayjs(user?.subscription?.start)
.add(
user?.subscription?.productId?.includes("monthly") ? 7 : 14,
"day"
)
.format("dddd, MMMM D, YYYY h:mm A");
if (
user.subscription?.plan !== SubscriptionPlan.FREE &&
user.subscription?.productId
) {
if (user.subscription?.plan !== SubscriptionPlan.FREE) {
const status = user.subscription?.status;
return status === SubscriptionStatus.TRIAL
? strings.trialOnGoing(trialEndDate)
? strings.trialEndsOn(
dayjs(user?.subscription?.start)
.add(
user?.subscription?.productId.includes("monthly") ? 7 : 14
)
.format("dddd, MMMM D, YYYY h:mm A")
)
: status === SubscriptionStatus.ACTIVE
? strings.subRenewOn(expiryDate)
: status === SubscriptionStatus.CANCELED ||
@@ -286,17 +280,10 @@ export const settingsGroups: SettingSection[] = [
{
id: "change-password",
name: strings.changePassword(),
// type: "screen",
type: "screen",
description: strings.changePasswordDesc(),
// component: "change-password",
icon: "form-textbox-password",
modifer: () => {
ToastManager.show({
type: "info",
message:
"Password changing has been disabled temporarily to address some issues faced by users. It will be enabled again once the issues have resolved."
});
}
component: "change-password",
icon: "form-textbox-password"
},
{
id: "change-email",
@@ -723,22 +710,6 @@ export const settingsGroups: SettingSection[] = [
component: "date-format-selector",
icon: "calendar-blank"
},
{
id: "day-format",
name: strings.dayFormat(),
description: strings.dayFormatDesc(),
type: "component",
component: "day-format-selector",
icon: "calendar-today"
},
{
id: "week-format",
name: strings.weekFormat(),
description: strings.weekFormatDesc(),
type: "component",
component: "week-format-selector",
icon: "calendar"
},
{
id: "time-format",
name: strings.timeFormat(),
@@ -838,16 +809,6 @@ export const settingsGroups: SettingSection[] = [
property: "defaultFontFamily",
component: "font-selector"
},
{
id: "default-line-height",
name: strings.lineHeight(),
description: strings.lineHeightDesc(),
type: "input-selector",
property: "defaultLineHeight",
icon: "format-line-spacing",
minInputValue: EDITOR_LINE_HEIGHT.MIN,
maxInputValue: EDITOR_LINE_HEIGHT.MAX
},
{
id: "title-format",
name: strings.titleFormat(),
@@ -929,6 +890,7 @@ export const settingsGroups: SettingSection[] = [
hidden: (current) => (current as VaultStatusType)?.exists,
modifer: () => {
openVault({
item: {},
novault: false,
title: strings.createVault()
});
@@ -942,6 +904,7 @@ export const settingsGroups: SettingSection[] = [
hidden: (current) => !(current as VaultStatusType)?.exists,
modifer: () =>
openVault({
item: {},
changePassword: true,
novault: true,
title: strings.changeVaultPassword()
@@ -955,6 +918,7 @@ export const settingsGroups: SettingSection[] = [
hidden: (current) => !(current as VaultStatusType)?.exists,
modifer: () => {
openVault({
item: {},
clearVault: true,
novault: true,
title: strings.clearVault() + "?"
@@ -969,6 +933,7 @@ export const settingsGroups: SettingSection[] = [
hidden: (current) => !(current as VaultStatusType)?.exists,
modifer: () => {
openVault({
item: {},
deleteVault: true,
novault: true,
title: strings.deleteVault() + "?"
@@ -990,6 +955,7 @@ export const settingsGroups: SettingSection[] = [
modifer: (current) => {
const _current = current as VaultStatusType;
openVault({
item: {},
fingerprintAccess: !_current.biometryEnrolled,
revokeFingerprintAccess: _current.biometryEnrolled,
novault: true,
@@ -1357,7 +1323,7 @@ export const settingsGroups: SettingSection[] = [
if (settings.notifNotes) {
Notifications.unpinQuickNote();
} else {
Notifications.pinQuickNote();
Notifications.pinQuickNote(false);
}
SettingsService.set({
notifNotes: !settings.notifNotes

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 { LegendList } from "@legendapp/list";
import { strings } from "@notesnook/intl";
import {
THEME_COMPATIBILITY_VERSION,
ThemeDefinition,
@@ -30,7 +28,8 @@ import type {
ThemeMetadata,
ThemesRouter
} from "@notesnook/themes-server";
import { keepLocalCopy, pick } from "@react-native-documents/picker";
import { pick } from "@react-native-documents/picker";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
import { createTRPCReact } from "@trpc/react-query";
@@ -41,24 +40,23 @@ import {
TouchableOpacity,
View
} from "react-native";
import ReactNativeBlobUtil from "react-native-blob-util";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { DatabaseLogger, db } from "../../common/database";
import { santizeUri } from "../../common/filesystem/utils";
import SheetProvider from "../../components/sheet-provider";
import { Button } from "../../components/ui/button";
import { IconButton } from "../../components/ui/icon-button";
import Input from "../../components/ui/input";
import { Pressable } from "../../components/ui/pressable";
import Heading from "../../components/ui/typography/heading";
import Paragraph from "../../components/ui/typography/paragraph";
import { ToastManager, presentSheet } from "../../services/event-manager";
import { useThemeStore } from "../../stores/use-theme-store";
import { getColorLinearShade } from "../../utils/colors";
import { defaultBorderRadius, AppFontSize } from "../../utils/size";
import { getElevationStyle } from "../../utils/elevation";
import { MenuItemsList } from "../../utils/menu-items";
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
import { IconButton } from "../../components/ui/icon-button";
import { Pressable } from "../../components/ui/pressable";
import { getColorLinearShade } from "../../utils/colors";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../utils/styles";
import { LegendList } from "@legendapp/list";
const THEME_SERVER_URL = "https://themes-api.notesnook.com";
//@ts-ignore
@@ -414,43 +412,20 @@ function ThemeSelector() {
type={"secondaryAccented"}
icon="folder"
fontSize={AppFontSize.xs}
onPress={async () => {
try {
const pickResponse = await pick({
allowMultiSelection: false
onPress={() => {
pick({
allowMultiSelection: false
}).then((r) => {
fetch(r[0].uri).then(async (response) => {
const json = await response.json();
const result = validateTheme(json);
if (result.error) {
ToastManager.error(new Error(result.error));
return;
}
select(json, true);
});
const copiedFile = await keepLocalCopy({
destination: "cachesDirectory",
files: [
{
uri: pickResponse[0].uri,
fileName: pickResponse[0].name || "theme.json"
}
]
});
if (copiedFile[0].status !== "success") return;
const themeJsonCopiedPath = santizeUri(
copiedFile[0].localUri
);
const themeJson = await ReactNativeBlobUtil.fs.readFile(
themeJsonCopiedPath,
"utf8"
);
ReactNativeBlobUtil.fs
.unlink(themeJsonCopiedPath)
.catch(() => {});
const json = JSON.parse(themeJson);
const result = validateTheme(json);
if (result.error) {
ToastManager.error(new Error(result.error));
return;
}
select(json, true);
} catch (e) {
ToastManager.error(e as Error);
}
});
}}
/>
</View>

File diff suppressed because it is too large Load Diff

View File

@@ -114,7 +114,7 @@ async function onBackgroundSyncStarted() {
}
await Notifications.setupReminders();
if (SettingsService.get().notifNotes) {
Notifications.pinQuickNote();
Notifications.pinQuickNote(false);
}
Notifications.restorePinnedNotes();
NotePreviewWidget.updateNotes();
@@ -142,7 +142,7 @@ const onBoot = async () => {
await Notifications.setupReminders();
if (SettingsService.get().notifNotes) {
Notifications.pinQuickNote();
Notifications.pinQuickNote(false);
}
Notifications.restorePinnedNotes();
NotePreviewWidget.updateNotes();

View File

@@ -76,11 +76,11 @@ async function getCredentials(title?: string, description?: string) {
const options = Platform.select({
ios: {
fallbackEnabled: false,
description: description || title || "Unlock"
description: description
},
android: {
title: title,
description: description || "Unlock",
description: description,
deviceCredentialAllowed: false
}
});
@@ -132,11 +132,11 @@ async function validateUser(title: string, description?: string) {
Platform.select({
ios: {
fallbackEnabled: false,
description: title || "Unlock"
description: title
},
android: {
title: title,
description: description || "Unlock",
description: description,
deviceCredentialAllowed: false
}
}) as AuthenticateIOS

View File

@@ -50,7 +50,6 @@ export class DeviceDetectionService {
windowSize = size || windowSize;
this.width = windowSize.width;
this.height = windowSize.height;
this.orientation = orientation;
this.adjustedWidth = this.width * this.pixelDensity;
this.adjustedHeight = this.height * this.pixelDensity;
screenSize = Dimensions.get("screen");

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 { EventHandler, EventManager, Note, NoteContent } from "@notesnook/core";
import { EventHandler, EventManager } from "@notesnook/core";
import Clipboard from "@react-native-clipboard/clipboard";
import { RefObject } from "react";
import { ActionSheetRef } from "react-native-actions-sheet";
@@ -32,8 +32,8 @@ import {
} from "../utils/events";
import { strings } from "@notesnook/intl";
export type Vault = {
item: Note;
type Vault = {
item: unknown;
novault: boolean;
title: string;
description: string;
@@ -48,14 +48,6 @@ export type Vault = {
clearVault: boolean;
deleteVault: boolean;
copyNote: boolean;
customActionTitle: string;
customActionParagraph: string;
onUnlock: (
item: Note & {
content?: NoteContent<false>;
},
password: string
) => void;
};
type NoteEdit = {

View File

@@ -71,8 +71,7 @@ const routeNames = {
Archive: "Archive",
ManageTags: "ManageTags",
AddReminder: "AddReminder",
PayWall: "PayWall",
Wrapped: "Wrapped"
PayWall: "PayWall"
};
export type NavigationProps<T extends RouteName> = NativeStackScreenProps<

View File

@@ -109,15 +109,6 @@ async function initDatabase() {
const onEvent = async ({ type, detail }: Event) => {
await initDatabase();
const { notification, pressAction, input } = detail;
if (
type === EventType.DISMISSED &&
Platform.OS === "android" &&
notification?.id === "notesnook_note_input" &&
SettingsService.getProperty("notifNotes")
) {
pinQuickNote();
}
if (type === EventType.DELIVERED && Platform.OS === "android") {
if (notification?.id) {
const reminder = await db.reminders?.reminder(
@@ -447,7 +438,6 @@ async function scheduleNotification(
async function loadNote(id: string, jump: boolean) {
if (!id || id === "notesnook_note_input") return;
editorState().initialLoadCalled = true;
const note = await db.notes.note(id);
if (!note) return;
if (!DDS.isTab && jump) {
@@ -463,11 +453,9 @@ async function loadNote(id: string, jump: boolean) {
const tab = useTabStore.getState().getTabForNote(id);
if (useTabStore.getState().currentTab !== tab) {
setTimeout(() => {
eSendEvent(eOnLoadNote, {
item: note
});
}, 300);
eSendEvent(eOnLoadNote, {
item: note
});
}
}
@@ -514,8 +502,7 @@ async function displayNotification({
ongoing,
reply_placeholder_text,
reply_button_text,
id,
channelId = "default"
id
}: {
title?: string;
message: string;
@@ -526,7 +513,6 @@ async function displayNotification({
reply_placeholder_text?: string;
reply_button_text?: string;
id?: string;
channelId?: "silent" | "vibrate" | "urgent" | "default";
}) {
useUserStore.setState({
disableAppLockRequests: true
@@ -551,7 +537,7 @@ async function displayNotification({
ongoing: ongoing,
smallIcon: "ic_stat_name",
localOnly: true,
channelId: await getChannelId(channelId),
channelId: await getChannelId("default"),
autoCancel: false,
pressAction: {
id: "default",
@@ -898,7 +884,7 @@ function init() {
notifee.onForegroundEvent(onEvent);
}
async function pinQuickNote() {
async function pinQuickNote(launch: boolean) {
useUserStore.setState({
disableAppLockRequests: true
});
@@ -910,13 +896,14 @@ async function pinQuickNote() {
return;
}
get().then((items) => {
const notification = items.find((n) => n.id === "notesnook_note_input");
if (notification) return;
const notification = items.filter((n) => n.id === "notesnook_note_input");
if (notification && launch) {
return;
}
displayNotification({
title: strings.quickNoteTitle(),
message: strings.quickNoteContent(),
ongoing: true,
channelId: "silent",
actions: ["ReplyInput", strings.hide()],
reply_button_text: strings.takeNote(),
reply_placeholder_text: strings.quickNotePlaceholder(),
@@ -1043,7 +1030,6 @@ async function pinNote(id: string) {
subtitle: "",
bigText: html,
ongoing: true,
channelId: "silent",
actions: [strings.unpin()],
id: note.id
});
@@ -1054,13 +1040,9 @@ async function pinNote(id: string) {
async function restorePinnedNotes() {
const pinnedNotes = PinnedNotesStorage.get();
Notifications.get().then(() => {
for (const id of pinnedNotes) {
if (!isNotePinned(id)) {
pinNote(id);
}
}
});
for (const id of pinnedNotes) {
pinNote(id);
}
}
async function clearPinnedNotes() {

View File

@@ -23,7 +23,6 @@ import ShareExtension, {
import { getPreviewData } from "@flyerhq/react-native-link-preview";
import {
formatBytes,
getFormattedDate,
isFeatureAvailable,
useIsFeatureAvailable
} from "@notesnook/common";
@@ -48,6 +47,7 @@ import {
import RNFetchBlob from "react-native-blob-util";
import {
SafeAreaProvider,
SafeAreaView,
useSafeAreaInsets
} from "react-native-safe-area-context";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
@@ -70,20 +70,14 @@ import { Search } from "./search";
import { initDatabase, useShareStore } from "./store";
import { isTablet } from "react-native-device-info";
import { NotesnookModule } from "../utils/notesnook-module";
import { DefaultAppStyles } from "../utils/styles";
const getLinkPreview = (url: string) => {
return getPreviewData(url, 5000);
};
async function sanitizeHtml(site: string, title?: string) {
async function sanitizeHtml(site: string) {
try {
let html = await fetchHandle.current?.processUrl(site);
return (
html +
"<hr/>" +
`<p>Clipped from <a href="${site}">${title || site}</a></p>
<p>Date clipped ${getFormattedDate(Date.now())}</p>`
);
return html;
} catch (e) {
return "";
}
@@ -151,7 +145,6 @@ const ShareView = () => {
const noteContent = useRef<string>(undefined);
const noteTitle = useRef<string>(undefined);
const [loading, setLoading] = useState(false);
const [loadingData, setLoadingData] = useState(true);
const [loadingExtension, setLoadingExtension] = useState(true);
const fullQualityImages = useIsFeatureAvailable("fullQualityImages");
const [rawData, setRawData] = useState<{
@@ -174,11 +167,6 @@ const ShareView = () => {
globalThis["IS_SHARE_EXTENSION"] = true;
const onKeyboardDidShow = (event: KeyboardEvent) => {
let height = Dimensions.get("screen").height - event.endCoordinates.screenY;
if (height < 100) {
setKeyboardHeight(0);
keyboardHeightRef.current = 0;
return;
}
keyboardHeightRef.current = height;
setKeyboardHeight(height);
};
@@ -222,6 +210,7 @@ const ShareView = () => {
};
const onLoad = useCallback(() => {
console.log(noteContent.current, "current...");
eSendEvent(eOnLoadNote + "shareEditor", {
id: null,
content: {
@@ -237,7 +226,6 @@ const ShareView = () => {
try {
if (noteContent.current) {
onLoad();
setLoadingData(false);
return;
}
defaultNote.content.data = undefined;
@@ -254,7 +242,6 @@ const ShareView = () => {
editorRef.current?.focus();
}, 300);
}
setLoadingData(false);
return;
}
@@ -264,7 +251,6 @@ const ShareView = () => {
setRawData(item);
if (isURL(item.value)) {
note = await showLinkPreview(note, item.value);
noteTitle.current = note.title;
} else {
note.content.data = makeHtmlFromPlainText(item.value);
}
@@ -275,6 +261,9 @@ const ShareView = () => {
if (key.includes("TITLE") || key.includes("SUBJECT")) {
note.title = item[key];
noteTitle.current = note.title;
inputRef.current?.setNativeProps?.({
text: noteTitle.current
});
}
if (key.includes("TEXT") && !note.content.data) {
note.content.data = item[key];
@@ -305,15 +294,9 @@ const ShareView = () => {
}
}
}
inputRef.current?.setNativeProps?.({
text: noteTitle.current
});
onLoad();
setNote({ ...note });
setLoadingData(false);
} catch (e) {
setLoadingData(false);
console.error(e);
}
},
@@ -325,8 +308,8 @@ const ShareView = () => {
try {
await initDatabase();
setLoadingExtension(false);
useShareStore.getState().restore();
loadData(false);
useShareStore.getState().restore();
} catch (e) {
DatabaseLogger.error(e);
}
@@ -405,7 +388,7 @@ const ShareView = () => {
if (value === 2) {
setLoadingPage(true);
setTimeout(async () => {
let html = await sanitizeHtml(rawData?.value || "", note.title);
let html = await sanitizeHtml(rawData?.value || "");
noteContent.current = html || "";
setLoadingPage(false);
onLoad();
@@ -464,7 +447,11 @@ const ShareView = () => {
overflow: "hidden",
paddingTop: insets.top,
paddingLeft: insets.left,
paddingRight: insets.right
paddingRight: insets.right,
paddingBottom:
Platform.OS === "android" && keyboardHeight
? keyboardHeight - (insets.bottom || (gesturesEnabled ? 25 : 50))
: 0
}}
>
{loadingPage ? <HtmlLoadingWebViewAgent /> : null}
@@ -512,414 +499,394 @@ const ShareView = () => {
display: searchMode ? "none" : "flex",
borderTopRightRadius: Platform.OS === "ios" ? 10 : 15,
borderTopLeftRadius: Platform.OS === "ios" ? 10 : 15,
maxHeight: Platform.OS === "android" ? undefined : "100%",
paddingBottom:
Platform.OS === "android" && keyboardHeight
? keyboardHeight
: insets.bottom
maxHeight: Platform.OS === "android" ? undefined : "100%"
}}
>
{loadingData ? (
<ScrollView>
<View
style={{
justifyContent: "center",
alignItems: "center",
height: 300,
gap: DefaultAppStyles.GAP_VERTICAL
maxHeight: "100%"
}}
>
<ActivityIndicator color={colors.primary.accent} />
<Paragraph>Loading clip...</Paragraph>
</View>
) : (
<ScrollView>
<View
style={{
maxHeight: "100%"
width: "100%"
}}
>
<View
style={{
width: "100%"
minHeight: 100,
backgroundColor: colors.primary.background,
overflow: "hidden"
}}
>
<View
style={{
minHeight: 100,
backgroundColor: colors.primary.background,
overflow: "hidden"
justifyContent: "space-between",
flexDirection: "row",
alignItems: "center",
borderBottomWidth: 1,
paddingBottom: 12,
borderBottomColor: colors.secondary.background,
paddingHorizontal: 12,
gap: 10
}}
>
<View
style={{
justifyContent: "space-between",
flexDirection: "row",
alignItems: "center",
borderBottomWidth: 1,
paddingBottom: 12,
borderBottomColor: colors.secondary.background,
paddingHorizontal: 12,
gap: 10
}}
>
{appendNoteId ? (
<Heading
style={{
flexShrink: 1,
flexGrow: 1,
fontFamily: "Inter-SemiBold",
fontSize: AppFontSize.lg,
paddingBottom: 0,
paddingTop: 0
}}
allowFontScaling={false}
>
Save note
</Heading>
) : (
<TextInput
placeholder="Enter note title"
ref={inputRef}
allowFontScaling={false}
style={{
flexShrink: 1,
flexGrow: 1,
fontFamily: "Inter-SemiBold",
fontSize: AppFontSize.lg,
paddingBottom: 0,
paddingTop: 0,
color: colors.primary.heading
}}
onChangeText={(value) => {
noteTitle.current = value;
}}
defaultValue={noteTitle.current}
blurOnSubmit={false}
onSubmitEditing={() => {
editorRef.current?.focus();
}}
/>
)}
<Button
title="Done"
type="accent"
loading={loading}
onPress={onPress}
allowFontScaling={false}
/>
</View>
{rawFiles?.length > 0 ? (
<View
style={{
paddingHorizontal: 12,
paddingVertical: 12,
backgroundColor: colors.secondary.background
}}
>
<Paragraph
style={{ marginBottom: 6 }}
color={colors.primary.paragraph}
allowFontScaling={false}
>
Attaching {rawFiles.length} file(s):
</Paragraph>
<ScrollView horizontal>
{rawFiles.map((item) =>
isImage(item.type) || item.value?.endsWith(".png") ? (
<TouchableOpacity
onPress={() => onRemoveFile(item)}
key={item.name}
activeOpacity={0.9}
>
<Image
source={{
uri:
Platform.OS === "android"
? `file://${item.value}`
: item.value
}}
style={{
width: 100,
height: 100,
borderRadius: defaultBorderRadius,
backgroundColor: "black",
marginRight: 6
}}
resizeMode="cover"
/>
</TouchableOpacity>
) : (
<TouchableOpacity
activeOpacity={0.9}
key={item.name}
onPress={() => onRemoveFile(item)}
style={{
borderRadius: defaultBorderRadius,
backgroundColor: colors.secondary.background,
flexDirection: "row",
borderWidth: 1,
borderColor: colors.primary.border,
alignItems: "center",
paddingVertical: 5,
paddingHorizontal: 8,
marginRight: 6
}}
>
<Icon
color={colors.primary.icon}
size={15}
name="file"
/>
<Paragraph
size={AppFontSize.xs}
color={colors.primary.paragraph}
allowFontScaling={false}
style={{
marginLeft: 4,
paddingRight: 8
}}
>
{item.name || item.value.split("/").pop()}
{item.size ? `(${formatBytes(item.size)})` : ""}
</Paragraph>
</TouchableOpacity>
)
)}
</ScrollView>
<Paragraph
color={colors.secondary.paragraph}
size={AppFontSize.xs}
allowFontScaling={false}
style={{
marginTop: 6
}}
>
Tap to remove an attachment.
</Paragraph>
{rawFiles.some((item) => isImage(item.type)) ? (
<TouchableOpacity
activeOpacity={1}
style={{
flexDirection: "row",
alignSelf: "center",
alignItems: "center",
width: "100%",
marginTop: 6
}}
onPress={() => {
if (fullQualityImages?.isAllowed) {
setCompress(!compress);
}
}}
>
<Icon
size={20}
name={
compress
? "checkbox-marked"
: "checkbox-blank-outline"
}
allowFontScaling={false}
color={
compress && fullQualityImages?.isAllowed
? colors.primary.accent
: colors.primary.icon
}
/>
<Text
style={{
flexShrink: 1,
marginLeft: 3,
fontSize: 12
}}
allowFontScaling={false}
>
Compress image(s) (recommended)
</Text>
</TouchableOpacity>
) : null}
</View>
) : null}
<View
style={{
width: "100%",
height: rawFiles.length > 0 ? 100 : 200,
paddingBottom: 15,
marginBottom: 10,
borderBottomColor: colors.secondary.background,
borderBottomWidth: 1
}}
>
<SafeAreaProvider
style={{
flex: 1,
paddingTop: 10,
justifyContent: loadingPage ? "center" : undefined,
alignItems: loadingPage ? "center" : undefined
}}
>
{!loadingExtension && !loadingPage ? (
<>
<Editor
editorRef={editorRef}
onLoad={onLoadEditor}
onChange={(html) => {
noteContent.current = html;
}}
/>
</>
) : (
<>
{loadingPage ? (
<>
<ActivityIndicator
color={colors.primary.accent}
/>
<Paragraph>Preparing web clip...</Paragraph>
</>
) : null}
</>
)}
</SafeAreaProvider>
</View>
{appendNoteId ? (
<AppendNote
id={appendNoteId}
onLoad={(title) => {
if (!noteTitle.current) {
noteTitle.current = title;
inputRef.current?.setNativeProps?.({
text: noteTitle.current
});
}
<Heading
style={{
flexShrink: 1,
flexGrow: 1,
fontFamily: "Inter-SemiBold",
fontSize: AppFontSize.lg,
paddingBottom: 0,
paddingTop: 0
}}
allowFontScaling={false}
>
Save note
</Heading>
) : (
<TextInput
placeholder="Enter note title"
ref={inputRef}
allowFontScaling={false}
style={{
flexShrink: 1,
flexGrow: 1,
fontFamily: "Inter-SemiBold",
fontSize: AppFontSize.lg,
paddingBottom: 0,
paddingTop: 0,
color: colors.primary.heading
}}
onChangeText={(value) => {
noteTitle.current = value;
}}
defaultValue={noteTitle.current}
blurOnSubmit={false}
onSubmitEditing={() => {
editorRef.current?.focus();
}}
/>
) : null}
)}
<Button
title="Done"
type="accent"
loading={loading}
onPress={onPress}
allowFontScaling={false}
/>
</View>
{rawFiles?.length > 0 ? (
<View
style={{
flexDirection: "row",
paddingHorizontal: 12,
width: "100%",
alignItems: "center"
paddingVertical: 12,
backgroundColor: colors.secondary.background
}}
>
<Paragraph
style={{
marginRight: 10
}}
style={{ marginBottom: 6 }}
color={colors.primary.paragraph}
allowFontScaling={false}
>
Clip Mode:
Attaching {rawFiles.length} file(s):
</Paragraph>
{rawData.value && isURL(rawData.value) ? (
<Button
type={mode === 2 ? "inverted" : "plain"}
icon={mode === 2 ? "radiobox-marked" : "radiobox-blank"}
onPress={() => changeMode(2)}
title={modes[2].title}
height={30}
allowFontScaling={false}
style={{
paddingHorizontal: 6
}}
/>
) : null}
<Button
type={mode === 1 ? "inverted" : "plain"}
icon={mode === 1 ? "radiobox-marked" : "radiobox-blank"}
onPress={() => changeMode(1)}
<ScrollView horizontal>
{rawFiles.map((item) =>
isImage(item.type) || item.value?.endsWith(".png") ? (
<TouchableOpacity
onPress={() => onRemoveFile(item)}
key={item.name}
activeOpacity={0.9}
>
<Image
source={{
uri:
Platform.OS === "android"
? `file://${item.value}`
: item.value
}}
style={{
width: 100,
height: 100,
borderRadius: defaultBorderRadius,
backgroundColor: "black",
marginRight: 6
}}
resizeMode="cover"
/>
</TouchableOpacity>
) : (
<TouchableOpacity
activeOpacity={0.9}
key={item.name}
onPress={() => onRemoveFile(item)}
style={{
borderRadius: defaultBorderRadius,
backgroundColor: colors.secondary.background,
flexDirection: "row",
borderWidth: 1,
borderColor: colors.primary.border,
alignItems: "center",
paddingVertical: 5,
paddingHorizontal: 8,
marginRight: 6
}}
>
<Icon
color={colors.primary.icon}
size={15}
name="file"
/>
<Paragraph
size={AppFontSize.xs}
color={colors.primary.paragraph}
allowFontScaling={false}
style={{
marginLeft: 4,
paddingRight: 8
}}
>
{item.name || item.value.split("/").pop()}
{item.size ? `(${formatBytes(item.size)})` : ""}
</Paragraph>
</TouchableOpacity>
)
)}
</ScrollView>
<Paragraph
color={colors.secondary.paragraph}
size={AppFontSize.xs}
allowFontScaling={false}
title={modes[1].title}
style={{
marginTop: 6
}}
>
Tap to remove an attachment.
</Paragraph>
{rawFiles.some((item) => isImage(item.type)) ? (
<TouchableOpacity
activeOpacity={1}
style={{
flexDirection: "row",
alignSelf: "center",
alignItems: "center",
width: "100%",
marginTop: 6
}}
onPress={() => {
if (fullQualityImages?.isAllowed) {
setCompress(!compress);
}
}}
>
<Icon
size={20}
name={
compress
? "checkbox-marked"
: "checkbox-blank-outline"
}
allowFontScaling={false}
color={
compress && fullQualityImages?.isAllowed
? colors.primary.accent
: colors.primary.icon
}
/>
<Text
style={{
flexShrink: 1,
marginLeft: 3,
fontSize: 12
}}
allowFontScaling={false}
>
Compress image(s) (recommended)
</Text>
</TouchableOpacity>
) : null}
</View>
) : null}
<View
style={{
width: "100%",
height: rawFiles.length > 0 ? 100 : 200,
paddingBottom: 15,
marginBottom: 10,
borderBottomColor: colors.secondary.background,
borderBottomWidth: 1
}}
>
<SafeAreaProvider
style={{
flex: 1,
paddingTop: 10,
justifyContent: loadingPage ? "center" : undefined,
alignItems: loadingPage ? "center" : undefined
}}
>
{!loadingExtension && !loadingPage ? (
<>
<Editor
editorRef={editorRef}
onLoad={onLoadEditor}
onChange={(html) => {
noteContent.current = html;
}}
/>
</>
) : (
<>
{loadingPage ? (
<>
<ActivityIndicator color={colors.primary.accent} />
<Paragraph>Preparing web clip...</Paragraph>
</>
) : null}
</>
)}
</SafeAreaProvider>
</View>
{appendNoteId ? (
<AppendNote
id={appendNoteId}
onLoad={(title) => {
if (!noteTitle.current) {
noteTitle.current = title;
inputRef.current?.setNativeProps?.({
text: noteTitle.current
});
}
}}
/>
) : null}
<View
style={{
flexDirection: "row",
paddingHorizontal: 12,
width: "100%",
alignItems: "center"
}}
>
<Paragraph
style={{
marginRight: 10
}}
allowFontScaling={false}
>
Clip Mode:
</Paragraph>
{rawData.value && isURL(rawData.value) ? (
<Button
type={mode === 2 ? "inverted" : "plain"}
icon={mode === 2 ? "radiobox-marked" : "radiobox-blank"}
onPress={() => changeMode(2)}
title={modes[2].title}
height={30}
allowFontScaling={false}
style={{
paddingHorizontal: 6
}}
/>
</View>
) : null}
<Button
type={mode === 1 ? "inverted" : "plain"}
icon={mode === 1 ? "radiobox-marked" : "radiobox-blank"}
onPress={() => changeMode(1)}
allowFontScaling={false}
title={modes[1].title}
height={30}
style={{
paddingHorizontal: 6
}}
/>
</View>
</View>
</View>
<View
style={{
width: "100%",
borderRadius: 10,
flexDirection: "column",
marginTop: 10,
alignSelf: "center",
alignItems: "center",
paddingHorizontal: 12,
gap: 10
<View
style={{
width: "100%",
borderRadius: 10,
flexDirection: "column",
marginTop: 10,
alignSelf: "center",
alignItems: "center",
paddingHorizontal: 12,
gap: 10
}}
>
<Button
icon="plus"
onPress={() => {
useShareStore.getState().setAppendNote(null);
}}
>
<Button
icon="plus"
onPress={() => {
useShareStore.getState().setAppendNote(null);
}}
type={!appendNoteId ? "transparent" : "plain"}
title="New note"
allowFontScaling={false}
style={{
paddingHorizontal: 12,
height: 45,
width: "100%",
marginRight: 0,
borderWidth: 1,
borderColor: colors.secondary.background,
justifyContent: "flex-start"
}}
/>
<Button
icon="text-short"
onPress={() => {
setSearchMode("appendNote");
}}
allowFontScaling={false}
type={appendNoteId ? "transparent" : "plain"}
title={`Append to a note`}
style={{
paddingHorizontal: 12,
height: 45,
width: "100%",
marginRight: 0,
borderWidth: 1,
borderColor: colors.secondary.background,
justifyContent: "flex-start"
}}
/>
{!appendNoteId ? (
<AddTags
onPress={() => {
setSearchMode("selectTags");
}}
/>
) : null}
{!appendNoteId ? (
<AddNotebooks
onPress={() => {
setSearchMode("selectNotebooks");
}}
/>
) : null}
</View>
<View
type={!appendNoteId ? "transparent" : "plain"}
title="New note"
allowFontScaling={false}
style={{
height: isTablet() ? 150 : Platform.OS === "ios" ? 110 : 0
paddingHorizontal: 12,
height: 45,
width: "100%",
marginRight: 0,
borderWidth: 1,
borderColor: colors.secondary.background,
justifyContent: "flex-start"
}}
/>
<Button
icon="text-short"
onPress={() => {
setSearchMode("appendNote");
}}
allowFontScaling={false}
type={appendNoteId ? "transparent" : "plain"}
title={`Append to a note`}
style={{
paddingHorizontal: 12,
height: 45,
width: "100%",
marginRight: 0,
borderWidth: 1,
borderColor: colors.secondary.background,
justifyContent: "flex-start"
}}
/>
{!appendNoteId ? (
<AddTags
onPress={() => {
setSearchMode("selectTags");
}}
/>
) : null}
{!appendNoteId ? (
<AddNotebooks
onPress={() => {
setSearchMode("selectNotebooks");
}}
/>
) : null}
</View>
</ScrollView>
)}
<View
style={{
height: isTablet() ? 150 : Platform.OS === "ios" ? 110 : 0
}}
/>
</View>
</ScrollView>
</WrapperView>
</View>
);

View File

@@ -17,13 +17,12 @@ 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 { DatabaseLogger, db } from "../common/database";
import { db } from "../common/database";
import { eSendEvent } from "../services/event-manager";
import Navigation from "../services/navigation";
import { NotePreviewWidget } from "../services/note-preview-widget";
import Notifications from "../services/notifications";
import { eAfterSync } from "../utils/events";
import { NotesnookModule, ShortcutInfo } from "../utils/notesnook-module";
import { useFavoriteStore } from "./use-favorite-store";
import { useMenuStore } from "./use-menu-store";
import { useMonographStore } from "./use-monograph-store";
@@ -35,81 +34,6 @@ import { useTagStore } from "./use-tag-store";
import { useTrashStore } from "./use-trash-store";
import { useUserStore } from "./use-user-store";
async function syncShortcuts(result: ShortcutInfo[]) {
try {
for (let shortcut of result) {
switch (shortcut.type) {
case "note":
{
const note = await db.notes.note(shortcut.id);
if (!note) {
NotesnookModule.removeShortcut(shortcut.id);
} else if (note.title !== shortcut.title) {
NotesnookModule.updateShortcut(
shortcut.id,
"note",
note.title,
note.headline
);
}
}
break;
case "notebook":
{
const notebook = await db.notebooks.notebook(shortcut.id);
if (!notebook) {
NotesnookModule.removeShortcut(shortcut.id);
} else if (notebook.title !== shortcut.title) {
NotesnookModule.updateShortcut(
shortcut.id,
"notebook",
notebook.title,
notebook.description
);
}
}
break;
case "tag":
{
const tag = await db.tags.tag(shortcut.id);
if (!tag) {
NotesnookModule.removeShortcut(shortcut.id);
} else if (tag.title !== shortcut.title) {
NotesnookModule.updateShortcut(
shortcut.id,
"tag",
tag.title,
tag.title
);
}
}
break;
case "color":
{
const color = await db.colors.color(shortcut.id);
if (!color) {
NotesnookModule.removeShortcut(shortcut.id);
} else if (color.title !== shortcut.title) {
NotesnookModule.updateShortcut(
shortcut.id,
"color",
color.title,
color.title,
color.colorCode
);
}
}
break;
}
}
} catch (e) {
DatabaseLogger.error(
e as Error,
"Error while syncing homescreen shortcuts"
);
}
}
export function initAfterSync(type: "full" | "send" = "send") {
if (type === "full") {
Navigation.queueRoutesForUpdate();
@@ -122,16 +46,9 @@ export function initAfterSync(type: "full" | "send" = "send") {
profile: db.settings.getProfile()
});
}
Notifications.setupReminders(true);
NotePreviewWidget.updateNotes();
eSendEvent(eAfterSync);
NotesnookModule.getAllShortcuts()
.then(syncShortcuts)
.catch((e) => {
DatabaseLogger.log(e);
});
}
export async function initialize() {}

View File

@@ -25,7 +25,8 @@ import {
Note,
Notebook,
Reminder,
Tag
Tag,
TrashItem
} from "@notesnook/core";
import { ParamListBase } from "@react-navigation/core";
import { create } from "zustand";
@@ -35,15 +36,12 @@ export type GenericRouteParam = {
};
export type NotebookScreenParams = {
id: string;
item?: Notebook;
item: Notebook;
canGoBack?: boolean;
};
export type NotesScreenParams = {
type: "tag" | "color" | "monograph";
id: string;
item?: Tag | Color;
item: Note | Notebook | Tag | Color | TrashItem | Reminder;
canGoBack?: boolean;
};
@@ -114,7 +112,6 @@ export interface RouteParams extends ParamListBase {
context: "signup" | "logged-in" | "logged-out" | "subscribed";
state?: BillingState;
};
Wrapped: GenericRouteParam;
}
export type RouteName = keyof RouteParams;

View File

@@ -24,9 +24,7 @@ import { initialWindowMetrics } from "react-native-safe-area-context";
import { FileType } from "react-native-scoped-storage";
import { create } from "zustand";
import { ThemeDark, ThemeLight, ThemeDefinition } from "@notesnook/theme";
import { DayFormat, WeekFormat, Reminder } from "@notesnook/core";
import { db } from "../common/database";
import { EDITOR_LINE_HEIGHT } from "../utils/constants";
import { Reminder } from "@notesnook/core";
export const HostIds = [
"API_HOST",
"AUTH_HOST",
@@ -101,7 +99,6 @@ export type Settings = {
serverUrls?: Record<HostId, string>;
defaultSidebarTab: number;
checkForUpdates?: boolean;
defaultLineHeight: number;
};
type DimensionsType = {
@@ -137,12 +134,8 @@ export interface SettingStore {
setInsets: (insets: Insets) => void;
timeFormat: string;
dateFormat: string;
dayFormat: DayFormat;
weekFormat: WeekFormat;
dbPassword?: string;
isOldAppLock: () => boolean;
initialUrl: string | null;
refresh: () => void;
}
const { width, height } = Dimensions.get("window");
@@ -205,8 +198,7 @@ export const defaultSettings: SettingStore["settings"] = {
backupType: "partial",
fullBackupReminder: "never",
lastFullBackupDate: 0,
checkForUpdates: true,
defaultLineHeight: EDITOR_LINE_HEIGHT.DEFAULT
checkForUpdates: true
};
export const useSettingStore = create<SettingStore>((set, get) => ({
@@ -229,8 +221,6 @@ export const useSettingStore = create<SettingStore>((set, get) => ({
setInsets: (insets) => set({ insets }),
timeFormat: "12-hour",
dateFormat: "DD-MM-YYYY",
dayFormat: "short",
weekFormat: "Sun",
setAppDidEnterBackgroundForAction: (value: boolean) => {
set({
appDidEnterBackgroundForAction: value
@@ -246,14 +236,5 @@ export const useSettingStore = create<SettingStore>((set, get) => ({
},
insets: initialWindowMetrics?.insets
? initialWindowMetrics.insets
: { top: 0, right: 0, left: 0, bottom: 0 },
initialUrl: null,
refresh: () => {
set({
dayFormat: db.settings.getDayFormat(),
timeFormat: db.settings.getTimeFormat(),
dateFormat: db.settings?.getTimeFormat(),
weekFormat: db.settings.getWeekFormat()
});
}
: { top: 0, right: 0, left: 0, bottom: 0 }
}));

View File

@@ -122,9 +122,3 @@ export const SUBSCRIPTION_PROVIDER = {
icon: "web"
}
};
export const EDITOR_LINE_HEIGHT = {
DEFAULT: 1.2,
MAX: 10,
MIN: 1
};

View File

@@ -35,10 +35,6 @@ import { useTagStore } from "../stores/use-tag-store";
import { eUpdateNoteInEditor } from "./events";
import { unlockVault } from "./unlock-vault";
export const valueLimiter = (value: number, min: number, max: number) => {
return value < min ? min : value > max ? max : value;
};
export function getObfuscatedEmail(email: string) {
if (!email) return "";
const [username, provider] = email.split("@");

View File

@@ -19,13 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { NativeModules, Platform } from "react-native";
export type ShortcutInfo = {
id: string;
title: string;
description: string;
type: "note" | "notebook" | "tag" | "color";
};
interface NotesnookModuleInterface {
getActivityName: () => Promise<string>;
setBackgroundColor: (color: string) => void;
@@ -48,23 +41,6 @@ interface NotesnookModuleInterface {
updateWidgetNote: (noteId: string, data: string) => void;
updateReminderWidget: () => void;
isGestureNavigationEnabled: () => boolean;
addShortcut: (
id: string,
type: "note" | "notebook" | "tag" | "color",
title: string,
description?: string,
color?: string
) => Promise<boolean>;
removeShortcut: (id: string) => Promise<boolean>;
updateShortcut: (
id: string,
type: "note" | "notebook" | "tag" | "color",
title: string,
description?: string,
color?: string
) => Promise<boolean>;
removeAllShortcuts: () => Promise<boolean>;
getAllShortcuts: () => Promise<ShortcutInfo[]>;
}
export const NotesnookModule: NotesnookModuleInterface = Platform.select({
@@ -85,12 +61,7 @@ export const NotesnookModule: NotesnookModuleInterface = Platform.select({
hasWidgetNote: () => {},
updateWidgetNote: () => {},
updateReminderWidget: () => {},
isGestureNavigationEnabled: () => true,
addShortcut: () => Promise.resolve(false),
removeShortcut: () => Promise.resolve(false),
updateShortcut: () => Promise.resolve(false),
removeAllShortcuts: () => Promise.resolve(false),
getAllShortcuts: () => Promise.resolve([])
isGestureNavigationEnabled: () => true
},
android: NativeModules.NNativeModule
});

View File

@@ -23,7 +23,7 @@ module.exports = function (api, opts) {
});
}
env.plugins.push("react-native-worklets/plugin");
env.plugins.push("react-native-reanimated/plugin");
api.cache(true);
return env;

View File

@@ -121,7 +121,7 @@ describe("NOTEBOOKS", () => {
.run();
});
it("Remove note from notebook", async () => {
it.only("Remove note from notebook", async () => {
await TestBuilder.create()
.prepare()
.openSideMenu()
@@ -138,8 +138,7 @@ describe("NOTEBOOKS", () => {
.longPressByText(note.body)
.wait(500)
.waitAndTapById("select-minus")
.wait(500)
.isNotVisibleByText(note.body)
.isNotVisibleById(note.title)
.run();
})
.run();
@@ -200,7 +199,6 @@ describe("NOTEBOOKS", () => {
.longPressByText("Notebook 1")
.wait(500)
.waitAndTapByText("Move to trash")
.wait(500)
.waitAndTapByText("Delete")
.waitAndTapById("tab-home")
.waitAndTapByText("Trash")

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