Compare commits

..

4 Commits

Author SHA1 Message Date
Abdullah Atta
e6ff0a2430 editor: adjust font size and padding of inline code 2025-11-24 11:51:24 +05:00
01zulfi
eab0bdee8b editor: update text color of inline code
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-11-24 10:10:01 +05:00
01zulfi
0d94797bc1 editor: remove top&bottom padding from inline code
* it was cutting off a bit of text from top&bottom lines

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-11-24 09:36:24 +05:00
01zulfi
372c49d81e editor: improve inline code styling
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-11-21 15:46:24 +05:00
707 changed files with 50712 additions and 49053 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

@@ -5,6 +5,7 @@ on: workflow_dispatch
jobs:
build:
runs-on: ubuntu-22.04
timeout-minutes: 40
env:
CMAKE_C_COMPILER_LAUNCHER: ccache
CMAKE_CXX_COMPILER_LAUNCHER: ccache
@@ -30,23 +31,7 @@ jobs:
npm run bootstrap -- --scope=mobile
- name: Make Gradlew Executable
run: cd apps/mobile/android && chmod +x ./gradlew
- 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
run: cd apps/mobile/native/android && chmod +x ./gradlew
- name: Install CCache
uses: hendrikmuhs/ccache-action@v1.2.11
@@ -94,9 +79,6 @@ jobs:
cd apps/mobile
npx tsc --noEmit
- name: Remove debug keystore
run: rm -rf apps/mobile/android/app/debug.keystore
- name: Build unsigned app bundle
run: yarn release:android:bundle
@@ -107,13 +89,13 @@ jobs:
id: sign_app
uses: r0adkll/sign-android-release@master
with:
releaseDirectory: apps/mobile/android/app/build/outputs/bundle/release
releaseDirectory: apps/mobile/native/android/app/build/outputs/bundle/release
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.ALIAS }}
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
@@ -122,17 +104,17 @@ jobs:
id: sign_apk
uses: r0adkll/sign-android-release@master
with:
releaseDirectory: apps/mobile/android/app/build/outputs/apk/release
releaseDirectory: apps/mobile/native/android/app/build/outputs/apk/release
signingKeyBase64: ${{ secrets.PUBLIC_SIGNING_KEY }}
alias: ${{ secrets.PUBLIC_ALIAS }}
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: |
cd apps/mobile/android/app/build/outputs/apk/release/
cd apps/mobile/native/android/app/build/outputs/apk/release/
mv app-arm64-v8a-release-unsigned-signed.apk notesnook-arm64-v8a.apk
mv app-armeabi-v7a-release-unsigned-signed.apk notesnook-armeabi-v7a.apk
mv app-x86-release-unsigned-signed.apk notesnook-x86.apk
@@ -140,7 +122,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
@@ -153,21 +135,21 @@ jobs:
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
track: beta
status: completed
whatsNewDirectory: apps/mobile/android/releasenotes/
whatsNewDirectory: apps/mobile/native/android/releasenotes/
- name: Create release draft on Github
uses: softprops/action-gh-release@v1
with:
draft: true
tag_name: ${{ steps.package-version.outputs.version}}-beta-android
name: Notesnook Android v${{ steps.package-version.outputs.version}}
tag_name: ${{ steps.package-version.outputs.current-version}}-beta-android
name: Notesnook Android v${{ steps.package-version.outputs.current-version}}
repository: streetwriters/notesnook
token: ${{ secrets.GITHUB_TOKEN }}
files: |
apps/mobile/android/app/build/outputs/apk/release/notesnook-arm64-v8a.apk
apps/mobile/android/app/build/outputs/apk/release/notesnook-armeabi-v7a.apk
apps/mobile/android/app/build/outputs/apk/release/notesnook-x86.apk
apps/mobile/android/app/build/outputs/apk/release/notesnook-x86_64.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-arm64-v8a.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-armeabi-v7a.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
${{steps.sign_app.outputs.signedReleaseFile}}
- name: Upload sourcemaps
@@ -175,5 +157,5 @@ jobs:
with:
name: sourcemaps
path: |
apps/mobile/android/app/build/**/*.map
apps/mobile/native/android/app/build/**/*.map
packages/editor-mobile/sourcemaps/*.map

View File

@@ -5,6 +5,7 @@ on: workflow_dispatch
jobs:
build:
runs-on: ubuntu-22.04
timeout-minutes: 40
env:
CMAKE_C_COMPILER_LAUNCHER: ccache
CMAKE_CXX_COMPILER_LAUNCHER: ccache
@@ -24,29 +25,13 @@ 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
npm run bootstrap -- --scope=mobile
- name: Make Gradlew Executable
run: cd apps/mobile/android && chmod +x ./gradlew
run: cd apps/mobile/native/android && chmod +x ./gradlew
- name: Install CCache
uses: hendrikmuhs/ccache-action@v1.2.11
@@ -94,9 +79,6 @@ jobs:
cd apps/mobile
npx tsc --noEmit
- name: Remove debug keystore
run: rm -rf apps/mobile/android/app/debug.keystore
- name: Build unsigned app bundle
run: yarn release:android:bundle
@@ -107,13 +89,13 @@ jobs:
id: sign_app
uses: r0adkll/sign-android-release@master
with:
releaseDirectory: apps/mobile/android/app/build/outputs/bundle/release
releaseDirectory: apps/mobile/native/android/app/build/outputs/bundle/release
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.ALIAS }}
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
@@ -122,17 +104,17 @@ jobs:
id: sign_apk
uses: r0adkll/sign-android-release@master
with:
releaseDirectory: apps/mobile/android/app/build/outputs/apk/release
releaseDirectory: apps/mobile/native/android/app/build/outputs/apk/release
signingKeyBase64: ${{ secrets.PUBLIC_SIGNING_KEY }}
alias: ${{ secrets.PUBLIC_ALIAS }}
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: |
cd apps/mobile/android/app/build/outputs/apk/release/
cd apps/mobile/native/android/app/build/outputs/apk/release/
mv app-arm64-v8a-release-unsigned-signed.apk notesnook-arm64-v8a.apk
mv app-armeabi-v7a-release-unsigned-signed.apk notesnook-armeabi-v7a.apk
mv app-x86-release-unsigned-signed.apk notesnook-x86.apk
@@ -140,7 +122,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
@@ -153,26 +135,26 @@ jobs:
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
track: production
status: completed
whatsNewDirectory: apps/mobile/android/releasenotes/
whatsNewDirectory: apps/mobile/native/android/releasenotes/
- name: Create release draft on Github
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: |
apps/mobile/android/app/build/outputs/apk/release/notesnook-arm64-v8a.apk
apps/mobile/android/app/build/outputs/apk/release/notesnook-armeabi-v7a.apk
apps/mobile/android/app/build/outputs/apk/release/notesnook-x86.apk
apps/mobile/android/app/build/outputs/apk/release/notesnook-x86_64.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-arm64-v8a.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-armeabi-v7a.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
- name: Upload sourcemaps
uses: actions/upload-artifact@v4
with:
name: sourcemaps
path: |
apps/mobile/android/app/build/**/*.map
apps/mobile/native/android/app/build/**/*.map
packages/editor-mobile/sourcemaps/*.map

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.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/desktop",
"version": "3.3.8",
"version": "3.3.5",
"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.5",
"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

@@ -1,92 +0,0 @@
/** @type {Detox.DetoxConfig} */
module.exports = {
testRunner: {
args: {
$0: "jest",
config: "e2e/jest.config.js"
},
jest: {
setupTimeout: 120000
}
},
apps: {
"ios.debug": {
type: "ios.app",
binaryPath:
"ios/build/Build/Products/Debug-iphonesimulator/Notesnook.app",
build:
"xcodebuild -workspace ios/Notesnook.xcworkspace -scheme YOUR_APP -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build"
},
"ios.release": {
type: "ios.app",
binaryPath:
"ios/build/Build/Products/Release-iphonesimulator/Notesnook.app",
build:
"xcodebuild -workspace ios/Notesnook.xcworkspace -scheme YOUR_APP -configuration Release -sdk iphonesimulator -derivedDataPath ios/build"
},
"android.debug": {
type: "android.apk",
binaryPath: "android/app/build/outputs/apk/debug/app-arm64-v8a-debug.apk",
testBinaryPath:
"android/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk",
build:
"cd android ; ENVFILE=.env.test ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug && cd ..",
reversePorts: [8081]
},
"android.release": {
type: "android.apk",
binaryPath:
"android/app/build/outputs/apk/release/app-arm64-v8a-release.apk",
testBinaryPath:
"android/app/build/outputs/apk/androidTest/release/app-release-androidTest.apk",
build:
"cd android ; ENVFILE=.env.test ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release ; cd .."
}
},
devices: {
simulator: {
type: "ios.simulator",
device: {
type: "iPhone 17 Pro Max"
}
},
attached: {
type: "android.attached",
device: {
adbName: ".*"
}
},
emulator: {
type: "android.emulator",
device: {
avdName: "Pixel_5_API_36"
}
}
},
configurations: {
"ios.sim.debug": {
device: "simulator",
app: "ios.debug"
},
"ios.sim.release": {
device: "simulator",
app: "ios.release"
},
"android.att.debug": {
device: "attached",
app: "android.debug"
},
"android.att.release": {
device: "attached",
app: "android.release"
},
"android.emu.debug": {
device: "emulator",
app: "android.debug"
},
"android.emu.release": {
device: "emulator",
app: "android.release"
}
}
};

View File

@@ -4,7 +4,7 @@ artifacts/
#
.DS_Store
android/app/src/main/assets/
native/android/app/src/main/assets/
*Issues.md
build_cache/
#
@@ -16,12 +16,11 @@ build_cache/
.cxx/
*.keystore
!debug.keystore
.kotlin/
# Xcode
#
ios/Pods
ios/DerivedData
native/ios/Pods
native/ios/DerivedData
build/
*.pbxuser
!default.pbxuser
@@ -40,7 +39,7 @@ DerivedData
*.xcuserstate
*.hprof
**/.xcode.env.local
cache
native/cache
# Android/IntelliJ
#
rn-build-deps/
@@ -58,7 +57,7 @@ yarn-error.log
# BUCK
buck-out/
\.buckd/
*.keystore
# fastlane
#
@@ -70,7 +69,7 @@ buck-out/
*/fastlane/report.xml
*/fastlane/Preview.html
*/fastlane/screenshots
vendor
native/vendor
# Bundle artifact
*.jsbundle

View File

@@ -1,28 +0,0 @@
package com.streetwriters.notesnook
import android.app.Application
import com.facebook.react.PackageList
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(
context = applicationContext,
packageList =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
add(NNativeModulePackage());
},
)
}
override fun onCreate() {
super.onCreate()
registerActivityLifecycleCallbacks(OrientationActivityLifecycle.getInstance());
loadReactNative(this)
}
}

View File

@@ -1,465 +0,0 @@
package com.streetwriters.notesnook;
import android.app.Activity;
import android.appwidget.AppWidgetManager;
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;
import android.util.Log;
import android.view.WindowManager;
import android.widget.RemoteViews;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.google.gson.Gson;
import com.streetwriters.notesnook.datatypes.Note;
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 {
Intent lastIntent;
ReactContext mContext;
static String IntentType = "com.streetwriters.notesnook.IntentType";
public RCTNNativeModule(ReactApplicationContext reactContext) {
super(reactContext);
mContext = reactContext;
}
@Override
public String getName() {
return "NNativeModule";
}
@ReactMethod
public void setBackgroundColor(final String color) {
try {
getCurrentActivity().getWindow().getDecorView().setBackgroundColor(Color.parseColor(color));
} catch (Exception e) {
}
}
@ReactMethod
public void getActivityName(Promise promise) {
try {
promise.resolve(getCurrentActivity().getClass().getSimpleName());
} catch (Exception e) {
promise.resolve(null);
}
}
@ReactMethod
public void setSecureMode(final boolean mode) {
try {
getCurrentActivity().runOnUiThread(() -> {
try {
if (mode)
getCurrentActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
else
getCurrentActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
} catch (Exception e) {
}
});
} catch (Exception e) {
}
}
@ReactMethod
public void setAppState(final String appState) {
SharedPreferences appStateDetails = getReactApplicationContext().getSharedPreferences("appStateDetails", Context.MODE_PRIVATE);
SharedPreferences.Editor edit = appStateDetails.edit();
edit.putString("appState", appState);
edit.apply();
}
@ReactMethod(isBlockingSynchronousMethod = true)
public String getAppState() {
SharedPreferences appStateDetails = getReactApplicationContext().getSharedPreferences("appStateDetails", Context.MODE_PRIVATE);
String appStateValue = appStateDetails.getString("appState", "");
return appStateValue.isEmpty() ? null : appStateValue;
}
@ReactMethod(isBlockingSynchronousMethod = true)
public int getWidgetId() {
return NotePreviewConfigureActivity.appWidgetId;
}
@ReactMethod
public void setString(final String storeName, final String key, final String value) {
SharedPreferences details = getReactApplicationContext().getSharedPreferences(storeName, Context.MODE_PRIVATE);
SharedPreferences.Editor edit = details.edit();
edit.putString(key, value);
edit.apply();
}
@ReactMethod
public void removeString(final String storeName, final String key) {
SharedPreferences details = getReactApplicationContext().getSharedPreferences(storeName, Context.MODE_PRIVATE);
SharedPreferences.Editor edit = details.edit();
edit.remove(key);
edit.apply();
}
@ReactMethod
public void getString(final String storeName, final String key, Promise promise) {
SharedPreferences details = getReactApplicationContext().getSharedPreferences(storeName, Context.MODE_PRIVATE);
String value = details.getString(key, "");
promise.resolve(value.isEmpty() ? null : value);
}
@ReactMethod
public void saveAndFinish() {
NotePreviewConfigureActivity.saveAndFinish(mContext);
}
@ReactMethod(isBlockingSynchronousMethod = true)
public WritableMap getIntent() {
WritableMap map = Arguments.createMap();
if (getCurrentActivity() != null) {
Intent intent = getCurrentActivity().getIntent();
Bundle extras = getCurrentActivity().getIntent().getExtras();
if (extras != null && intent != lastIntent) {
lastIntent = intent;
if (Objects.equals(extras.getString(IntentType), "NewReminder")) {
map.putString(ReminderWidgetProvider.NewReminder, extras.getString(ReminderWidgetProvider.NewReminder));
} else if (Objects.equals(extras.getString(IntentType), "OpenReminder")) {
map.putString(ReminderViewsService.OpenReminderId, extras.getString(ReminderViewsService.OpenReminderId));
} else if (Objects.equals(extras.getString(IntentType), "OpenNote")) {
map.putString(NotePreviewWidget.OpenNoteId, extras.getString(NotePreviewWidget.OpenNoteId));
}
}
}
return map;
}
@ReactMethod
public void cancelAndFinish() {
NotePreviewConfigureActivity.activity.setResult(Activity.RESULT_CANCELED);
NotePreviewConfigureActivity.activity.finish();
}
@ReactMethod
public void getWidgetNotes(Promise promise) {
SharedPreferences pref = getReactApplicationContext().getSharedPreferences("appPreview", Context.MODE_PRIVATE);
Map<String, ?> map = pref.getAll();
WritableArray arr = Arguments.createArray();
for(Map.Entry<String,?> entry : map.entrySet()){
if (entry.getKey().equals("remindersList")) continue;
String value = (String) entry.getValue();
Gson gson = new Gson();
Note note = gson.fromJson(value, Note.class);
arr.pushString(note.getId());
}
promise.resolve(arr);
}
@ReactMethod
public void hasWidgetNote(final String noteId, Promise promise) {
SharedPreferences pref = getReactApplicationContext().getSharedPreferences("appPreview", Context.MODE_PRIVATE);
Map<String, ?> map = pref.getAll();
boolean found = false;
for(Map.Entry<String,?> entry : map.entrySet()){
String value = (String) entry.getValue();
if (value.contains(noteId)) {
found = true;
}
}
promise.resolve(found);
}
@ReactMethod
public void updateWidgetNote(final String noteId, final String data) {
SharedPreferences pref = getReactApplicationContext().getSharedPreferences("appPreview", Context.MODE_PRIVATE);
Map<String, ?> map = pref.getAll();
SharedPreferences.Editor edit = pref.edit();
ArrayList<String> ids = new ArrayList<>();
for(Map.Entry<String,?> entry : map.entrySet()) {
String value = (String) entry.getValue();
if (value.contains(noteId)) {
edit.putString(entry.getKey(), data);
ids.add(entry.getKey());
}
}
edit.apply();
for (String id: ids) {
NotePreviewWidget.updateAppWidget(mContext, AppWidgetManager.getInstance(mContext), Integer.parseInt(id));
}
}
@ReactMethod
public void updateReminderWidget() {
AppWidgetManager wm = AppWidgetManager.getInstance(mContext);
int[] ids = wm.getAppWidgetIds(ComponentName.createRelative(mContext.getPackageName(), ReminderWidgetProvider.class.getName()));
for (int id: ids) {
Log.d("Reminders", "Updating" + id);
RemoteViews views = new RemoteViews(mContext.getPackageName(), R.layout.widget_reminders);
ReminderWidgetProvider.updateAppWidget(mContext, wm, id, views);
wm.notifyAppWidgetViewDataChanged(id, R.id.widget_list_view);
}
}
@ReactMethod(isBlockingSynchronousMethod = true)
public boolean isGestureNavigationEnabled() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
try {
String navBarMode = Settings.Secure.getString(
mContext.getContentResolver(),
"navigation_mode"
);
return "2".equals(navBarMode);
} catch (Exception e) {
return false;
}
} else {
return false;
}
}
@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

@@ -1,13 +0,0 @@
- 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
Thank you for using Notesnook!

View File

@@ -1,11 +0,0 @@
pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
plugins { id("com.facebook.react.settings") }
extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
rootProject.name = 'Notesnook'
include ':app'
includeBuild('../node_modules/@react-native/gradle-plugin')
include ":react-native-config"
project(':react-native-config').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-config/android')
include ":lazysodium-android"
project(':lazysodium-android').projectDir = new File(rootProject.projectDir, '../node_modules/@ammarahmed/react-native-sodium/android/lazysodium-android/app')

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);
@@ -138,9 +132,7 @@ export const withTheme = (Element: (props: any) => JSX.Element) => {
const listener = Appearance.addChangeListener(({ colorScheme }) => {
if (colorScheme && SettingsService.getProperty("useSystemTheme")) {
useThemeStore
.getState()
.setColorScheme(colorScheme as "light" | "dark");
useThemeStore.getState().setColorScheme(colorScheme);
}
});
return () => {

View File

@@ -94,7 +94,9 @@ export async function encryptDatabaseKeyWithPassword(appLockPassword: string) {
}
export async function restoreDatabaseKeyToKeyChain(appLockPassword: string) {
const databaseKeyCipher = CipherStorage.getMap(DB_KEY_CIPHER) as Cipher;
const databaseKeyCipher: Cipher = CipherStorage.getMap(
DB_KEY_CIPHER
) as Cipher;
const databaseKey = (await decrypt(
{
password: appLockPassword
@@ -205,8 +207,9 @@ export async function getDatabaseKey(appLockPassword?: string) {
}
if (await Keychain.hasInternetCredentials("notesnook")) {
const userKeyCredentials =
await Keychain.getInternetCredentials("notesnook");
const userKeyCredentials = await Keychain.getInternetCredentials(
"notesnook"
);
if (userKeyCredentials) {
const userKeyCipher: Cipher = (await encrypt(

View File

@@ -72,8 +72,8 @@ export async function setupDatabase(password?: string) {
({
compress: Gzip.deflate,
decompress: Gzip.inflate
}) as ICompressor,
batchSize: 50,
} as ICompressor),
batchSize: 100,
sqliteOptions: {
dialect: (name) => ({
createDriver: () => {

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) {
@@ -115,13 +115,7 @@ export async function downloadFile(
.progress(async (recieved, total) => {
useAttachmentStore
.getState()
.setProgress(
0,
parseInt(total),
filename,
parseInt(recieved),
"download"
);
.setProgress(0, total, filename, recieved, "download");
DatabaseLogger.log(`Downloading: ${filename}, ${recieved}/${total}`);
});

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

@@ -30,7 +30,7 @@ import { AppFontSize } from "../../utils/size";
import { Button } from "../ui/button";
import { strings } from "@notesnook/intl";
export const Announcement = () => {
export const Announcement = ({ color }) => {
const { colors } = useThemeColors();
const [announcements, remove] = useMessageStore((state) => [
state.announcements,
@@ -82,6 +82,7 @@ export const Announcement = () => {
<Button
type="plain"
icon="close"
height={null}
onPress={() => {
remove(announcement.id);
}}
@@ -101,6 +102,7 @@ export const Announcement = () => {
renderItem({
item: item,
index: index,
color: colors[color],
inline: true
})
)}

View File

@@ -19,18 +19,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import Paragraph from "../ui/typography/paragraph";
import { BodyItemProps, getStyle } from "./functions";
import { getStyle } from "./functions";
import { DefaultAppStyles } from "../../utils/styles";
export const Body = (props: BodyItemProps) => {
export const Body = ({ text, style = {} }) => {
return (
<Paragraph
style={{
paddingHorizontal: DefaultAppStyles.GAP,
...getStyle(props.item.style)
...getStyle(style)
}}
>
{props.item.text}
{text}
</Paragraph>
);
};

View File

@@ -19,25 +19,24 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { Linking, View } from "react-native";
//import SettingsBackupAndRestore from '../../screens/settings/backup-restore';
import { useThemeColors } from "@notesnook/theme";
import { eSendEvent } from "../../services/event-manager";
import { eSendEvent, presentSheet } from "../../services/event-manager";
import { eCloseAnnouncementDialog } from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { sleep } from "../../utils/time";
import SheetProvider from "../sheet-provider";
import { Button } from "../ui/button";
import { allowedOnPlatform, BodyItemProps, getStyle } from "./functions";
import { allowedOnPlatform, getStyle } from "./functions";
import { DefaultAppStyles } from "../../utils/styles";
import { Action } from "../../stores/use-message-store";
export const Cta = (props: BodyItemProps) => {
export const Cta = ({ actions, style = {}, color, inline }) => {
const { colors } = useThemeColors();
let buttons =
props.item.actions.filter((item) => allowedOnPlatform(item.platforms)) ||
[];
actions.filter((item) => allowedOnPlatform(item.platforms)) || [];
const onPress = async (item: Action) => {
if (!props.inline) {
const onPress = async (item) => {
if (!inline) {
eSendEvent(eCloseAnnouncementDialog);
await sleep(500);
}
@@ -51,14 +50,14 @@ export const Cta = (props: BodyItemProps) => {
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP,
...getStyle(props.item.style),
flexDirection: props.inline ? "row" : "column",
...getStyle(style),
flexDirection: inline ? "row" : "column",
gap: DefaultAppStyles.GAP_SMALL
}}
>
<SheetProvider context="premium_cta" />
{props.inline ? (
{inline ? (
<>
{buttons.length > 0 &&
buttons.slice(0, 1).map((item) => (
@@ -107,9 +106,11 @@ export const Cta = (props: BodyItemProps) => {
key={item.title}
title={item.title}
buttonType={{
color: colors.primary.accent,
text: colors.primary.accentForeground,
selected: colors.primary.accent,
color: color ? color : colors.primary.accent,
text: color
? colors.static.white
: colors.primary.accentForeground,
selected: color ? color : colors.primary.accent,
opacity: 1
}}
onPress={() => onPress(item)}

View File

@@ -20,20 +20,20 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { AppFontSize } from "../../utils/size";
import Paragraph from "../ui/typography/paragraph";
import { BodyItemProps, getStyle } from "./functions";
import { getStyle } from "./functions";
import { DefaultAppStyles } from "../../utils/styles";
export const Description = (props: BodyItemProps) => {
export const Description = ({ text, style = {}, inline }) => {
return (
<Paragraph
style={{
paddingHorizontal: DefaultAppStyles.GAP,
...getStyle(props.item.style),
textAlign: props.inline ? "left" : props.item.style?.textAlign
...getStyle(style),
textAlign: inline ? "left" : style?.textAlign
}}
size={AppFontSize.sm}
>
{props.item.text}
{text}
</Paragraph>
);
};

View File

@@ -19,8 +19,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment } from "react";
import { View } from "react-native";
import { allowedPlatforms, BodyItem } from "../../stores/use-message-store";
import { DefaultAppStyles } from "../../utils/styles";
import { allowedPlatforms } from "../../stores/use-message-store";
import { ProFeatures } from "../dialogs/result/pro-features";
import { Body } from "./body";
import { Cta } from "./cta";
import { Description } from "./description";
@@ -28,8 +28,9 @@ import { List } from "./list";
import { Photo } from "./photo";
import { SubHeading } from "./subheading";
import { Title } from "./title";
import { DefaultAppStyles } from "../../utils/styles";
export function allowedOnPlatform(platforms: string[]) {
export function allowedOnPlatform(platforms) {
if (!platforms) return true;
return platforms.some((platform) => allowedPlatforms.indexOf(platform) > -1);
}
@@ -40,15 +41,11 @@ export const margins = {
2: 20
};
export const getStyle = (style: BodyItem["style"]) => {
export const getStyle = (style) => {
if (!style) return {};
return {
marginTop: style.marginTop
? margins[style.marginTop as keyof typeof margins] || 0
: 0,
marginBottom: style.marginBottom
? margins[style.marginBottom as keyof typeof margins] || 0
: 0,
marginTop: margins[style.marginTop] || 0,
marginBottom: margins[style.marginBottom] || 0,
textAlign: style.textAlign || "left"
};
};
@@ -61,7 +58,9 @@ const Features = () => {
alignItems: "center",
width: "100%"
}}
></View>
>
<ProFeatures />
</View>
);
};
@@ -77,25 +76,16 @@ const renderItems = {
callToActions: Cta
};
export const renderItem = ({
item,
index,
inline
}: {
item: BodyItem;
index: number;
inline?: boolean;
}) => {
const Item = renderItems[item.type as keyof typeof renderItems] || Fragment;
export const renderItem = ({ item, index, color, inline }) => {
const Item = renderItems[item.type] || Fragment;
return (
<Item
key={item.text || item.src || item.type}
item={item}
{...item}
index={index}
color={color}
inline={inline}
/>
);
};
export type BodyItemProps = { item: BodyItem; index: number; inline?: boolean };

View File

@@ -24,7 +24,7 @@ import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../services/event-manager";
import { Announcement, useMessageStore } from "../../stores/use-message-store";
import { useMessageStore } from "../../stores/use-message-store";
import { useThemeColors } from "@notesnook/theme";
import {
eCloseAnnouncementDialog,
@@ -97,24 +97,9 @@ import { DefaultAppStyles } from "../../utils/styles";
export const AnnouncementDialog = () => {
const { colors } = useThemeColors();
const [visible, setVisible] = useState(false);
const [info, setInfo] = useState<Announcement | undefined>(undefined);
const [info, setInfo] = useState();
const remove = useMessageStore((state) => state.remove);
const open = (data: Announcement) => {
setInfo(data);
setImmediate(() => {
setVisible(true);
});
};
const close = useCallback(() => {
if (visible) {
if (info?.id) remove(info?.id);
setInfo(undefined);
setVisible(false);
}
}, [info?.id, remove, visible]);
useEffect(() => {
eSubscribeEvent(eOpenAnnouncementDialog, open);
eSubscribeEvent(eCloseAnnouncementDialog, close);
@@ -124,6 +109,21 @@ export const AnnouncementDialog = () => {
};
}, [close, visible]);
const open = (data) => {
setInfo(data);
setImmediate(() => {
setVisible(true);
});
};
const close = useCallback(() => {
if (visible) {
remove(info?.id);
setInfo(null);
setVisible(false);
}
}, [info?.id, remove, visible]);
return (
<BaseDialog
animated={false}

View File

@@ -21,19 +21,19 @@ import React from "react";
import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import Paragraph from "../ui/typography/paragraph";
import { BodyItemProps, getStyle } from "./functions";
import { getStyle } from "./functions";
import { DefaultAppStyles } from "../../utils/styles";
export const List = (props: BodyItemProps) => {
export const List = ({ items, listType, style = {} }) => {
return (
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP,
paddingLeft: props.item.listType === "ordered" ? 25 : 25,
...getStyle(props.item.style)
paddingLeft: listType === "ordered" ? 25 : 25,
...getStyle(style)
}}
>
{props.item.items?.map((item, index) => (
{items.map((item, index) => (
<View
key={item.text}
style={{
@@ -41,7 +41,7 @@ export const List = (props: BodyItemProps) => {
flexDirection: "row"
}}
>
{props.item.listType === "ordered" ? (
{listType === "ordered" ? (
<Paragraph
style={{
marginRight: 5

View File

@@ -19,18 +19,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { Image } from "react-native";
import { BodyItemProps, getStyle } from "./functions";
import { getStyle } from "./functions";
export const Photo = (props: BodyItemProps) => {
return props.item.src ? (
export const Photo = ({ src, style = {} }) => {
return src ? (
<Image
source={{ uri: props.item.src }}
source={{ uri: src }}
resizeMode="cover"
style={{
width: "100%",
height: 200,
alignSelf: "center",
...getStyle(props.item.style)
...getStyle(style)
}}
/>
) : null;

View File

@@ -20,19 +20,20 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { AppFontSize } from "../../utils/size";
import Heading from "../ui/typography/heading";
import { BodyItemProps, getStyle } from "./functions";
import { getStyle } from "./functions";
import { DefaultAppStyles } from "../../utils/styles";
export const SubHeading = (props: BodyItemProps) => {
export const SubHeading = ({ text, style = {} }) => {
return (
<Heading
size={AppFontSize.md + 2}
style={{
marginHorizontal: DefaultAppStyles.GAP,
...getStyle(props.item.style)
marginTop: DefaultAppStyles.GAP_VERTICAL,
...getStyle(style)
}}
>
{props.item.text}
{text}
</Heading>
);
};

View File

@@ -19,42 +19,45 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { View } from "react-native";
import { useMessageStore } from "../../stores/use-message-store";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import { BodyItemProps, getStyle } from "./functions";
import { getStyle } from "./functions";
import { DefaultAppStyles } from "../../utils/styles";
export const Title = (props: BodyItemProps) => {
return props.inline ? (
export const Title = ({ text, style = {}, inline }) => {
return inline ? (
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: props.inline ? DefaultAppStyles.GAP_VERTICAL_SMALL : 0
marginBottom: inline ? DefaultAppStyles.GAP_VERTICAL_SMALL : 0
}}
>
<Heading
style={{
paddingHorizontal: DefaultAppStyles.GAP,
marginTop: DefaultAppStyles.GAP_VERTICAL,
...getStyle(props.item.style),
textAlign: props.inline ? "left" : props.item.style?.textAlign,
...getStyle(style),
textAlign: inline ? "left" : style?.textAlign,
flexShrink: 1
}}
size={props.inline ? AppFontSize.md : AppFontSize.xl}
size={inline ? AppFontSize.md : AppFontSize.xl}
>
{props.item.text?.toUpperCase()}
{inline ? text?.toUpperCase() : text}
</Heading>
</View>
) : (
<Heading
style={{
paddingHorizontal: DefaultAppStyles.GAP,
...getStyle(props.item.style)
...getStyle(style),
marginTop: style?.marginTop || DefaultAppStyles.GAP_VERTICAL
}}
>
{props.item.text}
{text}
</Heading>
);
};

View File

@@ -92,7 +92,7 @@ const AppLocked = () => {
const lockApp = useUserStore((state) => state.lockApp);
const deviceMode = useSettingStore((state) => state.deviceMode);
const passwordInputRef = useRef<TextInput>(null);
const password = useRef<string>(undefined);
const password = useRef<string>();
const appState = useAppState();
const lastAppState = useRef<AppStateStatus>(appState);
const biometricUnlockAwaitingUserInput = useRef(false);
@@ -239,8 +239,8 @@ const AppLocked = () => {
deviceMode !== "mobile"
? "50%"
: Platform.OS == "ios"
? "95%"
: "100%",
? "95%"
: "100%",
paddingHorizontal: 12,
marginBottom: 30,
marginTop: 15,

View File

@@ -17,7 +17,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { LegendList } from "@legendapp/list";
import {
Attachment,
FilteredSelector,
@@ -29,9 +28,10 @@ import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import { ActivityIndicator, View } from "react-native";
import { ScrollView } from "react-native-actions-sheet";
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
import { ScrollView } from "react-native-gesture-handler";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import create from "zustand";
import create, { State } from "zustand";
import { db } from "../../common/database";
import filesystem from "../../common/filesystem";
import { downloadAttachments } from "../../common/filesystem/download-attachment";
@@ -39,7 +39,6 @@ import { AttachmentGroupProgress } from "../../screens/settings/attachment-group
import { presentSheet, ToastManager } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { Dialog } from "../dialog";
import { presentDialog } from "../dialog/functions";
import { Header } from "../header";
@@ -51,6 +50,7 @@ import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { AttachmentItem } from "./attachment-item";
import { DefaultAppStyles } from "../../utils/styles";
const DEFAULT_SORTING: SortOptions = {
sortBy: "dateEdited",
@@ -65,7 +65,7 @@ type RecheckerProgress = {
filter: string;
};
interface RecheckerState {
interface RecheckerState extends State {
progress: {
[key: string]: RecheckerProgress;
};
@@ -135,9 +135,9 @@ export const AttachmentDialog = ({
const { colors } = useThemeColors();
const [attachments, setAttachments] =
useState<VirtualizedGrouping<Attachment>>();
const attachmentSearchValue = useRef<string>(undefined);
const attachmentSearchValue = useRef<string>();
const [loading, setLoading] = useState(true);
const searchTimer = useRef<NodeJS.Timeout>(undefined);
const searchTimer = useRef<NodeJS.Timeout>();
const [currentFilter, setCurrentFilter] = useState("all");
const rechecker = useRechecker((state) =>
note ? state.progress[note.id] || {} : state.progress.all
@@ -199,7 +199,7 @@ export const AttachmentDialog = ({
}, 300);
};
const renderItem = ({ index }: { item: boolean | number; index: number }) => (
const renderItem = ({ index }: { item: boolean; index: number }) => (
<AttachmentItem
setAttachments={async () => {
setAttachments(await filterAttachments(currentFilter));
@@ -326,6 +326,31 @@ export const AttachmentDialog = ({
title={strings.manageAttachments()}
renderedInRoute="SettingsGroup"
canGoBack
// TODO: Add headerRightButtons
// headerRightButtons={[
// {
// onPress() {
// onCheck();
// },
// title: strings.recheckAll()
// },
// {
// onPress() {
// if (!attachments) return;
// presentDialog({
// title: strings.doActions.download.attachment(
// attachments.placeholders.length
// ),
// positiveText: strings.network.download(),
// positivePress: async () => {
// downloadAttachments(await attachments.ids());
// },
// negativeText: strings.cancel()
// });
// },
// title: strings.downloadAllAttachments()
// }
// ]}
/>
) : (
<View
@@ -509,8 +534,7 @@ export const AttachmentDialog = ({
</ScrollView>
</View>
<LegendList
renderScrollComponent={(props) => <ScrollView {...props} />}
<FlashList
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
ListEmptyComponent={
@@ -544,8 +568,7 @@ export const AttachmentDialog = ({
/>
}
estimatedItemSize={50}
data={loading ? [] : attachments?.placeholders || []}
extraData={attachments}
data={loading ? [] : attachments?.placeholders}
renderItem={renderItem}
/>
</View>

View File

@@ -0,0 +1,114 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../services/event-manager";
import { useUserStore } from "../../stores/use-user-store";
import { eCloseLoginDialog, eOpenLoginDialog } from "../../utils/events";
import { sleep } from "../../utils/time";
import BaseDialog from "../dialog/base-dialog";
import { Toast } from "../toast";
import { initialAuthMode } from "./common";
import { Login } from "./login";
import { Signup } from "./signup";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../utils/styles";
export const AuthMode = {
login: 0,
signup: 1,
welcomeSignup: 2,
trialSignup: 3
};
const AuthModal = () => {
const { colors } = useThemeColors();
const [visible, setVisible] = useState(false);
const [currentAuthMode, setCurrentAuthMode] = useState(AuthMode.login);
const actionSheetRef = useRef();
useEffect(() => {
eSubscribeEvent(eOpenLoginDialog, open);
eSubscribeEvent(eCloseLoginDialog, close);
return () => {
eUnSubscribeEvent(eOpenLoginDialog, open);
eUnSubscribeEvent(eCloseLoginDialog, close);
};
}, []);
async function open(mode) {
setCurrentAuthMode(mode ? mode : AuthMode.login);
initialAuthMode.current = mode ? mode : AuthMode.login;
setVisible(true);
await sleep(10);
actionSheetRef.current?.show();
}
const close = () => {
useUserStore.setState({
disableAppLockRequests: false
});
actionSheetRef.current?.hide();
setCurrentAuthMode(AuthMode.login);
setVisible(false);
};
return !visible ? null : (
<BaseDialog
overlayOpacity={0}
statusBarTranslucent={false}
onShow={() => {
useUserStore.setState({
disableAppLockRequests: true
});
}}
onRequestClose={currentAuthMode !== AuthMode.welcomeSignup && close}
visible={true}
onClose={close}
useSafeArea={false}
bounce={false}
background={colors.primary.background}
transparent={false}
animated={false}
centered={false}
enableSheetKeyboardHandler
>
{currentAuthMode !== AuthMode.login ? (
<Signup
changeMode={(mode) => setCurrentAuthMode(mode)}
trial={AuthMode.trialSignup === currentAuthMode}
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
/>
) : (
<Login
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
changeMode={(mode) => setCurrentAuthMode(mode)}
/>
)}
<Toast context="local" />
</BaseDialog>
);
};
export default AuthModal;

View File

@@ -0,0 +1,21 @@
/*
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/>.
*/
export const SVG = (color) =>
`<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="1920" height="1080" preserveAspectRatio="none" viewBox="0 0 1920 1080"><g fill="none"><path d="M684.78-215.3C516.78-49.87 619.54 659.12 349.29 666.13 79.03 673.14-116.04 204.76-321.7 180.13" stroke="${color}" stroke-width="2"></path><path d="M1337.71-20.29C1105.2 122.03 1140.53 887.64 809.58 933.71 478.63 979.78 545.52 798.71 281.45 798.71 17.39 798.71-110.41 932.65-246.68 933.71" stroke="${color}" stroke-width="2"></path><path d="M1631.89-52.3C1379.35-21.93 1283.05 511.66 808.42 545.28 333.79 578.9 209.15 894.14-15.05 901.68" stroke="${color}" stroke-width="2"></path><path d="M1523.21-71.7C1244.4-46.38 1056.2 532.95 581.02 533.49 105.84 534.03-109.4 182.98-361.17 177.09" stroke="${color}" stroke-width="2"></path><path d="M1333.35-39.1C1041.86 57 974.42 893.52 550.36 906.48 126.3 919.44-23.8 619.56-232.63 614.88" stroke="${color}" stroke-width="2"></path></g><defs><mask id="SvgjsMask1032"><rect width="1920" height="1080" fill="#ffffff"></rect></mask></defs></svg>`;

View File

@@ -31,13 +31,12 @@ import { Dialog } from "../dialog";
import { Button } from "../ui/button";
import Input from "../ui/input";
import { Notice } from "../ui/notice";
import { TextInput } from "react-native-gesture-handler";
export const ChangePassword = () => {
const passwordInputRef = useRef<TextInput>(null);
const password = useRef<string>(undefined);
const oldPasswordInputRef = useRef<TextInput>(null);
const oldPassword = useRef<string>(undefined);
const passwordInputRef = useRef();
const password = useRef();
const oldPasswordInputRef = useRef();
const oldPassword = useRef();
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
@@ -86,7 +85,7 @@ export const ChangePassword = () => {
setLoading(false);
ToastManager.show({
heading: strings.passwordChangeFailed(),
message: (e as Error).message,
message: e.message,
type: "error",
context: "local"
});

View File

@@ -21,7 +21,6 @@ import { createRef } from "react";
import { eSendEvent } from "../../services/event-manager";
import { eCloseLoginDialog } from "../../utils/events";
import Navigation from "../../services/navigation";
import { AuthParams } from "../../stores/use-navigation-store";
export const AuthMode = {
login: 0,
signup: 1,
@@ -30,16 +29,15 @@ export const AuthMode = {
trialSignup: 4
};
export const initialAuthMode = createRef<number>();
initialAuthMode.current = AuthMode.login;
export function hideAuth(context?: AuthParams["context"]) {
export const initialAuthMode = createRef(0);
export function hideAuth(context) {
eSendEvent(eCloseLoginDialog);
if (
initialAuthMode.current === AuthMode.welcomeSignup ||
initialAuthMode.current === AuthMode.welcomeLogin ||
context === "intro"
) {
Navigation.replace("FluidPanelsView", {});
Navigation.replace("FluidPanelsView");
} else {
Navigation.goBack();
}

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useRef, useState } from "react";
import { TextInput, View } from "react-native";
import { View } from "react-native";
import ActionSheet from "react-native-actions-sheet";
import { db } from "../../common/database";
import { DDS } from "../../services/device-detection";
@@ -37,8 +37,8 @@ import { DefaultAppStyles } from "../../utils/styles";
export const ForgotPassword = () => {
const { colors } = useThemeColors("sheet");
const email = useRef<string>(undefined);
const emailInputRef = useRef<TextInput>(null);
const email = useRef();
const emailInputRef = useRef();
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
const [sent, setSent] = useState(false);
@@ -57,7 +57,7 @@ export const ForgotPassword = () => {
let lastRecoveryEmailTime = SettingsService.get().lastRecoveryEmailTime;
if (
lastRecoveryEmailTime &&
Date.now() - lastRecoveryEmailTime < 60000 * 3
Date.now() - JSON.parse(lastRecoveryEmailTime) < 60000 * 3
) {
throw new Error(strings.pleaseWaitBeforeSendEmail());
}
@@ -78,7 +78,7 @@ export const ForgotPassword = () => {
setLoading(false);
ToastManager.show({
heading: strings.recoveryEmailFailed(),
message: (e as Error).message,
message: e.message,
type: "error",
context: "local"
});
@@ -93,6 +93,7 @@ export const ForgotPassword = () => {
setSent(false);
setLoading(false);
}}
keyboardShouldPersistTaps="always"
onOpen={() => {
emailInputRef.current?.setNativeProps({
text: email.current

View File

@@ -23,7 +23,6 @@ import { View } from "react-native";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import { hideAuth } from "./common";
import { AuthParams } from "../../stores/use-navigation-store";
export const AuthHeader = (props: { welcome?: boolean }) => {
const { colors } = useThemeColors();
const route = useRoute();
@@ -48,7 +47,7 @@ export const AuthHeader = (props: { welcome?: boolean }) => {
<IconButton
name="arrow-left"
onPress={() => {
hideAuth((route.params as AuthParams)?.context);
hideAuth((route.params as any)?.context);
}}
color={colors.primary.paragraph}
/>

View File

@@ -25,9 +25,8 @@ import { AuthMode, initialAuthMode } from "./common";
import { Login } from "./login";
import { Signup } from "./signup";
import { useThemeColors } from "@notesnook/theme";
import { NavigationProps } from "../../services/navigation";
const Auth = ({ navigation, route }: NavigationProps<"Auth">) => {
const Auth = ({ navigation, route }) => {
const [currentAuthMode, setCurrentAuthMode] = useState(
route?.params?.mode || AuthMode.login
);
@@ -43,10 +42,14 @@ const Auth = ({ navigation, route }: NavigationProps<"Auth">) => {
currentAuthMode !== AuthMode.welcomeLogin ? (
<Signup
changeMode={(mode) => setCurrentAuthMode(mode)}
trial={AuthMode.trialSignup === currentAuthMode}
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
/>
) : (
<Login changeMode={(mode) => setCurrentAuthMode(mode)} />
<Login
welcome={initialAuthMode.current}
changeMode={(mode) => setCurrentAuthMode(mode)}
/>
)}
<Toast context="local" />

View File

@@ -19,18 +19,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import { RouteProp, useRoute } from "@react-navigation/native";
import { useRoute } from "@react-navigation/native";
import React, { useEffect, useState } from "react";
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";
import Sync from "../../services/sync";
import { RouteParams } from "../../stores/use-navigation-store";
import { useUserStore } from "../../stores/use-user-store";
import { eUserLoggedIn } from "../../utils/events";
import { AppFontSize } from "../../utils/size";
@@ -46,6 +44,7 @@ import { hideAuth } from "./common";
import { ForgotPassword } from "./forgot-password";
import { AuthHeader } from "./header";
import { useLogin } from "./use-login";
import SettingsService from "../../services/settings";
const LoginSteps = {
emailAuth: 1,
@@ -53,14 +52,10 @@ const LoginSteps = {
passwordAuth: 3
};
export const Login = ({
changeMode
}: {
changeMode: (mode: number) => void;
}) => {
export const Login = ({ changeMode }) => {
const { colors } = useThemeColors();
const [focused, setFocused] = useState(false);
const route = useRoute<RouteProp<RouteParams, "Auth">>();
const route = useRoute();
const {
step,
setStep,
@@ -85,7 +80,7 @@ export const Login = ({
if (!PremiumService.get() && !SettingsService.getProperty("serverUrls")) {
Navigation.navigate("PayWall", {
context: "signup",
state: route.params?.state,
state: route.params.state,
canGoBack: false
});
} else {
@@ -120,7 +115,7 @@ export const Login = ({
minHeight: "90%"
}}
nestedScrollEnabled
enableAutomaticScroll={true}
enableAutomaticScroll={false}
keyboardShouldPersistTaps="handled"
>
<View
@@ -141,11 +136,11 @@ export const Login = ({
marginBottom: DefaultAppStyles.GAP_VERTICAL,
borderBottomColor: colors.primary.border,
alignSelf: isTablet ? "center" : undefined,
borderWidth: isTablet ? 1 : undefined,
borderColor: isTablet ? colors.primary.border : undefined,
borderRadius: isTablet ? 20 : undefined,
marginTop: isTablet ? 50 : undefined,
width: !isTablet ? undefined : "50%",
borderWidth: isTablet ? 1 : null,
borderColor: isTablet ? colors.primary.border : null,
borderRadius: isTablet ? 20 : null,
marginTop: isTablet ? 50 : null,
width: !isTablet ? null : "50%",
minHeight: height * 0.4
}}
>
@@ -192,8 +187,8 @@ export const Login = ({
? "50%"
: "49.99%"
: focused
? "100%"
: "99.9%",
? "100%"
: "99.9%",
backgroundColor: colors.primary.background,
alignSelf: "center",
paddingHorizontal: DDS.isTab ? 0 : DefaultAppStyles.GAP,
@@ -245,9 +240,7 @@ export const Login = ({
marginBottom={0}
editable={!loading}
defaultValue={password.current}
onSubmit={() => {
login();
}}
onSubmit={() => login()}
/>
<Button
title={strings.forgotPassword()}
@@ -257,13 +250,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) return;
SheetManager.show("forgotpassword_sheet", email.current);
}}
textStyle={{
textDecorationLine: "underline"

View File

@@ -83,13 +83,20 @@ export const SessionExpired = () => {
});
} catch (e) {
ToastManager.show({
heading: (e as Error).message,
heading: e.message,
type: "error",
context: "local"
});
}
};
useEffect(() => {
const sub = eSubscribeEvent(eLoginSessionExpired, open);
return () => {
sub.unsubscribe?.();
};
}, [visible, open]);
const open = React.useCallback(async () => {
try {
let res = await db.tokenManager.getToken();
@@ -126,13 +133,6 @@ export const SessionExpired = () => {
}
}, [email]);
useEffect(() => {
const sub = eSubscribeEvent(eLoginSessionExpired, open);
return () => {
sub?.unsubscribe?.();
};
}, [visible, open]);
return (
visible && (
<BaseDialog
@@ -191,9 +191,7 @@ export const SessionExpired = () => {
textAlign: "center"
}}
>
{strings.sessionExpiredDesc(
getObfuscatedEmail(email.current as string)
)}
{strings.sessionExpiredDesc(getObfuscatedEmail(email.current))}
</Paragraph>
</View>
@@ -210,9 +208,7 @@ export const SessionExpired = () => {
autoCapitalize="none"
autoCorrect={false}
placeholder={strings.password()}
onSubmit={() => {
login();
}}
onSubmit={() => login()}
/>
) : null}

View File

@@ -19,11 +19,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useContext } from "react";
export const SignupContext = React.createContext<{
signup: () => Promise<boolean | undefined>;
signup?: () => Promise<boolean>;
}>({
signup: async () => {
return false;
}
signup: undefined
});
export const useSignupContext = () => useContext(SignupContext);

View File

@@ -19,14 +19,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import { RouteProp, useRoute } from "@react-navigation/native";
import { useRoute } from "@react-navigation/native";
import React, { useRef, useState } from "react";
import {
TextInput,
TouchableOpacity,
View,
useWindowDimensions
} from "react-native";
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import { db } from "../../common/database";
import { DDS } from "../../services/device-detection";
@@ -44,7 +39,6 @@ import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { AuthHeader } from "./header";
import { SignupContext } from "./signup-context";
import { RouteParams } from "../../stores/use-navigation-store";
import SettingsService from "../../services/settings";
const SignupSteps = {
@@ -53,28 +47,22 @@ const SignupSteps = {
createAccount: 2
};
export const Signup = ({
changeMode,
welcome
}: {
changeMode: (mode: number) => void;
welcome: boolean;
}) => {
export const Signup = ({ changeMode, welcome }) => {
const [currentStep, setCurrentStep] = useState(SignupSteps.signup);
const { colors } = useThemeColors();
const email = useRef<string>(undefined);
const emailInputRef = useRef<TextInput>(null);
const passwordInputRef = useRef<TextInput>(null);
const password = useRef<string>(undefined);
const confirmPasswordInputRef = useRef<TextInput>(null);
const confirmPassword = useRef<string>(undefined);
const email = useRef();
const emailInputRef = useRef();
const passwordInputRef = useRef();
const password = useRef();
const confirmPasswordInputRef = useRef();
const confirmPassword = useRef();
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
const setUser = useUserStore((state) => state.setUser);
const setLastSynced = useUserStore((state) => state.setLastSynced);
const { width, height } = useWindowDimensions();
const isTablet = width > 600;
const route = useRoute<RouteProp<RouteParams, "Auth">>();
const route = useRoute();
const validateInfo = () => {
if (!password.current || !email.current || !confirmPassword.current) {
@@ -98,7 +86,7 @@ export const Signup = ({
setLoading(true);
try {
setCurrentStep(SignupSteps.createAccount);
await db.user.signup(email.current!.toLowerCase(), password.current!);
await db.user.signup(email.current.toLowerCase(), password.current);
let user = await db.user.getUser();
setUser(user);
setLastSynced(await db.lastSynced());
@@ -117,7 +105,7 @@ export const Signup = ({
setLoading(false);
ToastManager.show({
heading: strings.signupFailed(),
message: (e as Error).message,
message: e.message,
type: "error",
context: "local"
});
@@ -142,6 +130,7 @@ export const Signup = ({
minHeight: "90%"
}}
nestedScrollEnabled
enableAutomaticScroll={false}
keyboardShouldPersistTaps="handled"
>
<View
@@ -162,11 +151,11 @@ export const Signup = ({
borderBottomWidth: 0.8,
borderBottomColor: colors.primary.border,
alignSelf: isTablet ? "center" : undefined,
borderWidth: isTablet ? 1 : undefined,
borderColor: isTablet ? colors.primary.border : undefined,
borderRadius: isTablet ? 20 : undefined,
marginTop: isTablet ? 50 : undefined,
width: !isTablet ? undefined : "50%",
borderWidth: isTablet ? 1 : null,
borderColor: isTablet ? colors.primary.border : null,
borderRadius: isTablet ? 20 : null,
marginTop: isTablet ? 50 : null,
width: !isTablet ? null : "50%",
minHeight: height * 0.25
}}
>
@@ -277,12 +266,10 @@ export const Signup = ({
autoCorrect={false}
blurOnSubmit={false}
validationType="confirmPassword"
customValidator={() => password.current!}
customValidator={() => password.current}
placeholder={strings.confirmPassword()}
marginBottom={12}
onSubmit={() => {
signup();
}}
onSubmit={signup}
/>
<Button
@@ -324,9 +311,7 @@ export const Signup = ({
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP,
width: DDS.isTab ? "50%" : "100%",
alignSelf: "center"
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<Paragraph
@@ -341,7 +326,7 @@ export const Signup = ({
<Paragraph
size={AppFontSize.xxs}
onPress={() => {
openLinkInBrowser("https://notesnook.com/tos");
openLinkInBrowser("https://notesnook.com/tos", colors);
}}
style={{
textDecorationLine: "underline"
@@ -355,7 +340,10 @@ export const Signup = ({
<Paragraph
size={AppFontSize.xxs}
onPress={() => {
openLinkInBrowser("https://notesnook.com/privacy");
openLinkInBrowser(
"https://notesnook.com/privacy",
colors
);
}}
style={{
textDecorationLine: "underline"

View File

@@ -20,7 +20,7 @@ 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 } from "react-native";
import { View } from "react-native";
import { ScrollView } from "react-native-actions-sheet";
import { db } from "../../common/database/index";
import useTimer from "../../hooks/use-timer";
@@ -36,44 +36,20 @@ import Paragraph from "../ui/typography/paragraph";
import { DefaultAppStyles } from "../../utils/styles";
import { presentDialog } from "../dialog/functions";
type MFAInfo = {
primaryMethod: string;
secondaryMethod: string;
token: string;
};
const TwoFactorVerification = ({
onMfaLogin,
mfaInfo,
onCancel
}: {
onMfaLogin: (
login: {
method: string;
code: string;
},
callback: (result: any) => void
) => Promise<void>;
mfaInfo: MFAInfo;
onCancel: () => void;
}) => {
const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
const { colors } = useThemeColors();
const code = useRef<string>(undefined);
const [currentMethod, setCurrentMethod] = useState<{
isPrimary: boolean;
method: string | null;
}>({
const code = useRef();
const [currentMethod, setCurrentMethod] = useState({
method: mfaInfo?.primaryMethod,
isPrimary: true
});
const { seconds, start, reset } = useTimer(currentMethod.method!);
const { seconds, start, reset } = useTimer(currentMethod.method);
const [loading, setLoading] = useState(false);
const inputRef = useRef<TextInput>(null);
const inputRef = useRef();
const [sending, setSending] = useState(false);
const onNext = async () => {
if (!code.current || code.current.length < 6 || !currentMethod.method)
return;
if (!code.current || code.current.length < 6) return;
setLoading(true);
inputRef.current?.blur();
await onMfaLogin(
@@ -130,25 +106,26 @@ const TwoFactorVerification = ({
);
};
const onSendCode = useCallback(async () => {
if (seconds || sending) return;
setSending(true);
try {
await db.mfa.sendCode(currentMethod.method as "sms" | "email");
start(60);
setSending(false);
} catch (e) {
setSending(false);
ToastManager.error(e as Error, "Error sending 2FA Code", "local");
}
}, [currentMethod.method, mfaInfo.token, seconds, sending, start]);
useEffect(() => {
if (currentMethod.method === "sms" || currentMethod.method === "email") {
onSendCode();
}
}, [currentMethod.method, onSendCode]);
const onSendCode = useCallback(async () => {
if (seconds || sending) return;
// TODO
setSending(true);
try {
await db.mfa.sendCode(currentMethod.method, mfaInfo.token);
start(60);
setSending(false);
} catch (e) {
setSending(false);
ToastManager.error(e, "Error sending 2FA Code", "local");
}
}, [currentMethod.method, mfaInfo.token, seconds, sending, start]);
return (
<ScrollView
keyboardShouldPersistTaps="handled"
@@ -194,11 +171,8 @@ const TwoFactorVerification = ({
textAlign: "center"
}}
>
{currentMethod.method
? strings["2faCodeHelpText"][
currentMethod.method as keyof (typeof strings)["2faCodeHelpText"]
]?.() || strings.select2faCodeHelpText()
: strings.select2faCodeHelpText()}
{strings["2faCodeHelpText"][currentMethod.method]?.() ||
strings.select2faCodeHelpText()}
</Paragraph>
{currentMethod.method === "sms" || currentMethod.method === "email" ? (
@@ -210,7 +184,7 @@ const TwoFactorVerification = ({
? ""
: `${
seconds
? strings.resend2faCode(`${seconds}`)
? strings.resend2faCode(seconds)
: strings.sendCode()
}`
}
@@ -235,6 +209,7 @@ const TwoFactorVerification = ({
textAlign="center"
onChangeText={(value) => {
code.current = value;
//onNext();
}}
cursorColor={colors.selected.accent}
selectionHandleColor={colors.selected.accent}
@@ -278,7 +253,7 @@ const TwoFactorVerification = ({
<Button
title={strings["2faCodeSecondaryMethodText"][
currentMethod.method as keyof (typeof strings)["2faCodeSecondaryMethodText"]
currentMethod.method
]()}
type="plain"
onPress={onRequestSecondaryMethod}
@@ -332,18 +307,7 @@ const TwoFactorVerification = ({
);
};
TwoFactorVerification.present = (
onMfaLogin: (
login: {
method: string;
code: string;
},
callback: (result: any) => void
) => Promise<void>,
data: MFAInfo,
onCancel: () => void,
context?: string
) => {
TwoFactorVerification.present = (onMfaLogin, data, onCancel, context) => {
presentDialog({
component: () => (
<TwoFactorVerification
@@ -353,6 +317,7 @@ TwoFactorVerification.present = (
/>
),
context: context || "two_factor_verify",
disableClosing: true,
transparent: false,
statusBarTranslucent: true
});

View File

@@ -43,8 +43,8 @@ export const useLogin = (
const [loading, setLoading] = useState(false);
const setUser = useUserStore((state) => state.setUser);
const [step, setStep] = useState(LoginSteps.emailAuth);
const email = useRef<string>(undefined);
const password = useRef<string>(undefined);
const email = useRef<string>();
const password = useRef<string>();
const emailInputRef = useRef<TextInput>(null);
const passwordInputRef = useRef<TextInput>(null);

View File

@@ -36,6 +36,7 @@ import { getElevationStyle } from "../../utils/elevation";
import { AppFontSize, normalize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { hexToRGBA, RGB_Linear_Shade } from "../../utils/colors";
import useKeyboard from "../../hooks/use-keyboard";
interface FloatingButtonProps {
onPress: () => void;
@@ -119,16 +120,14 @@ const FloatingButton = ({
style={[
{
position: "absolute",
right: position === "right" ? DefaultAppStyles.GAP - 7 : undefined,
left: position === "left" ? DefaultAppStyles.GAP - 7 : undefined,
bottom: 15,
right: position === "right" ? DefaultAppStyles.GAP : undefined,
left: position === "left" ? DefaultAppStyles.GAP : undefined,
bottom: 20,
zIndex: 10,
padding: 15,
...style
},
animatedStyle
]}
pointerEvents="box-none"
>
<TouchableOpacity
testID={testID || notesnook.buttons.add}
@@ -161,10 +160,10 @@ const FloatingButton = ({
icon
? icon
: route.name === "Notebooks"
? "notebook-plus"
: route.name === "Trash"
? "delete"
: "plus"
? "notebook-plus"
: route.name === "Trash"
? "delete"
: "plus"
}
color={color || colors.primary.accent}
size={size === "small" ? AppFontSize.xl : AppFontSize.xxxl}

View File

@@ -30,7 +30,7 @@ export const Container = ({ children }: PropsWithChildren) => {
const [height, setHeight] = useState(0);
const windowHeightRef = useRef(Dimensions.get("window").height);
const { height: windowHeight } = useWindowDimensions();
const timerRef = useRef<NodeJS.Timeout>(undefined);
const timerRef = useRef<NodeJS.Timeout>();
useEffect(() => {
if (windowHeight !== windowHeightRef.current) {
setHeight(0);

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

@@ -20,12 +20,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { AnnouncementDialog } from "../announcements";
import AuthModal from "../auth/auth-modal";
import { SessionExpired } from "../auth/session-expired";
import { Dialog } from "../dialog";
import { AppLockPassword } from "../dialogs/applock-password";
import JumpToSectionDialog from "../dialogs/jump-to-section";
import { LoadingDialog } from "../dialogs/loading";
import PDFPreview from "../dialogs/pdf-preview";
import ResultDialog from "../dialogs/result";
import { VaultDialog } from "../dialogs/vault";
import ImagePreview from "../image-preview";
import MergeConflicts from "../merge-conflicts";
@@ -52,6 +54,7 @@ const DialogProvider = () => {
<>
<MergeConflicts />
<RecoveryKeySheet colors={colors} />
<ResultDialog />
<VaultDialog colors={colors} />
<RateAppSheet />
<ImagePreview />

View File

@@ -0,0 +1,52 @@
/*
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 { eSendEvent } from "../../services/event-manager";
import {
eCloseActionSheet,
eCloseAddNotebookDialog,
eCloseMoveNoteDialog,
eOpenActionSheet,
eOpenAddNotebookDialog,
eOpenMoveNoteDialog
} from "../../utils/events";
export const ActionSheetEvent = (item, buttons) => {
eSendEvent(eOpenActionSheet, {
item,
buttons
});
};
export const ActionSheetHideEvent = () => {
eSendEvent(eCloseActionSheet);
};
export const moveNoteEvent = () => {
eSendEvent(eOpenMoveNoteDialog);
};
export const moveNoteHideEvent = () => {
eSendEvent(eCloseMoveNoteDialog);
};
export const AddNotebookEvent = (notebook) => {
eSendEvent(eOpenAddNotebookDialog, notebook);
};
export const HideAddNotebookEvent = (notebook) => {
eSendEvent(eCloseAddNotebookDialog, notebook);
};

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

@@ -201,14 +201,12 @@ export const AppLockPassword = () => {
accountPass
? strings.enterAccountPassword()
: mode === "change"
? keyboardType === "pin"
? strings.newPin()
: strings.newPassword()
: `${
keyboardType === "pin"
? strings.pin()
: strings.password()
}`
? keyboardType === "pin"
? strings.newPin()
: strings.newPassword()
: `${
keyboardType === "pin" ? strings.pin() : strings.password()
}`
}
/>
@@ -385,8 +383,8 @@ export const AppLockPassword = () => {
mode === "remove"
? strings.remove()
: mode === "change"
? strings.change()
: strings.save()
? strings.change()
: strings.save()
}
negativeTitle={strings.cancel()}
positiveType="transparent"

View File

@@ -51,7 +51,7 @@ const ColorPicker = ({
const [selectedColor, setSelectedColor] = useState<string>();
const { colors } = useThemeColors();
const inputRef = useRef<TextInput>(null);
const title = useRef<string>(undefined);
const title = useRef<string>();
const colorRef = useStoredRef("color-ref", "#f0f0f0");
return (
@@ -167,16 +167,14 @@ const ColorPicker = ({
);
if (!title.current)
return ToastManager.error(
new Error(strings.allFieldsRequired()),
"color-picker"
new Error(strings.allFieldsRequired())
);
const exists = await db.colors.all.find((v) =>
v.and([v(`colorCode`, "==", selectedColor)])
);
if (exists)
return ToastManager.error(
new Error(strings.colorExists(selectedColor)),
"color-picker"
new Error(strings.colorExists(selectedColor))
);
const id = await db.colors.add({
title: title.current,

View File

@@ -46,7 +46,7 @@ import Paragraph from "../../ui/typography/paragraph";
import { DefaultAppStyles } from "../../../utils/styles";
const JumpToSectionDialog = () => {
const scrollRef = useRef<RefObject<FlatList>>(undefined);
const scrollRef = useRef<RefObject<FlatList>>();
const [data, setData] = useState<VirtualizedGrouping<Item>>();
const { colors } = useThemeColors();
const notes = data;

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

@@ -50,7 +50,7 @@ export default function Progress() {
const { colors } = useThemeColors();
const [progress, setProgress] = React.useState<string | undefined>();
const [visible, setVisible] = React.useState(false);
const cancelCallback = React.useRef<() => void>(undefined);
const cancelCallback = React.useRef<() => void>();
const [data, setData] = React.useState<{
title?: string;
paragraph?: string;

View File

@@ -0,0 +1,145 @@
/*
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, { useEffect, useState } from "react";
import { View } from "react-native";
import { DDS } from "../../../services/device-detection";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../../services/event-manager";
import { useThemeColors } from "@notesnook/theme";
import { getElevationStyle } from "../../../utils/elevation";
import { eCloseResultDialog, eOpenResultDialog } from "../../../utils/events";
import { AppFontSize } from "../../../utils/size";
import BaseDialog from "../../dialog/base-dialog";
import { Button } from "../../ui/button";
import Seperator from "../../ui/seperator";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { ProFeatures } from "./pro-features";
import { DefaultAppStyles } from "../../../utils/styles";
const ResultDialog = () => {
const { colors } = useThemeColors();
const [visible, setVisible] = useState(false);
const [dialogData, setDialogData] = useState({
title: "",
paragraph: "",
button: ""
});
useEffect(() => {
eSubscribeEvent(eOpenResultDialog, open);
eSubscribeEvent(eCloseResultDialog, close);
return () => {
eUnSubscribeEvent(eOpenResultDialog, open);
eUnSubscribeEvent(eCloseResultDialog, close);
};
}, []);
const open = (data) => {
if (data) {
setDialogData(data);
}
setVisible(true);
};
const close = () => {
setVisible(false);
};
return !visible ? null : (
<BaseDialog visible={true} onRequestClose={close}>
<View
style={{
...getElevationStyle(5),
width: DDS.isTab ? 350 : "85%",
maxHeight: 500,
borderRadius: 10,
backgroundColor: colors.primary.background,
paddingTop: 20,
justifyContent: "center",
alignItems: "center"
}}
>
<Heading
size={AppFontSize.lg}
textBreakStrategy="balanced"
style={{
alignSelf: "center",
textAlign: "center",
marginTop: DefaultAppStyles.GAP_VERTICAL,
maxWidth: "100%",
marginBottom: DefaultAppStyles.GAP_VERTICAL,
paddingHorizontal: DefaultAppStyles.GAP
}}
>
{dialogData.title}
</Heading>
<Paragraph
color={colors.secondary.paragraph}
size={AppFontSize.md}
style={{
alignSelf: "center",
textAlign: "center",
maxWidth: "80%",
lineHeight: AppFontSize.sm + 5
}}
>
{dialogData.paragraph}
</Paragraph>
<Seperator />
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP,
alignItems: "center",
width: "100%"
}}
>
<ProFeatures count={4} />
</View>
<Seperator />
<View
style={{
backgroundColor: colors.secondary.background,
width: "100%",
borderBottomRightRadius: 10,
borderBottomLeftRadius: 10,
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
>
<Button
title={dialogData.button}
width={null}
style={{
paddingHorizontal: DefaultAppStyles.GAP
}}
onPress={close}
/>
</View>
</View>
</BaseDialog>
);
};
export default ResultDialog;

View File

@@ -0,0 +1,104 @@
/*
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 Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { eSendEvent } from "../../../services/event-manager";
import { useThemeColors } from "@notesnook/theme";
import {
eCloseSheet,
eCloseResultDialog,
eOpenPremiumDialog
} from "../../../utils/events";
import { defaultBorderRadius, AppFontSize } from "../../../utils/size";
import { sleep } from "../../../utils/time";
import Paragraph from "../../ui/typography/paragraph";
import { DefaultAppStyles } from "../../../utils/styles";
export const ProFeatures = ({ count = 6 }) => {
const { colors } = useThemeColors();
return (
<>
{[
{
content:
"Unlock unlimited notebooks, tags, colors. Organize like a pro"
},
{
content:
"Attach files upto 500MB, upload 4K images with unlimited storage"
},
{
content: "Instantly sync to unlimited devices"
},
{
content: "A private vault to keep everything important always locked"
},
{
content:
"Rich note editing experience with markdown, tables, checklists and more"
},
{
content: "Export your notes in Pdf, markdown and html formats"
}
]
.slice(0, count)
.map((item) => (
<View
key={item.content}
style={{
flexDirection: "row",
width: "100%",
height: 40,
paddingHorizontal: 0,
marginBottom: DefaultAppStyles.GAP_VERTICAL,
alignItems: "center",
borderRadius: defaultBorderRadius,
justifyContent: "flex-start"
}}
>
<Icon
size={AppFontSize.lg}
color={colors.primary.accent}
name="check"
/>
<Paragraph style={{ marginLeft: 5, flexShrink: 1 }}>
{item.content}
</Paragraph>
</View>
))}
<Paragraph
onPress={async () => {
eSendEvent(eCloseResultDialog);
eSendEvent(eCloseSheet);
await sleep(300);
eSendEvent(eOpenPremiumDialog);
}}
size={AppFontSize.xs}
style={{
textDecorationLine: "underline",
color: colors.secondary.paragraph
}}
>
See all features included in Notesnook Pro
</Paragraph>
</>
);
};

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,
@@ -68,7 +67,7 @@ export interface TabsRef {
page: () => FluidTabPage;
setScrollEnabled: () => true;
isDrawerOpen: () => boolean;
node: RefObject<Animated.View | null>;
node: RefObject<Animated.View>;
tabChangedFromSwipeAction: SharedValue<boolean>;
}
@@ -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

@@ -46,7 +46,7 @@ const ImagePreview = () => {
const [visible, setVisible] = useState(false);
const [image, setImage] = useState<string>();
const [loading, setLoading] = useState(false);
const imageRef = useRef<ImageAttributes>(null);
const imageRef = useRef<ImageAttributes>();
const insets = useGlobalSafeAreaInsets();
const [showHeader, setShowHeader] = useState(true);

View File

@@ -19,10 +19,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import { useMessageStore } from "../../../stores/use-message-store";
import { RouteParams } from "../../../stores/use-navigation-store";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { Announcement } from "../../announcements/announcement";
import { Card } from "../../list/card";
import { RouteParams } from "../../../stores/use-navigation-store";
export type ListHeaderProps = {
noAnnouncement?: boolean;
@@ -46,7 +48,7 @@ export const Header = React.memo(
return (
<>
{announcements.length !== 0 && !noAnnouncement ? (
<Announcement />
<Announcement color={color || colors.primary.accent} />
) : (screen as any) === "Search" ? null : !shouldShow ? (
<>
{messageCard ? (

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { GroupingKey, Item, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import { LegendList, LegendListRenderItemProps } from "@legendapp/list";
import { FlashList } from "@shopify/flash-list";
import React, { useEffect, useRef } from "react";
import {
NativeScrollEvent,
@@ -38,7 +38,6 @@ import { fluidTabsRef } from "../../utils/global-refs";
import { Header } from "../list-items/headers/header";
import { Empty, PlaceholderData } from "./empty";
import { ListItemWrapper } from "./list-item.wrapper";
import { ScrollView } from "react-native-actions-sheet";
type ListProps = {
data: VirtualizedGrouping<Item> | undefined;
@@ -56,13 +55,9 @@ type ListProps = {
id?: string;
};
const onMomentumScrollEnd = () => {
fluidTabsRef.current?.unlock();
};
export default function List(props: ListProps) {
const { colors } = useThemeColors();
const scrollRef = useRef(null);
const scrollRef = useRef();
const [notesListMode, notebooksListMode] = useSettingStore((state) => [
state.settings.notesListMode,
state.settings.notebooksListMode
@@ -77,10 +72,10 @@ export default function List(props: ListProps) {
props.renderedInRoute === "Notes"
? "home"
: props.renderedInRoute === "Favorites"
? "favorites"
: props.renderedInRoute === "Trash" || props.dataType === "trash"
? "trash"
: `${props.dataType}s`;
? "favorites"
: props.renderedInRoute === "Trash" || props.dataType === "trash"
? "trash"
: `${props.dataType}s`;
const groupOptions = useGroupOptions(groupType);
@@ -90,18 +85,11 @@ export default function List(props: ListProps) {
});
};
const getItemType = React.useCallback(
(item: number | boolean, index: number) => {
return props.data?.type(index);
},
[]
);
const renderItem = React.useCallback(
(itemProps: LegendListRenderItemProps<any, any>) => {
({ index }: { index: number }) => {
return (
<ListItemWrapper
index={itemProps.index}
index={index}
isSheet={props.isRenderedInActionSheet || false}
items={props.data}
groupOptions={groupOptions}
@@ -145,6 +133,16 @@ export default function List(props: ListProps) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const styles = {
width: "100%",
minHeight: 1,
minWidth: 1
};
const ListView = props.CustomListComponent
? props.CustomListComponent
: FlashList;
return (
<>
<View
@@ -152,75 +150,74 @@ export default function List(props: ListProps) {
flex: 1
}}
>
<LegendList
ref={scrollRef}
contentContainerStyle={{
flexGrow: 1
}}
extraData={props.data}
testID={notesnook.list.id}
data={props.data?.placeholders || []}
renderScrollComponent={
props.isRenderedInActionSheet
? (props) => <ScrollView {...props} />
: undefined
}
renderItem={renderItem}
onScroll={onListScroll}
nestedScrollEnabled={true}
onMomentumScrollEnd={onMomentumScrollEnd}
getItemType={getItemType}
estimatedItemSize={isCompactModeEnabled ? 60 : 120}
directionalLockEnabled={true}
keyboardShouldPersistTaps="always"
keyboardDismissMode="interactive"
ListEmptyComponent={
<View
style={{
flex: 1
}}
>
<Empty
loading={props.loading}
title={props.headerTitle}
dataType={props.dataType}
{props.data?.placeholders?.length === 0 || !props.data ? (
<>
{props.CustomLisHeader ? (
props.CustomLisHeader
) : !props.headerTitle ? null : (
<Header
color={props.customAccentColor}
placeholder={props.placeholder}
screen={props.renderedInRoute}
/>
</View>
}
ListFooterComponent={
<View
style={{ height: props.data?.placeholders?.length ? 100 : 0 }}
)}
<Empty
loading={props.loading}
title={props.headerTitle}
dataType={props.dataType}
color={props.customAccentColor}
placeholder={props.placeholder}
screen={props.renderedInRoute}
/>
}
refreshControl={
props.isRenderedInActionSheet ? (
<></>
) : (
<RefreshControl
tintColor={colors.primary.accent}
colors={[colors.primary.accent]}
progressBackgroundColor={colors.secondary.background}
onRefresh={_onRefresh}
refreshing={false}
</>
) : (
<ListView
style={styles}
ref={scrollRef}
testID={notesnook.list.id}
data={props.data?.placeholders || []}
renderItem={renderItem}
onScroll={onListScroll}
nestedScrollEnabled={true}
onMomentumScrollEnd={() => {
fluidTabsRef.current?.unlock();
}}
getItemType={(item: number, index: number) => {
return props.data?.type(index);
}}
estimatedItemSize={isCompactModeEnabled ? 60 : 120}
directionalLockEnabled={true}
keyboardShouldPersistTaps="always"
keyboardDismissMode="interactive"
ListFooterComponent={
<View
style={{ height: props.data?.placeholders?.length ? 100 : 0 }}
/>
)
}
ListHeaderComponent={
<>
{props.CustomLisHeader ? (
props.CustomLisHeader
) : !props.headerTitle ? null : (
<Header
color={props.customAccentColor}
screen={props.renderedInRoute}
}
refreshControl={
props.isRenderedInActionSheet ? null : (
<RefreshControl
tintColor={colors.primary.accent}
colors={[colors.primary.accent]}
progressBackgroundColor={colors.secondary.background}
onRefresh={_onRefresh}
refreshing={false}
/>
)}
</>
}
/>
)
}
ListHeaderComponent={
<>
{props.CustomLisHeader ? (
props.CustomLisHeader
) : !props.headerTitle ? null : (
<Header
color={props.customAccentColor}
screen={props.renderedInRoute}
/>
)}
</>
}
/>
)}
</View>
</>
);

View File

@@ -67,16 +67,16 @@ type ListItemWrapperProps<TItem = Item> = {
export function ListItemWrapper(props: ListItemWrapperProps) {
const { items, group, isSheet, index, groupOptions } = props;
const [item, setItem] = useState<Item>();
const tags = useRef<TagsWithDateEdited>(undefined);
const notebooks = useRef<NotebooksWithDateEdited>(undefined);
const reminder = useRef<Reminder>(undefined);
const color = useRef<Color>(undefined);
const tags = useRef<TagsWithDateEdited>();
const notebooks = useRef<NotebooksWithDateEdited>();
const reminder = useRef<Reminder>();
const color = useRef<Color>();
const totalNotes = useRef<number>(0);
const attachmentsCount = useRef(0);
const [groupHeader, setGroupHeader] = useState<GroupHeader>();
const previousIndex = useRef<number>(undefined);
const refreshTimeout = useRef<NodeJS.Timeout>(undefined);
const currentItemId = useRef<string>(undefined);
const previousIndex = useRef<number>();
const refreshTimeout = useRef<NodeJS.Timeout>();
const currentItemId = useRef<string>();
const locked = useRef(false);
const compactMode = useIsCompactModeEnabled(props.dataType as ItemType);

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

@@ -20,25 +20,25 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { RefObject, useCallback, useEffect, useState } from "react";
import { ActivityIndicator, Text, View } from "react-native";
import { LegendList } from "@legendapp/list";
import { getFormattedDate, getTimeAgo } from "@notesnook/common";
import { HistorySession, Note, VirtualizedGrouping } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import { ActionSheetRef, ScrollView } from "react-native-actions-sheet";
import { ActionSheetRef } from "react-native-actions-sheet";
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import { useDBItem } from "../../hooks/use-db-item";
import { presentSheet } from "../../services/event-manager";
import { openLinkInBrowser } from "../../utils/functions";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import DialogHeader from "../dialog/dialog-header";
import SheetProvider from "../sheet-provider";
import { Pressable } from "../ui/pressable";
import Seperator from "../ui/seperator";
import Paragraph from "../ui/typography/paragraph";
import NotePreview from "./preview";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../utils/styles";
const HistoryItem = ({
index,
@@ -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]
);
@@ -157,11 +156,9 @@ export default function NoteHistory({
maxHeight: "100%"
}}
>
<LegendList
renderScrollComponent={(props) => <ScrollView {...props} />}
data={history?.placeholders || []}
<FlashList
data={history?.placeholders}
estimatedItemSize={55}
extraData={history}
ListEmptyComponent={
<View
style={{

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

@@ -744,7 +744,6 @@ const ComparePlans = React.memo(
{["Features", "Free", "Essential", "Pro", "Believer"].map(
(plan, index) => (
<View
key={plan}
style={{
width: index === 0 ? 150 : 120,
paddingHorizontal: 16,
@@ -764,7 +763,7 @@ const ComparePlans = React.memo(
{getFeaturesTable().map((item, keyIndex) => {
return (
<View
key={`${item[0] + item[1]}`}
key={keyIndex + "feature-item"}
style={{
flexDirection: "row",
alignItems: "center",
@@ -834,7 +833,6 @@ const ComparePlans = React.memo(
{["features", "free", "essential", "pro", "believer"].map(
(plan, index) => (
<View
key={plan + "btn"}
style={{
width: index === 0 ? 150 : 120,
paddingHorizontal: 16,
@@ -994,7 +992,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(
@@ -1006,8 +1004,8 @@ const PricingPlanCard = ({
: "monthly"
}`
: pricingPlans.isGithubRelease
? (WebPlan?.period as string)
: (product?.productId as string)
? (WebPlan?.period as string)
: (product?.productId as string)
);
setStep(Steps.buy);
}}

View File

@@ -17,13 +17,11 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { LegendList } from "@legendapp/list";
import { useIsFeatureAvailable } from "@notesnook/common";
import { Color, Note } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useState } from "react";
import { View } from "react-native";
import { FlashList } from "@shopify/flash-list";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../e2e/test.ids";
import { db } from "../../common/database";
@@ -34,11 +32,14 @@ import { useRelationStore } from "../../stores/use-relation-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { refreshNotesPage } from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import ColorPicker from "../dialogs/color-picker";
import PaywallSheet from "../sheets/paywall";
import { Button } from "../ui/button";
import NativeTooltip from "../../utils/tooltip";
import { Pressable } from "../ui/pressable";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../utils/styles";
import { isFeatureAvailable, useIsFeatureAvailable } from "@notesnook/common";
import PaywallSheet from "../sheets/paywall";
const ColorItem = ({ item, note }: { item: Color; note: Note }) => {
const { colors } = useThemeColors();
@@ -170,7 +171,7 @@ export const ColorTags = ({ item }: { item: Note }) => {
}}
/>
) : (
<LegendList
<FlashList
data={colorNotes}
estimatedItemSize={30}
horizontal

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

@@ -34,14 +34,14 @@ import { DefaultAppStyles } from "../../utils/styles";
export default function Notebooks({ note, close, full }) {
const { colors } = useThemeColors();
async function getNotebooks() {
let filteredNotebooks = await db.relations.to(note, "notebook").resolve();
return filteredNotebooks || [];
}
const [noteNotebooks, setNoteNotebooks] = useState([]);
useEffect(() => {
async function getNotebooks() {
let filteredNotebooks = await db.relations.to(note, "notebook").resolve();
return filteredNotebooks || [];
}
getNotebooks().then((notebooks) => setNoteNotebooks(notebooks));
}, []);
});
const navigateNotebook = async (id) => {
let item = await db.notebooks.notebook(id);

View File

@@ -70,6 +70,7 @@ export const Tags = ({ item, close }) => {
export const TagStrip = ({ item, close }) => {
const [tags, setTags] = useState([]);
useEffect(() => {
db.relations
.to(item, "tag")
@@ -77,7 +78,7 @@ export const TagStrip = ({ item, close }) => {
.then((tags) => {
setTags(tags);
});
}, []);
});
return tags?.length > 0 ? (
<View

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

@@ -20,7 +20,7 @@ import { EVENTS, Note } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect } from "react";
import { ListRenderItemInfo, View } from "react-native";
import { View } from "react-native";
import { FlatList } from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../../common/database";
@@ -187,7 +187,7 @@ export default function EditorTabs({
]);
const renderTabItem = React.useCallback(
({ item }: ListRenderItemInfo<TabItem>) => {
({ item }: { item: TabItem }) => {
return (
<TabItemComponent
key={item.id}

View File

@@ -118,8 +118,8 @@ const ListBlockItem = ({
{item?.content.length > 200
? item?.content.slice(0, 200) + "..."
: !item.content || item.content.trim() === ""
? strings.linkNoteEmptyBlock()
: item.content}
? strings.linkNoteEmptyBlock()
: item.content}
</Paragraph>
<View
@@ -149,12 +149,12 @@ export default function LinkNote(props: {
}) {
const blockLinking = useIsFeatureAvailable("blockLinking");
const { colors } = useThemeColors();
const query = useRef<string>(undefined);
const query = useRef<string>();
const [notes, setNotes] = useState<VirtualizedGrouping<Note>>();
const nodesRef = useRef<ContentBlock[]>([]);
const [nodes, setNodes] = useState<ContentBlock[]>([]);
const inputRef = useRef<TextInput>(null);
const inputRef = useRef<TextInput>();
const [selectedNote, setSelectedNote] = useState<Note>();
const [selectedNodeId, setSelectedNodeId] = useState<string>();

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

@@ -45,25 +45,6 @@ import { IconButton } from "../../ui/icon-button";
import Input from "../../ui/input";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { useAsync } from "react-async-hook";
import { isFeatureAvailable, useIsFeatureAvailable } from "@notesnook/common";
async function fetchMonographData(noteId: string) {
const monographId = db.monographs.monograph(noteId);
const monograph = monographId
? await db.monographs.get(monographId)
: undefined;
const analyticsFeature = await isFeatureAvailable("monographAnalytics");
const analytics =
monographId && analyticsFeature
? await db.monographs.analytics(monographId)
: undefined;
return {
monograph,
monographId,
analytics
};
}
const PublishNoteSheet = ({
note
@@ -75,23 +56,20 @@ const PublishNoteSheet = ({
const attachmentDownloads = useAttachmentStore((state) => state.downloading);
const downloading = attachmentDownloads?.[`monograph-${note.id}`];
const [selfDestruct, setSelfDestruct] = useState(false);
const isFeatureAvailable = useIsFeatureAvailable("monographAnalytics");
const [isLocked, setIsLocked] = useState(false);
const [monograph, setMonograph] = useState<Monograph>();
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);
const isPublished = !!monograph;
const pwdInput = useRef<TextInput>(null);
const passwordValue = useRef<string>();
useEffect(() => {
(async () => {
const monograph = await db.monographs.get(
db.monographs.monograph(note.id)
);
setMonograph(monograph);
if (monograph) {
setSelfDestruct(!!monograph?.selfDestruct);
if (monograph.password) {
@@ -102,7 +80,7 @@ const PublishNoteSheet = ({
}
}
})();
}, [monograph]);
}, []);
const publishNote = async () => {
if (publishing) return;
@@ -111,12 +89,12 @@ 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
});
await monographData.execute();
setMonograph(await db.monographs.get(db.monographs.monograph(note.id)));
Navigation.queueRoutesForUpdate();
setPublishLoading(false);
}
@@ -132,6 +110,7 @@ const PublishNoteSheet = ({
setPublishLoading(false);
};
const setPublishLoading = (value: boolean) => {
setPublishing(value);
};
@@ -142,7 +121,7 @@ const PublishNoteSheet = ({
try {
if (note?.id) {
await db.monographs.unpublish(note.id);
monographData.execute();
setMonograph(undefined);
Navigation.queueRoutesForUpdate();
setPublishLoading(false);
}
@@ -166,15 +145,12 @@ const PublishNoteSheet = ({
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
{isPublished &&
(monographData?.result?.monograph || monographData?.loading) ? null : (
<DialogHeader
title={strings.publishNote()}
paragraph={strings.publishNoteDesc()}
/>
)}
<DialogHeader
title={strings.publishNote()}
paragraph={strings.publishNoteDesc()}
/>
{publishing || monographData.loading ? (
{publishing ? (
<View
style={{
justifyContent: "center",
@@ -200,14 +176,7 @@ const PublishNoteSheet = ({
) : (
<>
{isPublished && publishUrl ? (
<TouchableOpacity
onPress={async () => {
try {
await openLinkInBrowser(publishUrl);
} catch (e) {
console.error(e);
}
}}
<View
style={{
flexDirection: "row",
alignItems: "center",
@@ -229,6 +198,23 @@ const PublishNoteSheet = ({
<Paragraph size={AppFontSize.sm} numberOfLines={1}>
{publishUrl}
</Paragraph>
<Paragraph
onPress={async () => {
try {
await openLinkInBrowser(publishUrl);
} catch (e) {
console.error(e);
}
}}
size={AppFontSize.xs}
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL_SMALL,
color: colors.primary.paragraph
}}
>
<Icon color={colors.primary.accent} name="open-in-new" />{" "}
{strings.openInBrowser()}
</Paragraph>
</View>
<IconButton
@@ -244,16 +230,9 @@ const PublishNoteSheet = ({
size={AppFontSize.lg}
name="content-copy"
/>
</TouchableOpacity>
</View>
) : null}
<Input
fwdRef={titleInput}
onChangeText={(value) => (customTitle.current = value)}
defaultValue={customTitle.current}
placeholder={strings.noteTitle()}
/>
<TouchableOpacity
onPress={() => {
if (publishing) return;
@@ -281,9 +260,9 @@ const PublishNoteSheet = ({
justifyContent: "space-between"
}}
>
<Paragraph size={AppFontSize.sm}>
<Heading size={AppFontSize.md}>
{strings.monographPassHeading()}
</Paragraph>
</Heading>
<ToggleSwitch
isOn={isLocked}
onColor={colors.primary.accent}
@@ -294,7 +273,7 @@ const PublishNoteSheet = ({
/>
</View>
{/* <Paragraph>{strings.monographPassDesc()}</Paragraph> */}
<Paragraph>{strings.monographPassDesc()}</Paragraph>
{isLocked ? (
<>
@@ -340,9 +319,9 @@ const PublishNoteSheet = ({
justifyContent: "space-between"
}}
>
<Paragraph size={AppFontSize.sm}>
<Heading size={AppFontSize.md}>
{strings.monographSelfDestructHeading()}
</Paragraph>
</Heading>
<ToggleSwitch
isOn={selfDestruct}
onColor={colors.primary.accent}
@@ -353,42 +332,10 @@ const PublishNoteSheet = ({
/>
</View>
{/* <Paragraph>{strings.monographSelfDestructDesc()}</Paragraph> */}
<Paragraph>{strings.monographSelfDestructDesc()}</Paragraph>
</View>
</TouchableOpacity>
{isFeatureAvailable?.isAllowed ? (
<View
style={{
flexDirection: "row",
alignItems: "center",
backgroundColor: colors.secondary.background,
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
borderRadius: defaultBorderRadius,
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<View
style={{
width: "100%",
flexShrink: 1
}}
>
<View
style={{
flexDirection: "row",
justifyContent: "space-between"
}}
>
<Paragraph size={AppFontSize.sm}>{strings.views()}</Paragraph>
<Paragraph>
{monographData?.result?.analytics?.totalViews || 0}
</Paragraph>
</View>
</View>
</View>
) : null}
<View
style={{
width: "100%",

View File

@@ -96,6 +96,12 @@ class RecoveryKeySheet extends React.Component {
recoveryKeySaved: true
});
clearMessage();
// if (this.signup) {
// this.signup = false;
// setTimeout(() => {
// eSendEvent(eOpenResultDialog);
// }, 500);
// }
};
async componentDidMount() {
eSubscribeEvent(eOpenRecoveryKeyDialog, this.open);

View File

@@ -16,36 +16,33 @@ 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 {
ContentBlock,
InternalLink,
ItemReference,
Note,
TextSlice,
VirtualizedGrouping,
createInternalLink,
highlightInternalLinks
} from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { ContentBlock, ItemReference, Note } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
import { ActivityIndicator, View } from "react-native";
import { ScrollView } from "react-native-actions-sheet";
import { create } from "zustand";
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
import create from "zustand";
import { db } from "../../../common/database";
import { useDBItem, useNoteLocked } from "../../../hooks/use-db-item";
import { eSendEvent, presentSheet } from "../../../services/event-manager";
import { useRelationStore } from "../../../stores/use-relation-store";
import { eOnLoadNote } from "../../../utils/events";
import { fluidTabsRef } from "../../../utils/global-refs";
import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import { defaultBorderRadius, AppFontSize } from "../../../utils/size";
import SheetProvider from "../../sheet-provider";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import { Pressable } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../../utils/styles";
export const useExpandedStore = create<{
expanded: {
@@ -104,8 +101,8 @@ const ListBlockItem = ({
{item?.content.length > 200
? item?.content.slice(0, 200) + "..."
: !item.content || item.content.trim() === ""
? strings.linkNoteEmptyBlock()
: item.content}
? strings.linkNoteEmptyBlock()
: item.content}
</Paragraph>
<View
@@ -404,7 +401,7 @@ export const ReferencesList = ({ item, close }: ReferencesListProps) => {
const { colors } = useThemeColors();
const [items, setItems] = useState<VirtualizedGrouping<Note>>();
const hasNoRelations = !items || items?.placeholders?.length === 0;
const internalLinks = useRef<InternalLink<"note">[]>([]);
const internalLinks = useRef<InternalLink<"note">[]>();
useEffect(() => {
db.relations?.[tab === 0 ? "from" : "to"]?.(
@@ -504,11 +501,9 @@ export const ReferencesList = ({ item, close }: ReferencesListProps) => {
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
>
<LegendList
renderScrollComponent={(props) => <ScrollView {...props} />}
<FlashList
bounces={false}
data={items.placeholders || []}
extraData={items}
data={items.placeholders}
renderItem={renderNote}
/>
</View>

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