mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-31 11:08:47 +02:00
Compare commits
179 Commits
background
...
3.0.15-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c54759e766 | ||
|
|
f15446b1d6 | ||
|
|
aa2a8ab6f4 | ||
|
|
8e59ff1318 | ||
|
|
ad99c87f72 | ||
|
|
ba776b5854 | ||
|
|
e526145e48 | ||
|
|
45c5e65bf6 | ||
|
|
87bec4e2ab | ||
|
|
433e67a6dc | ||
|
|
6acba51b88 | ||
|
|
cf1322952d | ||
|
|
b5ac9dd430 | ||
|
|
a3b590bcb7 | ||
|
|
d4f7e3ef07 | ||
|
|
16119f6958 | ||
|
|
e39c2583a5 | ||
|
|
eecff009dd | ||
|
|
d9ed61a7da | ||
|
|
58db265702 | ||
|
|
bd39f0cbf8 | ||
|
|
255015108a | ||
|
|
b80f366f8d | ||
|
|
c845c7fce6 | ||
|
|
6e7b7ce0e1 | ||
|
|
9b728902d4 | ||
|
|
9a1f54c41a | ||
|
|
b3251eb016 | ||
|
|
f4064b7939 | ||
|
|
f014f08ff4 | ||
|
|
dee1cf4b5a | ||
|
|
f35a3d8fa3 | ||
|
|
ca9a8d14fa | ||
|
|
35c35fa9d7 | ||
|
|
09fff95d58 | ||
|
|
287307a1ba | ||
|
|
d628e62302 | ||
|
|
a070bc46ad | ||
|
|
9027c44511 | ||
|
|
722aa7b9c6 | ||
|
|
a4e777b24b | ||
|
|
af7120e953 | ||
|
|
d5adc3bbac | ||
|
|
aa3d778ad1 | ||
|
|
3465a816c6 | ||
|
|
3d1726c5aa | ||
|
|
488de2d583 | ||
|
|
c415b114a6 | ||
|
|
da36784e9c | ||
|
|
78fafdc272 | ||
|
|
7d57055f73 | ||
|
|
b84cc747cb | ||
|
|
9a4f3ccbad | ||
|
|
68de3cc928 | ||
|
|
6e135cdd36 | ||
|
|
696116ac85 | ||
|
|
08a13ff35a | ||
|
|
af1cea81dc | ||
|
|
2c22a7ca90 | ||
|
|
6fd6bd0375 | ||
|
|
1a96dc965a | ||
|
|
9550350f25 | ||
|
|
0ed605a5b9 | ||
|
|
18910d931c | ||
|
|
87648b7d8b | ||
|
|
7e7a1aeecb | ||
|
|
cb353358b7 | ||
|
|
ba1619711b | ||
|
|
b3cc9475ad | ||
|
|
ee5c4077fe | ||
|
|
47bbfc618e | ||
|
|
b377329a8b | ||
|
|
ae8bcf3865 | ||
|
|
2f67611829 | ||
|
|
058c612f4f | ||
|
|
be8df90ccb | ||
|
|
aa730b5b93 | ||
|
|
34bbcf6387 | ||
|
|
33c3c78e2f | ||
|
|
bf144dcda4 | ||
|
|
284ebdb86f | ||
|
|
9dac4cf7f4 | ||
|
|
fb973a5514 | ||
|
|
f9226510f9 | ||
|
|
5c0a964699 | ||
|
|
41ffaab3dc | ||
|
|
764c721f1d | ||
|
|
3d267f91ef | ||
|
|
f10f28a7f8 | ||
|
|
8d04f89a18 | ||
|
|
9d1374dcd4 | ||
|
|
094a65e514 | ||
|
|
372da979e1 | ||
|
|
d3371326f2 | ||
|
|
57452bd0e6 | ||
|
|
993b8460bf | ||
|
|
081201aa42 | ||
|
|
270e6778d1 | ||
|
|
2074320583 | ||
|
|
cd90030616 | ||
|
|
807bd683b7 | ||
|
|
35929026a4 | ||
|
|
41eb94ee47 | ||
|
|
ba51e110ad | ||
|
|
ecad06d249 | ||
|
|
19a151f637 | ||
|
|
7c4a6f1e17 | ||
|
|
bc88e953d1 | ||
|
|
7d89ec676a | ||
|
|
c8f8bcca93 | ||
|
|
9fa904e849 | ||
|
|
05cc947cb4 | ||
|
|
a20a683b17 | ||
|
|
5fc43c931e | ||
|
|
0879c3b4d6 | ||
|
|
2eded71de3 | ||
|
|
9e30ee50b8 | ||
|
|
08621a5873 | ||
|
|
17f5bd28ba | ||
|
|
4558c0b2eb | ||
|
|
a442677e91 | ||
|
|
95666bee36 | ||
|
|
c4232ac95a | ||
|
|
0283037289 | ||
|
|
b6fd0df715 | ||
|
|
f53fb58024 | ||
|
|
21d452308d | ||
|
|
823608cca1 | ||
|
|
f3a0b998e4 | ||
|
|
a47a38b16b | ||
|
|
a184c53dd5 | ||
|
|
a3464255ec | ||
|
|
088c6670a9 | ||
|
|
27840b9bd6 | ||
|
|
ae819504f0 | ||
|
|
af792e394c | ||
|
|
76a4d5c2a5 | ||
|
|
4e23acd608 | ||
|
|
3c65dcdd8b | ||
|
|
efea6e3972 | ||
|
|
185cca7880 | ||
|
|
fbded93405 | ||
|
|
674881b873 | ||
|
|
248fb6dd2d | ||
|
|
3211e7a149 | ||
|
|
08496c699e | ||
|
|
060fa25814 | ||
|
|
a6b1effc77 | ||
|
|
5cf9c4808e | ||
|
|
d5a44067fd | ||
|
|
0de463526f | ||
|
|
5fd79875ef | ||
|
|
bdb19ad325 | ||
|
|
7a12efc823 | ||
|
|
2f3bddf860 | ||
|
|
6127eb97af | ||
|
|
86386e14f3 | ||
|
|
9cc17c29f6 | ||
|
|
a31e863c7b | ||
|
|
15a32ac83a | ||
|
|
fa5b84ec0d | ||
|
|
7905584eac | ||
|
|
afc9b96a11 | ||
|
|
4e6466fc18 | ||
|
|
233baf78ce | ||
|
|
42d0bee37f | ||
|
|
ccadab6a10 | ||
|
|
bfe5a5cb96 | ||
|
|
88fc24c4cf | ||
|
|
8be4fea7a3 | ||
|
|
a3d39d8b6d | ||
|
|
ff36f0259a | ||
|
|
d13654088a | ||
|
|
8a6b97c757 | ||
|
|
d71e0c109b | ||
|
|
7e345303fe | ||
|
|
d881bdb77c | ||
|
|
4961102d8f | ||
|
|
4f2ab4f1da |
@@ -78,13 +78,7 @@ module.exports = {
|
||||
semi: ["error", "always"],
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"react/prop-types": "off",
|
||||
"header/header": ["error", "block", LICENSE, 1],
|
||||
"@typescript-eslint/no-empty-interface": [
|
||||
"error",
|
||||
{
|
||||
allowSingleExtends: true
|
||||
}
|
||||
]
|
||||
"header/header": ["error", "block", LICENSE, 1]
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
|
||||
8
.github/workflows/android.publish.yml
vendored
8
.github/workflows/android.publish.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Publish @notesnook/mobile
|
||||
name: Publish @notesnook/android
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
@@ -33,13 +33,15 @@ jobs:
|
||||
|
||||
- name: Sign app bundle for Playstore release
|
||||
id: sign_app
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
uses: r0adkll/sign-android-release@master
|
||||
with:
|
||||
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: "33.0.0"
|
||||
|
||||
- name: Publish to Playstore
|
||||
id: deploy
|
||||
@@ -64,6 +66,8 @@ jobs:
|
||||
alias: ${{ secrets.PUBLIC_ALIAS }}
|
||||
keyStorePassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
|
||||
keyPassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
|
||||
env:
|
||||
BUILD_TOOLS_VERSION: "33.0.0"
|
||||
|
||||
- name: Rename apk files
|
||||
run: |
|
||||
|
||||
30
.github/workflows/desktop.publish.yml
vendored
30
.github/workflows/desktop.publish.yml
vendored
@@ -18,6 +18,21 @@ on:
|
||||
required: true
|
||||
default: true
|
||||
description: "Publish on GitHub releases?"
|
||||
build-windows:
|
||||
type: boolean
|
||||
required: true
|
||||
default: true
|
||||
description: "Build for Windows?"
|
||||
build-linux:
|
||||
type: boolean
|
||||
required: true
|
||||
default: true
|
||||
description: "Build for Linux?"
|
||||
build-mac:
|
||||
type: boolean
|
||||
required: true
|
||||
default: true
|
||||
description: "Build for macOS?"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -71,6 +86,7 @@ jobs:
|
||||
build-macos:
|
||||
name: Build for macOS
|
||||
needs: build
|
||||
if: inputs.build-mac
|
||||
runs-on: macos-12
|
||||
|
||||
steps:
|
||||
@@ -163,6 +179,7 @@ jobs:
|
||||
build-linux:
|
||||
name: Build for Linux
|
||||
needs: build
|
||||
if: inputs.build-linux
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
@@ -221,12 +238,22 @@ jobs:
|
||||
build-windows:
|
||||
name: Build for Windows
|
||||
needs: build
|
||||
if: inputs.build-windows
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup .NET Core SDK
|
||||
uses: actions/setup-dotnet@v2
|
||||
with:
|
||||
dotnet-version: 6.0.x
|
||||
|
||||
- name: Install AzureCodeSigning
|
||||
run: Install-Module -Name AzureCodeSigning -RequiredVersion 0.3.0 -Force -Repository PSGallery
|
||||
shell: pwsh
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
@@ -247,6 +274,9 @@ jobs:
|
||||
- name: Publish
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
|
||||
run: |
|
||||
if ($${{ inputs.publish-github }} -eq $true) {
|
||||
yarn electron-builder --win --publish always
|
||||
|
||||
67
.github/workflows/ios.publish.yml
vendored
Normal file
67
.github/workflows/ios.publish.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
name: Publish @notesnook/ios
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-13
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install node modules
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=mobile
|
||||
|
||||
- name: Build packages
|
||||
run: npx nx run @notesnook/mobile:build
|
||||
|
||||
- name: Install Pods
|
||||
run: npm run prepare:ios
|
||||
|
||||
- name: Build iOS App
|
||||
uses: yukiarrr/ios-build-action@v1.11.2
|
||||
with:
|
||||
bundle-identifier: org.streetwriters.notesnook
|
||||
scheme: Notesnook
|
||||
configuration: "Release"
|
||||
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
|
||||
NotesWidgetExtension
|
||||
disable-targets: Notesnook-tvOS,Notesnook-tvOSTests,NotesnookTests
|
||||
code-signing-identity: Apple Distribution
|
||||
team-id: ${{ secrets.APPLE_TEAM_ID }}
|
||||
p12-base64: ${{ secrets.APPLE_CERTIFICATE_P12 }}
|
||||
certificate-password: ${{ secrets.APPLE_CERTIFICATE_P12_PASSWORD }}
|
||||
app-store-connect-api-key-issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
|
||||
app-store-connect-api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
|
||||
app-store-connect-api-key-base64: ${{ secrets.APPSTORE_CONNECT_API_KEY_BASE64 }}
|
||||
output-path: Notesnook.ipa
|
||||
mobileprovision-base64: |
|
||||
${{ secrets.APPLE_MOBILE_PROVISION_APP }}
|
||||
${{ secrets.APPLE_MOBILE_PROVISION_SHARE }}
|
||||
${{ secrets.APPLE_MOBILE_PROVISION_WIDGET }}
|
||||
|
||||
- name: 'Upload app to TestFlight'
|
||||
uses: apple-actions/upload-testflight-build@v1
|
||||
with:
|
||||
app-path: Notesnook.ipa
|
||||
issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
|
||||
api-key-id: ${{ secrets.API_KEY_ID }}
|
||||
api-private-key: ${{ secrets.API_KEY }}
|
||||
|
||||
- name: Upload to Github
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Notesnook.ipa
|
||||
path: Notesnook.ipa
|
||||
4
.github/workflows/web.beta.publish.yml
vendored
4
.github/workflows/web.beta.publish.yml
vendored
@@ -1,10 +1,11 @@
|
||||
name: Publish @notesnook/web Beta
|
||||
|
||||
on:
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- '!v3-beta'
|
||||
paths-ignore:
|
||||
- "apps/mobile/**"
|
||||
- "packages/editor-mobile/**"
|
||||
@@ -22,6 +23,7 @@ on:
|
||||
- synchronize
|
||||
branches:
|
||||
- master
|
||||
- '!v3-beta'
|
||||
paths-ignore:
|
||||
- "apps/mobile/**"
|
||||
- "packages/editor-mobile/**"
|
||||
|
||||
40
.github/workflows/web.v3-beta.publish.yml
vendored
Normal file
40
.github/workflows/web.v3-beta.publish.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: Publish @notesnook/web v3 Beta
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
|
||||
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
|
||||
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate beta build
|
||||
run: npm run build:beta:web
|
||||
|
||||
- name: Publish to Cloudflare Pages
|
||||
uses: unlike-ltd/github-actions-cloudflare-pages@v0.1.1
|
||||
id: pages
|
||||
with:
|
||||
cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
cloudflare-account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
cloudflare-project-name: notesnook-v3-app
|
||||
directory: ./apps/web/build
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
github-environment: ${{ (github.ref == 'refs/heads/master' && 'beta') || 'preview' }}
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -9,4 +9,5 @@ nx-cloud.env
|
||||
.env.local
|
||||
.nyc_output
|
||||
site
|
||||
node_modules.backup
|
||||
node_modules.backup
|
||||
.nx
|
||||
176
apps/desktop/package-lock.json
generated
176
apps/desktop/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "2.6.12",
|
||||
"version": "2.6.17",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "2.6.12",
|
||||
"version": "2.6.17",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
@@ -24,8 +24,8 @@
|
||||
"@types/node": "18.16.1",
|
||||
"@types/yargs": "^17.0.24",
|
||||
"chokidar": "^3.5.3",
|
||||
"electron": "25.9.3",
|
||||
"electron-builder": "^24.6.4",
|
||||
"electron": "25.9.8",
|
||||
"electron-builder": "^24.9.1",
|
||||
"electron-builder-notarize": "^1.5.1",
|
||||
"esbuild": "^0.17.19",
|
||||
"tree-kill": "^1.2.2",
|
||||
@@ -173,12 +173,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/asar": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.4.tgz",
|
||||
"integrity": "sha512-lykfY3TJRRWFeTxccEKdf1I6BLl2Plw81H0bbp4Fc5iEc67foDCa5pjJQULVgo0wF+Dli75f3xVcdb/67FFZ/g==",
|
||||
"version": "3.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.8.tgz",
|
||||
"integrity": "sha512-cmskk5M06ewHMZAplSiF4AlME3IrnnZhKnWbtwKVLRkdJkKyUVjMLhDIiPIx/+6zQWVlKX/LtmK9xDme7540Sg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chromium-pickle-js": "^0.2.0",
|
||||
"commander": "^5.0.0",
|
||||
"glob": "^7.1.6",
|
||||
"minimatch": "^3.0.4"
|
||||
@@ -869,9 +868,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/debug": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz",
|
||||
"integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
||||
"integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/ms": "*"
|
||||
@@ -902,9 +901,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ms": {
|
||||
"version": "0.7.31",
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
|
||||
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==",
|
||||
"version": "0.7.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
|
||||
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
@@ -970,9 +969,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/7zip-bin": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.1.1.tgz",
|
||||
"integrity": "sha512-sAP4LldeWNz0lNzmTird3uWfFDWWTeg6V/MsmyyLR9X1idwKBWIgt/ZvinqQldJm3LecKEs1emkbquO6PCiLVQ==",
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz",
|
||||
"integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
@@ -1054,9 +1053,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/app-builder-lib": {
|
||||
"version": "24.6.4",
|
||||
"resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.6.4.tgz",
|
||||
"integrity": "sha512-m9931WXb83teb32N0rKg+ulbn6+Hl8NV5SUpVDOVz9MWOXfhV6AQtTdftf51zJJvCQnQugGtSqoLvgw6mdF/Rg==",
|
||||
"version": "24.9.1",
|
||||
"resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.9.1.tgz",
|
||||
"integrity": "sha512-Q1nYxZcio4r+W72cnIRVYofEAyjBd3mG47o+zms8HlD51zWtA/YxJb01Jei5F+jkWhge/PTQK+uldsPh6d0/4g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@develar/schema-utils": "~2.6.5",
|
||||
@@ -1065,15 +1064,15 @@
|
||||
"@electron/universal": "1.4.1",
|
||||
"@malept/flatpak-bundler": "^0.4.0",
|
||||
"@types/fs-extra": "9.0.13",
|
||||
"7zip-bin": "~5.1.1",
|
||||
"7zip-bin": "~5.2.0",
|
||||
"async-exit-hook": "^2.0.1",
|
||||
"bluebird-lst": "^1.0.9",
|
||||
"builder-util": "24.5.0",
|
||||
"builder-util-runtime": "9.2.1",
|
||||
"builder-util": "24.8.1",
|
||||
"builder-util-runtime": "9.2.3",
|
||||
"chromium-pickle-js": "^0.2.0",
|
||||
"debug": "^4.3.4",
|
||||
"ejs": "^3.1.8",
|
||||
"electron-publish": "24.5.0",
|
||||
"electron-publish": "24.8.1",
|
||||
"form-data": "^4.0.0",
|
||||
"fs-extra": "^10.1.0",
|
||||
"hosted-git-info": "^4.1.0",
|
||||
@@ -1092,6 +1091,19 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/app-builder-lib/node_modules/builder-util-runtime": {
|
||||
"version": "9.2.3",
|
||||
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz",
|
||||
"integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4",
|
||||
"sax": "^1.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
@@ -1116,9 +1128,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
|
||||
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==",
|
||||
"version": "3.2.5",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
|
||||
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/async-exit-hook": {
|
||||
@@ -1280,16 +1292,16 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/builder-util": {
|
||||
"version": "24.5.0",
|
||||
"resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.5.0.tgz",
|
||||
"integrity": "sha512-STnBmZN/M5vGcv01u/K8l+H+kplTaq4PAIn3yeuufUKSpcdro0DhJWxPI81k5XcNfC//bjM3+n9nr8F9uV4uAQ==",
|
||||
"version": "24.8.1",
|
||||
"resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.8.1.tgz",
|
||||
"integrity": "sha512-ibmQ4BnnqCnJTNrdmdNlnhF48kfqhNzSeqFMXHLIl+o9/yhn6QfOaVrloZ9YUu3m0k3rexvlT5wcki6LWpjTZw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/debug": "^4.1.6",
|
||||
"7zip-bin": "~5.1.1",
|
||||
"7zip-bin": "~5.2.0",
|
||||
"app-builder-bin": "4.0.0",
|
||||
"bluebird-lst": "^1.0.9",
|
||||
"builder-util-runtime": "9.2.1",
|
||||
"builder-util-runtime": "9.2.3",
|
||||
"chalk": "^4.1.2",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"debug": "^4.3.4",
|
||||
@@ -1315,6 +1327,19 @@
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/builder-util/node_modules/builder-util-runtime": {
|
||||
"version": "9.2.3",
|
||||
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz",
|
||||
"integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4",
|
||||
"sax": "^1.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/busboy": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
@@ -1413,9 +1438,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ci-info": {
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz",
|
||||
"integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==",
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
|
||||
"integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -1702,14 +1727,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dmg-builder": {
|
||||
"version": "24.6.4",
|
||||
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.6.4.tgz",
|
||||
"integrity": "sha512-BNcHRc9CWEuI9qt0E655bUBU/j/3wUCYBVKGu1kVpbN5lcUdEJJJeiO0NHK3dgKmra6LUUZlo+mWqc+OCbi0zw==",
|
||||
"version": "24.9.1",
|
||||
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.9.1.tgz",
|
||||
"integrity": "sha512-huC+O6hvHd24Ubj3cy2GMiGLe2xGFKN3klqVMLAdcbB6SWMd1yPSdZvV8W1O01ICzCCRlZDHiv4VrNUgnPUfbQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"app-builder-lib": "24.6.4",
|
||||
"builder-util": "24.5.0",
|
||||
"builder-util-runtime": "9.2.1",
|
||||
"app-builder-lib": "24.9.1",
|
||||
"builder-util": "24.8.1",
|
||||
"builder-util-runtime": "9.2.3",
|
||||
"fs-extra": "^10.1.0",
|
||||
"iconv-lite": "^0.6.2",
|
||||
"js-yaml": "^4.1.0"
|
||||
@@ -1718,6 +1743,19 @@
|
||||
"dmg-license": "^1.0.11"
|
||||
}
|
||||
},
|
||||
"node_modules/dmg-builder/node_modules/builder-util-runtime": {
|
||||
"version": "9.2.3",
|
||||
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz",
|
||||
"integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4",
|
||||
"sax": "^1.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dmg-license": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz",
|
||||
@@ -1774,9 +1812,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron": {
|
||||
"version": "25.9.3",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-25.9.3.tgz",
|
||||
"integrity": "sha512-dacaHg/PuwVcFRgPDCM5j7UDzqGJWOsbBRdS5wPKLNS/ejPeccIjuNUT1cqcrpvCJKAFW8swHWg9kdizNSEDHQ==",
|
||||
"version": "25.9.8",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-25.9.8.tgz",
|
||||
"integrity": "sha512-PGgp6PH46QVENHuAHc2NT1Su8Q1qov7qIl2jI5tsDpTibwV2zD8539AeWBQySeBU4dhbj9onIl7+1bXQ0wefBg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
@@ -1792,16 +1830,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron-builder": {
|
||||
"version": "24.6.4",
|
||||
"resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.6.4.tgz",
|
||||
"integrity": "sha512-uNWQoU7pE7qOaIQ6CJHpBi44RJFVG8OHRBIadUxrsDJVwLLo8Nma3K/EEtx5/UyWAQYdcK4nVPYKoRqBb20hbA==",
|
||||
"version": "24.9.1",
|
||||
"resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.9.1.tgz",
|
||||
"integrity": "sha512-v7BuakDuY6sKMUYM8mfQGrwyjBpZ/ObaqnenU0H+igEL10nc6ht049rsCw2HghRBdEwJxGIBuzs3jbEhNaMDmg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"app-builder-lib": "24.6.4",
|
||||
"builder-util": "24.5.0",
|
||||
"builder-util-runtime": "9.2.1",
|
||||
"app-builder-lib": "24.9.1",
|
||||
"builder-util": "24.8.1",
|
||||
"builder-util-runtime": "9.2.3",
|
||||
"chalk": "^4.1.2",
|
||||
"dmg-builder": "24.6.4",
|
||||
"dmg-builder": "24.9.1",
|
||||
"fs-extra": "^10.1.0",
|
||||
"is-ci": "^3.0.0",
|
||||
"lazy-val": "^1.0.5",
|
||||
@@ -1863,6 +1901,19 @@
|
||||
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/electron-builder/node_modules/builder-util-runtime": {
|
||||
"version": "9.2.3",
|
||||
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz",
|
||||
"integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4",
|
||||
"sax": "^1.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-notarize": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/electron-notarize/-/electron-notarize-1.2.2.tgz",
|
||||
@@ -1893,20 +1944,33 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron-publish": {
|
||||
"version": "24.5.0",
|
||||
"resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.5.0.tgz",
|
||||
"integrity": "sha512-zwo70suH15L15B4ZWNDoEg27HIYoPsGJUF7xevLJLSI7JUPC8l2yLBdLGwqueJ5XkDL7ucYyRZzxJVR8ElV9BA==",
|
||||
"version": "24.8.1",
|
||||
"resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.8.1.tgz",
|
||||
"integrity": "sha512-IFNXkdxMVzUdweoLJNXSupXkqnvgbrn3J4vognuOY06LaS/m0xvfFYIf+o1CM8if6DuWYWoQFKPcWZt/FUjZPw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/fs-extra": "^9.0.11",
|
||||
"builder-util": "24.5.0",
|
||||
"builder-util-runtime": "9.2.1",
|
||||
"builder-util": "24.8.1",
|
||||
"builder-util-runtime": "9.2.3",
|
||||
"chalk": "^4.1.2",
|
||||
"fs-extra": "^10.1.0",
|
||||
"lazy-val": "^1.0.5",
|
||||
"mime": "^2.5.2"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-publish/node_modules/builder-util-runtime": {
|
||||
"version": "9.2.3",
|
||||
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz",
|
||||
"integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4",
|
||||
"sax": "^1.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-trpc": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/electron-trpc/-/electron-trpc-0.5.2.tgz",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "2.6.12",
|
||||
"version": "2.6.17",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
@@ -25,8 +25,8 @@
|
||||
"@types/node": "18.16.1",
|
||||
"@types/yargs": "^17.0.24",
|
||||
"chokidar": "^3.5.3",
|
||||
"electron": "25.9.3",
|
||||
"electron-builder": "^24.6.4",
|
||||
"electron": "25.9.8",
|
||||
"electron-builder": "^24.9.1",
|
||||
"electron-builder-notarize": "^1.5.1",
|
||||
"esbuild": "^0.17.19",
|
||||
"tree-kill": "^1.2.2",
|
||||
@@ -135,6 +135,10 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"signingHashAlgorithms": [
|
||||
"sha256"
|
||||
],
|
||||
"sign": "./sign.js",
|
||||
"icon": "assets/icons/app.ico"
|
||||
},
|
||||
"portable": {
|
||||
|
||||
60
apps/desktop/sign.js
Normal file
60
apps/desktop/sign.js
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
const { writeFileSync, rmSync, readFileSync } = require("fs");
|
||||
const { execSync } = require("child_process");
|
||||
const { relative, join } = require("path");
|
||||
|
||||
module.exports = async function (configuration) {
|
||||
const Endpoint = "https://weu.codesigning.azure.net";
|
||||
const CodeSigningAccountName = "Notesnook";
|
||||
const CertificateProfileName = "Notesnook";
|
||||
const FileDigest = configuration.hash.toUpperCase();
|
||||
const TimestampRfc3161 = "http://timestamp.acs.microsoft.com";
|
||||
const TimestampDigest = configuration.hash.toUpperCase();
|
||||
const Description = "The Notesnook app";
|
||||
const DescriptionUrl = "https://notesnook.com/";
|
||||
const FilesCatalog = createCatalog(configuration.path);
|
||||
|
||||
const command = `Invoke-AzureCodeSigning -Endpoint "${Endpoint}" -CodeSigningAccountName "${CodeSigningAccountName}" -CertificateProfileName "${CertificateProfileName}" -FileDigest "${FileDigest}" -TimestampRfc3161 "${TimestampRfc3161}" -TimestampDigest "${TimestampDigest}" -Description "${Description}" -DescriptionUrl "${DescriptionUrl}" -FilesCatalog "${FilesCatalog}"`;
|
||||
|
||||
console.debug("Signing", configuration.path, "using command", command);
|
||||
|
||||
psexec(command);
|
||||
|
||||
console.debug("Signed", configuration.path);
|
||||
|
||||
deleteCatalog();
|
||||
};
|
||||
|
||||
function createCatalog(path) {
|
||||
writeFileSync("_catalog", relative(__dirname, path));
|
||||
return join(__dirname, "_catalog");
|
||||
}
|
||||
|
||||
function deleteCatalog() {
|
||||
rmSync("_catalog");
|
||||
}
|
||||
|
||||
function psexec(cmd) {
|
||||
return execSync(cmd, {
|
||||
env: process.env,
|
||||
stdio: "inherit",
|
||||
shell: "pwsh"
|
||||
});
|
||||
}
|
||||
@@ -54,6 +54,14 @@ export const osIntegrationRouter = t.router({
|
||||
config.zoomFactor = factor;
|
||||
}),
|
||||
|
||||
proxyRules: t.procedure.query(() => config.proxyRules),
|
||||
setProxyRules: t.procedure
|
||||
.input(z.string().optional())
|
||||
.mutation(({ input: proxyRules }) => {
|
||||
globalThis.window?.webContents.session.setProxy({ proxyRules });
|
||||
config.proxyRules = proxyRules || "";
|
||||
}),
|
||||
|
||||
privacyMode: t.procedure.query(() => config.privacyMode),
|
||||
setPrivacyMode: t.procedure
|
||||
.input(z.object({ enabled: z.boolean() }))
|
||||
|
||||
@@ -118,5 +118,13 @@ export const spellCheckerRouter = t.router({
|
||||
.mutation(({ input: { enabled } }) => {
|
||||
globalThis.window?.webContents.session.setSpellCheckerEnabled(enabled);
|
||||
config.isSpellCheckerEnabled = enabled;
|
||||
})
|
||||
}),
|
||||
words: t.procedure.query(() =>
|
||||
globalThis.window?.webContents.session.listWordsInSpellCheckerDictionary()
|
||||
),
|
||||
deleteWord: t.procedure.input(z.string()).mutation(({ input: word }) => {
|
||||
globalThis.window?.webContents.session.removeWordFromSpellCheckerDictionary(
|
||||
word
|
||||
);
|
||||
})
|
||||
});
|
||||
|
||||
@@ -112,6 +112,7 @@ async function createWindow() {
|
||||
mainWindow.webContents.session.setSpellCheckerDictionaryDownloadURL(
|
||||
"http://dictionaries.notesnook.com/"
|
||||
);
|
||||
mainWindow.webContents.session.setProxy({ proxyRules: config.proxyRules });
|
||||
|
||||
mainWindow.once("closed", () => {
|
||||
globalThis.window = null;
|
||||
|
||||
@@ -25,7 +25,11 @@ Type=Application
|
||||
Version=${app.getVersion()}
|
||||
Name=${app.getName()}
|
||||
Comment=${app.getName()} startup script
|
||||
Exec=${process.execPath}${hidden ? " --hidden" : ""}
|
||||
Exec=${
|
||||
process.env.APPIMAGE
|
||||
? `${process.env.APPIMAGE}${hidden ? " --hidden" : ""}`
|
||||
: `${process.execPath}${hidden ? " --hidden" : ""}`
|
||||
}
|
||||
StartupNotify=false
|
||||
Terminal=false`;
|
||||
|
||||
|
||||
@@ -41,7 +41,8 @@ export const config = {
|
||||
isSpellCheckerEnabled: true,
|
||||
zoomFactor: 1,
|
||||
theme: nativeTheme.themeSource,
|
||||
automaticUpdates: true
|
||||
automaticUpdates: true,
|
||||
proxyRules: ""
|
||||
};
|
||||
|
||||
type ConfigKey = keyof typeof config;
|
||||
|
||||
@@ -131,6 +131,17 @@ function setupMenu() {
|
||||
);
|
||||
}
|
||||
|
||||
if (params.mediaType === "image")
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
id: "copy-image",
|
||||
label: "Copy Image",
|
||||
click() {
|
||||
globalThis.window?.webContents.copyImageAt(params.x, params.y);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
if (params.isEditable)
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
|
||||
@@ -17,14 +17,13 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import "@azure/core-asynciterator-polyfill";
|
||||
import "react-native-gesture-handler";
|
||||
import SettingsService from "./services/settings";
|
||||
import {
|
||||
THEME_COMPATIBILITY_VERSION,
|
||||
useThemeEngineStore
|
||||
} from "@notesnook/theme";
|
||||
import React, { useEffect } from "react";
|
||||
import { View } from "react-native";
|
||||
import { I18nManager, View } from "react-native";
|
||||
import "react-native-gesture-handler";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
import AppLockedOverlay from "./components/app-lock-overlay";
|
||||
@@ -32,20 +31,25 @@ import { withErrorBoundry } from "./components/exception-handler";
|
||||
import GlobalSafeAreaProvider from "./components/globalsafearea";
|
||||
import { useAppEvents } from "./hooks/use-app-events";
|
||||
import { ApplicationHolder } from "./navigation";
|
||||
import { themeTrpcClient } from "./screens/settings/theme-selector";
|
||||
import Notifications from "./services/notifications";
|
||||
import SettingsService from "./services/settings";
|
||||
import { TipManager } from "./services/tip-manager";
|
||||
import { useThemeStore } from "./stores/use-theme-store";
|
||||
import { useUserStore } from "./stores/use-user-store";
|
||||
import { themeTrpcClient } from "./screens/settings/theme-selector";
|
||||
|
||||
I18nManager.allowRTL(false);
|
||||
I18nManager.forceRTL(false);
|
||||
I18nManager.swapLeftAndRightInRTL(false);
|
||||
|
||||
SettingsService.checkOrientation();
|
||||
const App = () => {
|
||||
const init = useAppEvents();
|
||||
useEffect(() => {
|
||||
const { appLockMode } = SettingsService.get();
|
||||
let { appLockMode } = SettingsService.get();
|
||||
if (appLockMode && appLockMode !== "none") {
|
||||
useUserStore.getState().lockApp(true);
|
||||
}
|
||||
//@ts-ignore
|
||||
globalThis["IS_MAIN_APP_RUNNING"] = true;
|
||||
init();
|
||||
setTimeout(async () => {
|
||||
@@ -97,10 +101,10 @@ const App = () => {
|
||||
let currTheme =
|
||||
useThemeStore.getState().colorScheme === "dark"
|
||||
? SettingsService.getProperty("darkTheme")
|
||||
: SettingsService.getProperty("lighTheme");
|
||||
: SettingsService.getProperty("lightTheme");
|
||||
useThemeEngineStore.getState().setTheme(currTheme);
|
||||
|
||||
export const withTheme = (Element: () => JSX.Element) => {
|
||||
export const withTheme = (Element) => {
|
||||
return function AppWithThemeProvider() {
|
||||
const [colorScheme, darkTheme, lightTheme] = useThemeStore((state) => [
|
||||
state.colorScheme,
|
||||
@@ -16,17 +16,15 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import "./logger";
|
||||
import { database } from "@notesnook/common";
|
||||
import { logger as dbLogger } from "@notesnook/core/dist/logger";
|
||||
import { Platform } from "react-native";
|
||||
import * as Gzip from "react-native-gzip";
|
||||
import EventSource from "../../utils/sse/even-source-ios";
|
||||
import AndroidEventSource from "../../utils/sse/event-source";
|
||||
import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
|
||||
import filesystem from "../filesystem";
|
||||
import "./logger";
|
||||
import Storage from "./storage";
|
||||
import { RNSqliteDriver } from "./sqlite.kysely";
|
||||
|
||||
database.host(
|
||||
__DEV__
|
||||
@@ -51,30 +49,15 @@ database.host(
|
||||
}
|
||||
);
|
||||
|
||||
database.setup({
|
||||
storage: Storage,
|
||||
eventsource: Platform.OS === "ios" ? EventSource : AndroidEventSource,
|
||||
fs: filesystem,
|
||||
compressor: {
|
||||
database.setup(
|
||||
Storage,
|
||||
Platform.OS === "ios" ? EventSource : AndroidEventSource,
|
||||
filesystem,
|
||||
{
|
||||
compress: Gzip.deflate,
|
||||
decompress: Gzip.inflate
|
||||
},
|
||||
batchSize: 500,
|
||||
sqliteOptions: {
|
||||
dialect: {
|
||||
createDriver: () =>
|
||||
new RNSqliteDriver({ async: true, dbName: "test.db" }),
|
||||
createAdapter: () => new SqliteAdapter(),
|
||||
createIntrospector: (db) => new SqliteIntrospector(db),
|
||||
createQueryCompiler: () => new SqliteQueryCompiler()
|
||||
}
|
||||
// journalMode: "MEMORY",
|
||||
// synchronous: "normal",
|
||||
// pageSize: 8192,
|
||||
// cacheSize: -16000,
|
||||
// lockingMode: "exclusive"
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
export const db = database;
|
||||
export const DatabaseLogger = dbLogger;
|
||||
|
||||
@@ -16,14 +16,14 @@ 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 { initalize } from "@notesnook/core/dist/logger";
|
||||
import { MMKVLoader } from "react-native-mmkv-storage";
|
||||
import { initialize } from "@notesnook/core/dist/logger";
|
||||
import { KV } from "./storage";
|
||||
|
||||
const LoggerStorage = new MMKVLoader()
|
||||
.withInstanceID("notesnook_logs")
|
||||
.initialize();
|
||||
|
||||
initialize(new KV(LoggerStorage));
|
||||
initalize(new KV(LoggerStorage));
|
||||
|
||||
export { LoggerStorage };
|
||||
|
||||
@@ -1,123 +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 type { DatabaseConnection, Driver, QueryResult } from "kysely";
|
||||
import { CompiledQuery } from "kysely";
|
||||
import { QuickSQLiteConnection, open } from "react-native-quick-sqlite";
|
||||
|
||||
type Config = { dbName: string; async: boolean; location: string };
|
||||
|
||||
export class RNSqliteDriver implements Driver {
|
||||
private connection?: DatabaseConnection;
|
||||
private connectionMutex = new ConnectionMutex();
|
||||
private db: QuickSQLiteConnection;
|
||||
constructor(private readonly config: Config) {
|
||||
this.db = open({
|
||||
name: config.dbName
|
||||
});
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
this.connection = new RNSqliteConnection(this.db);
|
||||
}
|
||||
|
||||
async acquireConnection(): Promise<DatabaseConnection> {
|
||||
// SQLite only has one single connection. We use a mutex here to wait
|
||||
// until the single connection has been released.
|
||||
await this.connectionMutex.lock();
|
||||
return this.connection!;
|
||||
}
|
||||
|
||||
async beginTransaction(connection: DatabaseConnection): Promise<void> {
|
||||
await connection.executeQuery(CompiledQuery.raw("begin"));
|
||||
}
|
||||
|
||||
async commitTransaction(connection: DatabaseConnection): Promise<void> {
|
||||
await connection.executeQuery(CompiledQuery.raw("commit"));
|
||||
}
|
||||
|
||||
async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
|
||||
await connection.executeQuery(CompiledQuery.raw("rollback"));
|
||||
}
|
||||
|
||||
async releaseConnection(): Promise<void> {
|
||||
this.connectionMutex.unlock();
|
||||
}
|
||||
|
||||
async destroy(): Promise<void> {
|
||||
this.db.close();
|
||||
}
|
||||
}
|
||||
|
||||
class ConnectionMutex {
|
||||
private promise?: Promise<void>;
|
||||
private resolve?: () => void;
|
||||
|
||||
async lock(): Promise<void> {
|
||||
while (this.promise) {
|
||||
await this.promise;
|
||||
}
|
||||
|
||||
this.promise = new Promise((resolve) => {
|
||||
this.resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
unlock(): void {
|
||||
const resolve = this.resolve;
|
||||
|
||||
this.promise = undefined;
|
||||
this.resolve = undefined;
|
||||
|
||||
resolve?.();
|
||||
}
|
||||
}
|
||||
|
||||
class RNSqliteConnection implements DatabaseConnection {
|
||||
constructor(private readonly db: QuickSQLiteConnection) {}
|
||||
|
||||
streamQuery<R>(): AsyncIterableIterator<QueryResult<R>> {
|
||||
throw new Error("wasqlite driver doesn't support streaming");
|
||||
}
|
||||
|
||||
async executeQuery<R>(
|
||||
compiledQuery: CompiledQuery<unknown>
|
||||
): Promise<QueryResult<R>> {
|
||||
const { parameters, sql, query } = compiledQuery;
|
||||
const mode =
|
||||
query.kind === "SelectQueryNode"
|
||||
? "query"
|
||||
: query.kind === "RawNode"
|
||||
? "raw"
|
||||
: "exec";
|
||||
const result = await this.db.executeAsync(sql, parameters as any[]);
|
||||
|
||||
// console.log("SQLITE result:", result?.rows?._array);
|
||||
if (mode === "query" || !result.insertId)
|
||||
return {
|
||||
rows: result.rows?._array || []
|
||||
};
|
||||
|
||||
return {
|
||||
insertId: BigInt(result.insertId),
|
||||
numAffectedRows: BigInt(result.rowsAffected),
|
||||
rows: mode === "raw" ? result.rows?._array || [] : []
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -29,12 +29,13 @@ import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { subscribe, zip } from "react-native-zip-archive";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { ShareComponent } from "../../components/sheets/export-notes/share";
|
||||
import { ToastManager, presentSheet } from "../../services/event-manager";
|
||||
import { ToastEvent, presentSheet } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { db } from "../database";
|
||||
import Storage from "../database/storage";
|
||||
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
|
||||
import { createCacheDir } from "./io";
|
||||
import { createCacheDir, exists } from "./io";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
|
||||
export const FileDownloadStatus = {
|
||||
Success: 1,
|
||||
@@ -181,12 +182,14 @@ export default async function downloadAttachment(
|
||||
silent: false,
|
||||
cache: false,
|
||||
throwError: false,
|
||||
groupId: undefined
|
||||
groupId: undefined,
|
||||
base64: false,
|
||||
text: false
|
||||
}
|
||||
) {
|
||||
await createCacheDir();
|
||||
|
||||
let attachment = await db.attachments.attachment(hash);
|
||||
let attachment = db.attachments.attachment(hash);
|
||||
if (!attachment) {
|
||||
console.log("attachment not found");
|
||||
return;
|
||||
@@ -208,16 +211,21 @@ export default async function downloadAttachment(
|
||||
attachment.metadata.hash,
|
||||
options.groupId
|
||||
);
|
||||
await db
|
||||
.fs()
|
||||
.downloadFile(
|
||||
options.groupId || attachment.metadata.hash,
|
||||
attachment.metadata.hash
|
||||
);
|
||||
if (
|
||||
!(await RNFetchBlob.fs.exists(`${cacheDir}/${attachment.metadata.hash}`))
|
||||
)
|
||||
await db.fs.downloadFile(
|
||||
options.groupId || attachment.metadata.hash,
|
||||
attachment.metadata.hash
|
||||
);
|
||||
|
||||
if (!(await exists(attachment.metadata.hash))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.base64 || options.text) {
|
||||
return await db.attachments.read(
|
||||
attachment.metadata.hash,
|
||||
options.base64 ? "base64" : "text"
|
||||
);
|
||||
}
|
||||
|
||||
let filename = getFileNameWithExtension(
|
||||
attachment.metadata.filename,
|
||||
@@ -235,7 +243,8 @@ export default async function downloadAttachment(
|
||||
mime: attachment.metadata.type,
|
||||
fileName: options.cache ? undefined : filename,
|
||||
uri: options.cache ? undefined : folder.uri,
|
||||
chunkSize: attachment.chunkSize
|
||||
chunkSize: attachment.chunkSize,
|
||||
appGroupId: IOS_APPGROUPID
|
||||
};
|
||||
|
||||
let fileUri = await Sodium.decryptFile(
|
||||
@@ -245,7 +254,7 @@ export default async function downloadAttachment(
|
||||
);
|
||||
|
||||
if (!options.silent) {
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Download successful",
|
||||
message: filename + " downloaded",
|
||||
type: "success"
|
||||
|
||||
@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import hosts from "@notesnook/core/dist/utils/constants";
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { ToastEvent } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { db } from "../database";
|
||||
import { cacheDir, fileCheck } from "./utils";
|
||||
@@ -74,13 +74,13 @@ export async function downloadFile(filename, data, cancelToken) {
|
||||
return status >= 200 && status < 300;
|
||||
} catch (e) {
|
||||
if (e.message !== "canceled") {
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Error downloading file",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "global"
|
||||
});
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Error downloading file",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
@@ -98,7 +98,7 @@ export async function downloadFile(filename, data, cancelToken) {
|
||||
export async function getUploadedFileSize(hash) {
|
||||
try {
|
||||
const url = `${hosts.API_HOST}/s3?name=${hash}`;
|
||||
const token = await db.tokenManager.getAccessToken();
|
||||
const token = await db.user.tokenManager.getAccessToken();
|
||||
|
||||
const attachmentInfo = await fetch(url, {
|
||||
method: "HEAD",
|
||||
|
||||
@@ -36,9 +36,13 @@ export async function readEncrypted(filename, key, cipherData) {
|
||||
}
|
||||
|
||||
const attachment = db.attachments.attachment(filename);
|
||||
const isPng = /(png)/g.test(attachment?.metadata.type);
|
||||
const isJpeg = /(jpeg|jpg)/g.test(attachment?.metadata.type);
|
||||
console.log("decrypting....");
|
||||
const isPng = !attachment.metadata.type
|
||||
? false
|
||||
: /(png)/g.test(attachment?.metadata.type);
|
||||
const isJpeg = !attachment.metadata.type
|
||||
? false
|
||||
: /(jpeg|jpg)/g.test(attachment?.metadata.type);
|
||||
|
||||
let output = await Sodium.decryptFile(
|
||||
key,
|
||||
{
|
||||
|
||||
@@ -18,8 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
import { Platform, TextInput, View } from "react-native";
|
||||
//@ts-ignore
|
||||
import { Platform, View } from "react-native";
|
||||
import { enabled } from "react-native-privacy-snapshot";
|
||||
import { db } from "../../common/database";
|
||||
import { useAppState } from "../../hooks/use-app-state";
|
||||
@@ -42,8 +41,8 @@ const AppLockedOverlay = () => {
|
||||
const appLocked = useUserStore((state) => state.appLocked);
|
||||
const lockApp = useUserStore((state) => state.lockApp);
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const passwordInputRef = useRef<TextInput>(null);
|
||||
const password = useRef<string>();
|
||||
const passwordInputRef = useRef();
|
||||
const password = useRef();
|
||||
const appState = useAppState();
|
||||
|
||||
const biometricUnlockAwaitingUserInput = useRef(false);
|
||||
@@ -56,14 +55,14 @@ const AppLockedOverlay = () => {
|
||||
}
|
||||
useSettingStore.getState().setRequestBiometrics(true);
|
||||
|
||||
const unlocked = await BiometricService.validateUser(
|
||||
let unlocked = await BiometricService.validateUser(
|
||||
"Unlock to access your notes",
|
||||
""
|
||||
);
|
||||
if (unlocked) {
|
||||
lockApp(false);
|
||||
enabled(false);
|
||||
password.current = undefined;
|
||||
password.current = null;
|
||||
}
|
||||
setTimeout(() => {
|
||||
biometricUnlockAwaitingUserInput.current = false;
|
||||
@@ -74,11 +73,11 @@ const AppLockedOverlay = () => {
|
||||
const onSubmit = async () => {
|
||||
if (!password.current) return;
|
||||
try {
|
||||
const unlocked = await db.user.verifyPassword(password.current);
|
||||
let unlocked = await db.user.verifyPassword(password.current);
|
||||
if (unlocked) {
|
||||
lockApp(false);
|
||||
enabled(false);
|
||||
password.current = undefined;
|
||||
password.current = null;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -171,9 +170,7 @@ const AppLockedOverlay = () => {
|
||||
secureTextEntry
|
||||
placeholder="Enter account password"
|
||||
onChangeText={(v) => (password.current = v)}
|
||||
onSubmit={() => {
|
||||
onSubmit();
|
||||
}}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
@@ -17,13 +17,9 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { formatBytes } from "@notesnook/common";
|
||||
import { Attachment, Note, VirtualizedGrouping } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import React, { RefObject, useEffect, useState } from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { ActionSheetRef } from "react-native-actions-sheet";
|
||||
import { ScrollView } from "react-native-gesture-handler";
|
||||
import { db } from "../../common/database";
|
||||
import filesystem from "../../common/filesystem";
|
||||
@@ -31,17 +27,14 @@ import downloadAttachment from "../../common/filesystem/download-attachment";
|
||||
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
|
||||
import picker from "../../screens/editor/tiptap/picker";
|
||||
import {
|
||||
ToastManager,
|
||||
eSendEvent,
|
||||
presentSheet
|
||||
presentSheet,
|
||||
ToastEvent
|
||||
} from "../../services/event-manager";
|
||||
import PremiumService from "../../services/premium";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import {
|
||||
eCloseAttachmentDialog,
|
||||
eCloseSheet,
|
||||
eDBItemUpdate
|
||||
} from "../../utils/events";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { eCloseAttachmentDialog, eCloseSheet } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Dialog } from "../dialog";
|
||||
@@ -53,33 +46,28 @@ import { Notice } from "../ui/notice";
|
||||
import { PressableButton } from "../ui/pressable";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { formatBytes } from "@notesnook/common";
|
||||
|
||||
const Actions = ({
|
||||
attachment,
|
||||
fwdRef
|
||||
}: {
|
||||
attachment: Attachment;
|
||||
fwdRef: RefObject<ActionSheetRef>;
|
||||
}) => {
|
||||
const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const contextId = attachment.hash;
|
||||
const [filename, setFilename] = useState(attachment.filename);
|
||||
const contextId = attachment.metadata.hash;
|
||||
const [filename, setFilename] = useState(attachment.metadata.filename);
|
||||
const [currentProgress] = useAttachmentProgress(attachment);
|
||||
const [failed, setFailed] = useState<string | undefined>(attachment.failed);
|
||||
const [notes, setNotes] = useState<Note[]>([]);
|
||||
const [loading, setLoading] = useState<{
|
||||
name?: string;
|
||||
}>({});
|
||||
const [failed, setFailed] = useState(attachment.failed);
|
||||
const [notes, setNotes] = useState([]);
|
||||
const [loading, setLoading] = useState({
|
||||
name: null
|
||||
});
|
||||
|
||||
const actions = [
|
||||
{
|
||||
name: "Download",
|
||||
onPress: async () => {
|
||||
if (currentProgress) {
|
||||
await db.fs().cancel(attachment.hash, "download");
|
||||
useAttachmentStore.getState().remove(attachment.hash);
|
||||
await db.fs.cancel(attachment.metadata.hash);
|
||||
useAttachmentStore.getState().remove(attachment.metadata.hash);
|
||||
}
|
||||
downloadAttachment(attachment.hash, false);
|
||||
downloadAttachment(attachment.metadata.hash, false);
|
||||
eSendEvent(eCloseSheet, contextId);
|
||||
},
|
||||
icon: "download"
|
||||
@@ -88,7 +76,7 @@ const Actions = ({
|
||||
name: "Reupload",
|
||||
onPress: async () => {
|
||||
if (!PremiumService.get()) {
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Upgrade to pro",
|
||||
type: "error",
|
||||
context: "local"
|
||||
@@ -97,9 +85,9 @@ const Actions = ({
|
||||
}
|
||||
await picker.pick({
|
||||
reupload: true,
|
||||
hash: attachment.hash,
|
||||
hash: attachment.metadata.hash,
|
||||
context: contextId,
|
||||
type: attachment.type
|
||||
type: attachment.metadata.type
|
||||
});
|
||||
},
|
||||
icon: "upload"
|
||||
@@ -110,28 +98,28 @@ const Actions = ({
|
||||
setLoading({
|
||||
name: "Run file check"
|
||||
});
|
||||
let res = await filesystem.checkAttachment(attachment.hash);
|
||||
let res = await filesystem.checkAttachment(attachment.metadata.hash);
|
||||
if (res.failed) {
|
||||
db.attachments.markAsFailed(attachment.id, res.failed);
|
||||
setFailed(res.failed);
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "File check failed with error: " + res.failed,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
} else {
|
||||
setFailed(undefined);
|
||||
db.attachments.markAsFailed(attachment.id);
|
||||
eSendEvent(eDBItemUpdate, attachment.id);
|
||||
ToastManager.show({
|
||||
setFailed(null);
|
||||
db.attachments.markAsFailed(attachment.id, null);
|
||||
ToastEvent.show({
|
||||
heading: "File check passed",
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
}
|
||||
|
||||
setAttachments();
|
||||
setLoading({
|
||||
name: undefined
|
||||
name: null
|
||||
});
|
||||
},
|
||||
icon: "file-check"
|
||||
@@ -140,19 +128,19 @@ const Actions = ({
|
||||
name: "Rename",
|
||||
onPress: () => {
|
||||
presentDialog({
|
||||
context: contextId as any,
|
||||
context: contextId,
|
||||
input: true,
|
||||
title: "Rename file",
|
||||
paragraph: "Enter a new name for the file",
|
||||
defaultValue: attachment.filename,
|
||||
defaultValue: attachment.metadata.filename,
|
||||
positivePress: async (value) => {
|
||||
if (value && value.length > 0) {
|
||||
await db.attachments.add({
|
||||
hash: attachment.hash,
|
||||
hash: attachment.metadata.hash,
|
||||
filename: value
|
||||
});
|
||||
setFilename(value);
|
||||
eSendEvent(eDBItemUpdate, attachment.id);
|
||||
setAttachments();
|
||||
}
|
||||
},
|
||||
positiveText: "Rename"
|
||||
@@ -163,22 +151,34 @@ const Actions = ({
|
||||
{
|
||||
name: "Delete",
|
||||
onPress: async () => {
|
||||
await db.attachments.remove(attachment.hash, false);
|
||||
eSendEvent(eDBItemUpdate, attachment.id);
|
||||
eSendEvent(eCloseSheet, contextId);
|
||||
await db.attachments.remove(attachment.metadata.hash, false);
|
||||
setAttachments();
|
||||
close();
|
||||
},
|
||||
icon: "delete-outline"
|
||||
}
|
||||
];
|
||||
|
||||
const getNotes = useCallback(() => {
|
||||
let allNotes = db.notes.all;
|
||||
let attachmentNotes = attachment.noteIds?.map((id) => {
|
||||
let index = allNotes?.findIndex((note) => id === note.id);
|
||||
if (index !== -1) {
|
||||
return allNotes[index];
|
||||
} else {
|
||||
return {
|
||||
type: "notfound",
|
||||
title: `Note with id ${id} does not exist.`,
|
||||
id: id
|
||||
};
|
||||
}
|
||||
});
|
||||
return attachmentNotes;
|
||||
}, [attachment.noteIds]);
|
||||
|
||||
useEffect(() => {
|
||||
db.relations
|
||||
.to(attachment, "note")
|
||||
.selector.items()
|
||||
.then((items) => {
|
||||
setNotes(items);
|
||||
});
|
||||
}, [attachment]);
|
||||
setNotes(getNotes());
|
||||
}, [attachment, getNotes]);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
@@ -221,7 +221,7 @@ const Actions = ({
|
||||
}}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{attachment.type}
|
||||
{attachment.metadata.type}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
style={{
|
||||
@@ -230,10 +230,10 @@ const Actions = ({
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{formatBytes(attachment.size)}
|
||||
{formatBytes(attachment.length)}
|
||||
</Paragraph>
|
||||
|
||||
{notes.length ? (
|
||||
{attachment.noteIds ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
marginRight: 10
|
||||
@@ -241,14 +241,14 @@ const Actions = ({
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{notes.length} note
|
||||
{notes.length > 1 ? "s" : ""}
|
||||
{attachment.noteIds.length} note
|
||||
{attachment.noteIds.length > 1 ? "s" : ""}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
<Paragraph
|
||||
onPress={() => {
|
||||
Clipboard.setString(attachment.hash);
|
||||
ToastManager.show({
|
||||
Clipboard.setString(attachment.metadata.hash);
|
||||
ToastEvent.show({
|
||||
type: "success",
|
||||
heading: "Attachment hash copied",
|
||||
context: "local"
|
||||
@@ -257,7 +257,7 @@ const Actions = ({
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{attachment.hash}
|
||||
{attachment.metadata.hash}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
@@ -286,11 +286,21 @@ const Actions = ({
|
||||
{notes.map((item) => (
|
||||
<PressableButton
|
||||
onPress={async () => {
|
||||
if (item.type === "notfound") {
|
||||
ToastEvent.show({
|
||||
heading: "Note not found",
|
||||
message:
|
||||
"A note with the given id was not found. Maybe you have deleted the note or moved it to trash already.",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return;
|
||||
}
|
||||
eSendEvent(eCloseSheet, contextId);
|
||||
await sleep(150);
|
||||
eSendEvent(eCloseAttachmentDialog);
|
||||
await sleep(300);
|
||||
openNote(item, (item as any).type === "trash");
|
||||
openNote(item, item.type === "trash");
|
||||
}}
|
||||
customStyle={{
|
||||
paddingVertical: 12,
|
||||
@@ -311,16 +321,17 @@ const Actions = ({
|
||||
<Button
|
||||
key={item.name}
|
||||
buttonType={{
|
||||
text:
|
||||
item.name === "Delete" || item.name === "PermDelete"
|
||||
? colors.error.paragraph
|
||||
: colors.primary.paragraph
|
||||
text: item.on
|
||||
? colors.primary.accent
|
||||
: item.name === "Delete" || item.name === "PermDelete"
|
||||
? colors.error.paragraph
|
||||
: colors.primary.paragraph
|
||||
}}
|
||||
onPress={item.onPress}
|
||||
title={item.name}
|
||||
icon={item.icon}
|
||||
loading={loading?.name === item.name}
|
||||
type="gray"
|
||||
type={item.on ? "shade" : "gray"}
|
||||
fontSize={SIZE.sm}
|
||||
style={{
|
||||
borderRadius: 0,
|
||||
@@ -348,14 +359,17 @@ const Actions = ({
|
||||
);
|
||||
};
|
||||
|
||||
Actions.present = (
|
||||
attachment: Attachment,
|
||||
set: (attachments: VirtualizedGrouping<Attachment>) => void,
|
||||
context?: string
|
||||
) => {
|
||||
Actions.present = (attachment, set, context) => {
|
||||
presentSheet({
|
||||
context: context,
|
||||
component: (ref) => <Actions fwdRef={ref} attachment={attachment} />
|
||||
component: (ref, close) => (
|
||||
<Actions
|
||||
fwdRef={ref}
|
||||
setAttachments={set}
|
||||
close={close}
|
||||
attachment={attachment}
|
||||
/>
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { formatBytes } from "@notesnook/common";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React from "react";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../common/database";
|
||||
@@ -29,47 +29,32 @@ import { IconButton } from "../ui/icon-button";
|
||||
import { ProgressCircleComponent } from "../ui/svg/lazy";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import Actions from "./actions";
|
||||
import { Attachment, VirtualizedGrouping } from "@notesnook/core";
|
||||
import { useDBItem } from "../../hooks/use-db-item";
|
||||
|
||||
function getFileExtension(filename: string) {
|
||||
function getFileExtension(filename) {
|
||||
var ext = /^.+\.([^.]+)$/.exec(filename);
|
||||
return ext == null ? "" : ext[1];
|
||||
}
|
||||
|
||||
export const AttachmentItem = ({
|
||||
id,
|
||||
attachments,
|
||||
attachment,
|
||||
encryption,
|
||||
setAttachments,
|
||||
pressable = true,
|
||||
hideWhenNotDownloading,
|
||||
context
|
||||
}: {
|
||||
id: string;
|
||||
attachments?: VirtualizedGrouping<Attachment>;
|
||||
encryption?: boolean;
|
||||
setAttachments: (attachments: any) => void;
|
||||
pressable?: boolean;
|
||||
hideWhenNotDownloading?: boolean;
|
||||
context?: string;
|
||||
}) => {
|
||||
const [attachment] = useDBItem(id, "attachment", attachments?.item);
|
||||
|
||||
const { colors } = useThemeColors();
|
||||
const [currentProgress, setCurrentProgress] = useAttachmentProgress(
|
||||
attachment,
|
||||
encryption
|
||||
);
|
||||
|
||||
const onPress = () => {
|
||||
if (!pressable || !attachment) return;
|
||||
if (!pressable) return;
|
||||
Actions.present(attachment, setAttachments, context);
|
||||
};
|
||||
|
||||
return (hideWhenNotDownloading &&
|
||||
(!currentProgress || !(currentProgress as any).value)) ||
|
||||
!attachment ? null : (
|
||||
return hideWhenNotDownloading &&
|
||||
(!currentProgress || !currentProgress.value) ? null : (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={onPress}
|
||||
@@ -82,6 +67,7 @@ export const AttachmentItem = ({
|
||||
borderRadius: 5,
|
||||
backgroundColor: colors.secondary.background
|
||||
}}
|
||||
type="grayBg"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
@@ -107,7 +93,7 @@ export const AttachmentItem = ({
|
||||
position: "absolute"
|
||||
}}
|
||||
>
|
||||
{getFileExtension(attachment.filename).toUpperCase()}
|
||||
{getFileExtension(attachment.metadata.filename).toUpperCase()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
@@ -127,14 +113,14 @@ export const AttachmentItem = ({
|
||||
lineBreakMode="middle"
|
||||
color={colors.primary.paragraph}
|
||||
>
|
||||
{attachment.filename}
|
||||
{attachment.metadata.filename}
|
||||
</Paragraph>
|
||||
|
||||
{!hideWhenNotDownloading ? (
|
||||
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
|
||||
{formatBytes(attachment.size)}{" "}
|
||||
{(currentProgress as any)?.type
|
||||
? "(" + (currentProgress as any).type + "ing - tap to cancel)"
|
||||
{formatBytes(attachment.length)}{" "}
|
||||
{currentProgress?.type
|
||||
? "(" + currentProgress.type + "ing - tap to cancel)"
|
||||
: ""}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
@@ -146,8 +132,8 @@ export const AttachmentItem = ({
|
||||
activeOpacity={0.9}
|
||||
onPress={() => {
|
||||
if (encryption || !pressable) return;
|
||||
db.fs().cancel(attachment.hash, currentProgress?.type as any);
|
||||
setCurrentProgress(undefined);
|
||||
db.fs.cancel(attachment.metadata.hash);
|
||||
setCurrentProgress(null);
|
||||
}}
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
@@ -21,10 +21,7 @@ import React, { useRef, useState } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { downloadAttachments } from "../../common/filesystem/download-attachment";
|
||||
import {
|
||||
PresentSheetOptions,
|
||||
presentSheet
|
||||
} from "../../services/event-manager";
|
||||
import { presentSheet } from "../../services/event-manager";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
@@ -32,19 +29,8 @@ import { ProgressBarComponent } from "../ui/svg/lazy";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import { AttachmentItem } from "./attachment-item";
|
||||
import { Attachment, VirtualizedGrouping } from "@notesnook/core";
|
||||
|
||||
const DownloadAttachments = ({
|
||||
close,
|
||||
attachments,
|
||||
isNote,
|
||||
update
|
||||
}: {
|
||||
attachments: VirtualizedGrouping<Attachment>;
|
||||
close?: ((ctx?: string | undefined) => void) | undefined;
|
||||
isNote?: boolean;
|
||||
update?: (props: PresentSheetOptions) => void;
|
||||
}) => {
|
||||
const DownloadAttachments = ({ close, attachments, isNote, update }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const [progress, setProgress] = useState({
|
||||
@@ -53,40 +39,38 @@ const DownloadAttachments = ({
|
||||
});
|
||||
const [result, setResult] = useState(new Map());
|
||||
const canceled = useRef(false);
|
||||
const groupId = useRef<string>();
|
||||
const groupId = useRef();
|
||||
|
||||
const onDownload = async () => {
|
||||
update?.({
|
||||
update({
|
||||
disableClosing: true
|
||||
} as PresentSheetOptions);
|
||||
});
|
||||
setDownloading(true);
|
||||
canceled.current = false;
|
||||
groupId.current = Date.now().toString();
|
||||
const result = await downloadAttachments(
|
||||
attachments,
|
||||
(progress: number, statusText: string) =>
|
||||
setProgress({ value: progress, statusText }),
|
||||
(progress, statusText) => setProgress({ value: progress, statusText }),
|
||||
canceled,
|
||||
groupId.current
|
||||
);
|
||||
if (canceled.current) return;
|
||||
setResult(result || new Map());
|
||||
setDownloading(false);
|
||||
update?.({
|
||||
update({
|
||||
disableClosing: false
|
||||
} as PresentSheetOptions);
|
||||
});
|
||||
};
|
||||
|
||||
const cancel = async () => {
|
||||
update?.({
|
||||
update({
|
||||
disableClosing: false
|
||||
} as PresentSheetOptions);
|
||||
});
|
||||
canceled.current = true;
|
||||
if (!groupId.current) return;
|
||||
console.log(groupId.current, "canceling groupId downloads");
|
||||
await db.fs().cancel(groupId.current, "download");
|
||||
await db.fs.cancel(groupId.current);
|
||||
setDownloading(false);
|
||||
groupId.current = undefined;
|
||||
groupId.current = null;
|
||||
};
|
||||
|
||||
const successResults = () => {
|
||||
@@ -107,11 +91,11 @@ const DownloadAttachments = ({
|
||||
|
||||
function getResultText() {
|
||||
const downloadedAttachmentsCount =
|
||||
attachments?.ids?.length - failedResults().length;
|
||||
attachments.length - failedResults().length;
|
||||
if (downloadedAttachmentsCount === 0)
|
||||
return "Failed to download all attachments";
|
||||
return `Successfully downloaded ${downloadedAttachmentsCount}/${
|
||||
attachments?.ids.length
|
||||
attachments.length
|
||||
} attachments as a zip file at ${
|
||||
Platform.OS === "android" ? "the selected folder" : "Notesnook/downloads"
|
||||
}`;
|
||||
@@ -173,9 +157,7 @@ const DownloadAttachments = ({
|
||||
width={null}
|
||||
animated={true}
|
||||
useNativeDriver
|
||||
progress={
|
||||
progress.value ? progress.value / attachments.ids?.length : 0
|
||||
}
|
||||
progress={progress.value ? progress.value / attachments.length : 0}
|
||||
unfilledColor={colors.secondary.background}
|
||||
color={colors.primary.accent}
|
||||
borderWidth={0}
|
||||
@@ -192,7 +174,7 @@ const DownloadAttachments = ({
|
||||
borderRadius: 5,
|
||||
marginVertical: 12
|
||||
}}
|
||||
data={downloading ? attachments.ids : undefined}
|
||||
data={downloading ? attachments : []}
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
@@ -207,15 +189,14 @@ const DownloadAttachments = ({
|
||||
</Paragraph>
|
||||
</View>
|
||||
}
|
||||
keyExtractor={(item) => item as string}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={({ item }) => {
|
||||
return (
|
||||
<AttachmentItem
|
||||
id={item as string}
|
||||
attachment={item}
|
||||
setAttachments={() => {}}
|
||||
pressable={false}
|
||||
hideWhenNotDownloading={true}
|
||||
attachments={attachments}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
@@ -228,9 +209,7 @@ const DownloadAttachments = ({
|
||||
borderRadius: 100,
|
||||
marginTop: 20
|
||||
}}
|
||||
onPress={() => {
|
||||
close?.();
|
||||
}}
|
||||
onPress={close}
|
||||
type="accent"
|
||||
title="Done"
|
||||
/>
|
||||
@@ -248,9 +227,7 @@ const DownloadAttachments = ({
|
||||
borderRadius: 100,
|
||||
marginRight: 5
|
||||
}}
|
||||
onPress={() => {
|
||||
close?.();
|
||||
}}
|
||||
onPress={close}
|
||||
type="grayBg"
|
||||
title="No"
|
||||
/>
|
||||
@@ -281,11 +258,7 @@ const DownloadAttachments = ({
|
||||
);
|
||||
};
|
||||
|
||||
DownloadAttachments.present = (
|
||||
context: string,
|
||||
attachments: VirtualizedGrouping<Attachment>,
|
||||
isNote?: boolean
|
||||
) => {
|
||||
DownloadAttachments.present = (context, attachments, isNote) => {
|
||||
presentSheet({
|
||||
context: context,
|
||||
component: (ref, close, update) => (
|
||||
@@ -17,25 +17,16 @@ 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 {
|
||||
Attachment,
|
||||
Note,
|
||||
SortOptions,
|
||||
VirtualizedGrouping
|
||||
} from "@notesnook/core";
|
||||
import { FilteredSelector } from "@notesnook/core/dist/database/sql-collection";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { ActivityIndicator, ScrollView, View } from "react-native";
|
||||
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 filesystem from "../../common/filesystem";
|
||||
import { presentSheet } from "../../services/event-manager";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Input from "../ui/input";
|
||||
import Seperator from "../ui/seperator";
|
||||
@@ -43,77 +34,82 @@ import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { AttachmentItem } from "./attachment-item";
|
||||
import DownloadAttachments from "./download-attachments";
|
||||
import { Button } from "../ui/button";
|
||||
import {
|
||||
isAudio,
|
||||
isDocument,
|
||||
isImage,
|
||||
isVideo
|
||||
} from "@notesnook/core/dist/utils/filename";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
|
||||
|
||||
const DEFAULT_SORTING: SortOptions = {
|
||||
sortBy: "dateEdited",
|
||||
sortDirection: "desc"
|
||||
};
|
||||
|
||||
export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
export const AttachmentDialog = ({ note }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const { height } = useSettingStore((state) => state.dimensions);
|
||||
const [attachments, setAttachments] =
|
||||
useState<VirtualizedGrouping<Attachment>>();
|
||||
const attachmentSearchValue = useRef<string>();
|
||||
const searchTimer = useRef<NodeJS.Timeout>();
|
||||
const [attachments, setAttachments] = useState(
|
||||
note
|
||||
? db.attachments.ofNote(note?.id, "all")
|
||||
: [...(db.attachments.all || [])]
|
||||
);
|
||||
|
||||
const attachmentSearchValue = useRef();
|
||||
const searchTimer = useRef();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [currentFilter, setCurrentFilter] = useState("all");
|
||||
|
||||
const refresh = React.useCallback(() => {
|
||||
if (note) {
|
||||
db.attachments.ofNote(note.id, "all").sorted(DEFAULT_SORTING);
|
||||
} else {
|
||||
db.attachments.all
|
||||
.sorted(DEFAULT_SORTING)
|
||||
.then((attachments) => setAttachments(attachments));
|
||||
}
|
||||
}, [note]);
|
||||
const onChangeText = (text) => {
|
||||
const attachments = note?.id
|
||||
? db.attachments.ofNote(note?.id, "all")
|
||||
: [...(db.attachments.all || [])];
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [note, refresh]);
|
||||
|
||||
const onChangeText = (text: string) => {
|
||||
attachmentSearchValue.current = text;
|
||||
if (
|
||||
!attachmentSearchValue.current ||
|
||||
attachmentSearchValue.current === ""
|
||||
) {
|
||||
refresh();
|
||||
setAttachments(filterAttachments(currentFilter));
|
||||
}
|
||||
clearTimeout(searchTimer.current);
|
||||
searchTimer.current = setTimeout(async () => {
|
||||
let results = await db.lookup.attachments(
|
||||
attachmentSearchValue.current as string
|
||||
searchTimer.current = setTimeout(() => {
|
||||
let results = db.lookup.attachments(
|
||||
attachments,
|
||||
attachmentSearchValue.current
|
||||
);
|
||||
setAttachments(results);
|
||||
if (results.length === 0) return;
|
||||
|
||||
setAttachments(filterAttachments(currentFilter, results));
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const renderItem = ({ item }: { item: string }) => (
|
||||
const renderItem = ({ item }) => (
|
||||
<AttachmentItem
|
||||
setAttachments={setAttachments}
|
||||
attachments={attachments}
|
||||
id={item}
|
||||
setAttachments={() => {
|
||||
setAttachments(filterAttachments(currentFilter));
|
||||
}}
|
||||
attachment={item}
|
||||
context="attachments-list"
|
||||
/>
|
||||
);
|
||||
|
||||
const onCheck = async () => {
|
||||
if (!attachments) return;
|
||||
setLoading(true);
|
||||
for (let id of attachments.ids) {
|
||||
const attachment = await attachments.item(id as string);
|
||||
if (!attachment) continue;
|
||||
|
||||
let result = await filesystem.checkAttachment(attachment.hash);
|
||||
const checkedAttachments = [];
|
||||
for (let attachment of attachments) {
|
||||
let result = await filesystem.checkAttachment(attachment.metadata.hash);
|
||||
if (result.failed) {
|
||||
await db.attachments.markAsFailed(attachment.hash, result.failed);
|
||||
await db.attachments.markAsFailed(
|
||||
attachment.metadata.hash,
|
||||
result.failed
|
||||
);
|
||||
} else {
|
||||
await db.attachments.markAsFailed(id as string, undefined);
|
||||
await db.attachments.markAsFailed(attachment.id, null);
|
||||
}
|
||||
checkedAttachments.push(
|
||||
db.attachments.attachment(attachment.metadata.hash)
|
||||
);
|
||||
setAttachments([...checkedAttachments]);
|
||||
}
|
||||
refresh();
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
@@ -140,33 +136,33 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
}
|
||||
];
|
||||
|
||||
const filterAttachments = async (type: string) => {
|
||||
let items: FilteredSelector<Attachment> = db.attachments.all;
|
||||
const filterAttachments = (type, _attachments) => {
|
||||
const attachments = _attachments
|
||||
? _attachments
|
||||
: note
|
||||
? db.attachments.ofNote(note?.id, "all")
|
||||
: [...(db.attachments.all || [])];
|
||||
|
||||
switch (type) {
|
||||
case "all":
|
||||
items = db.attachments.all;
|
||||
break;
|
||||
return attachments;
|
||||
case "images":
|
||||
items = note
|
||||
? db.attachments.ofNote(note.id, "images")
|
||||
: db.attachments.images;
|
||||
break;
|
||||
return attachments.filter((attachment) =>
|
||||
isImage(attachment.metadata.type)
|
||||
);
|
||||
case "video":
|
||||
items = items = note
|
||||
? db.attachments.ofNote(note.id, "all")
|
||||
: db.attachments.videos;
|
||||
break;
|
||||
return attachments.filter((attachment) =>
|
||||
isVideo(attachment.metadata.type)
|
||||
);
|
||||
case "audio":
|
||||
items = db.attachments.all;
|
||||
break;
|
||||
return attachments.filter((attachment) =>
|
||||
isAudio(attachment.metadata.type)
|
||||
);
|
||||
case "documents":
|
||||
items = note
|
||||
? db.attachments.ofNote(note.id, "all")
|
||||
: db.attachments.documents;
|
||||
return attachments.filter((attachment) =>
|
||||
isDocument(attachment.metadata.type)
|
||||
);
|
||||
}
|
||||
|
||||
return await items.sorted(DEFAULT_SORTING);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -224,7 +220,6 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
onPress={() => {
|
||||
if (!attachments) return;
|
||||
DownloadAttachments.present(
|
||||
"attachments-list",
|
||||
attachments,
|
||||
@@ -241,7 +236,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
placeholder="Filter attachments by filename, type or hash"
|
||||
onChangeText={onChangeText}
|
||||
onSubmit={() => {
|
||||
onChangeText(attachmentSearchValue.current as string);
|
||||
onChangeText(attachmentSearchValue.current);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -263,7 +258,10 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
<Button
|
||||
type={currentFilter === item.filterBy ? "grayAccent" : "gray"}
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
title={
|
||||
item.title +
|
||||
` (${filterAttachments(item.filterBy)?.length || 0})`
|
||||
}
|
||||
style={{
|
||||
borderRadius: 0,
|
||||
borderBottomWidth: 1,
|
||||
@@ -273,9 +271,9 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
? "transparent"
|
||||
: colors.primary.accent
|
||||
}}
|
||||
onPress={async () => {
|
||||
onPress={() => {
|
||||
setCurrentFilter(item.filterBy);
|
||||
setAttachments(await filterAttachments(item.filterBy));
|
||||
setAttachments(filterAttachments(item.filterBy));
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
@@ -306,7 +304,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
/>
|
||||
}
|
||||
estimatedItemSize={50}
|
||||
data={attachments?.ids as string[]}
|
||||
data={attachments}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
|
||||
@@ -329,7 +327,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
);
|
||||
};
|
||||
|
||||
AttachmentDialog.present = (note?: Note) => {
|
||||
AttachmentDialog.present = (note) => {
|
||||
presentSheet({
|
||||
component: () => <AttachmentDialog note={note} />
|
||||
});
|
||||
@@ -17,14 +17,16 @@ 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 { Platform, View } from "react-native";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eCloseLoginDialog, eOpenLoginDialog } from "../../utils/events";
|
||||
import { sleep } from "../../utils/time";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
@@ -34,7 +36,6 @@ import { IconButton } from "../ui/icon-button";
|
||||
import { hideAuth, initialAuthMode } from "./common";
|
||||
import { Login } from "./login";
|
||||
import { Signup } from "./signup";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
|
||||
export const AuthMode = {
|
||||
login: 0,
|
||||
@@ -68,6 +69,9 @@ const AuthModal = () => {
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: false
|
||||
});
|
||||
actionSheetRef.current?.hide();
|
||||
setCurrentAuthMode(AuthMode.login);
|
||||
setVisible(false);
|
||||
@@ -77,6 +81,11 @@ const AuthModal = () => {
|
||||
<BaseDialog
|
||||
overlayOpacity={0}
|
||||
statusBarTranslucent={false}
|
||||
onShow={() => {
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: true
|
||||
});
|
||||
}}
|
||||
onRequestClose={currentAuthMode !== AuthMode.welcomeSignup && close}
|
||||
visible={true}
|
||||
onClose={close}
|
||||
@@ -86,6 +95,7 @@ const AuthModal = () => {
|
||||
transparent={false}
|
||||
animated={false}
|
||||
centered={false}
|
||||
enableSheetKeyboardHandler
|
||||
>
|
||||
<KeyboardAwareScrollView
|
||||
style={{
|
||||
|
||||
@@ -23,15 +23,18 @@ import { db } from "../../common/database";
|
||||
import {
|
||||
eSendEvent,
|
||||
presentSheet,
|
||||
ToastManager
|
||||
ToastEvent
|
||||
} from "../../services/event-manager";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eCloseSheet } from "../../utils/events";
|
||||
import { eCloseSheet, eOpenRecoveryKeyDialog } from "../../utils/events";
|
||||
import DialogHeader from "../dialog/dialog-header";
|
||||
import { Button } from "../ui/button";
|
||||
import Input from "../ui/input";
|
||||
import { Notice } from "../ui/notice";
|
||||
import Seperator from "../ui/seperator";
|
||||
import { Dialog } from "../dialog";
|
||||
import BackupService from "../../services/backup";
|
||||
import { sleep } from "../../utils/time";
|
||||
|
||||
export const ChangePassword = () => {
|
||||
const passwordInputRef = useRef();
|
||||
@@ -45,7 +48,7 @@ export const ChangePassword = () => {
|
||||
|
||||
const changePassword = async () => {
|
||||
if (!user?.isEmailConfirmed) {
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Email not confirmed",
|
||||
message: "Please confirm your email to change account password",
|
||||
type: "error",
|
||||
@@ -54,7 +57,7 @@ export const ChangePassword = () => {
|
||||
return;
|
||||
}
|
||||
if (error || !oldPassword.current || !password.current) {
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "All fields required",
|
||||
message: "Fill all the fields and try again.",
|
||||
type: "error",
|
||||
@@ -64,18 +67,23 @@ export const ChangePassword = () => {
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await BackupService.run(false, "change-password-dialog");
|
||||
if (!result) throw new Error("Failed to create backup");
|
||||
|
||||
await db.user.clearSessions();
|
||||
await db.user.changePassword(oldPassword.current, password.current);
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Account password updated",
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
setLoading(false);
|
||||
eSendEvent(eCloseSheet);
|
||||
await sleep(300);
|
||||
eSendEvent(eOpenRecoveryKeyDialog);
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Failed to change password",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
@@ -92,6 +100,7 @@ export const ChangePassword = () => {
|
||||
padding: 12
|
||||
}}
|
||||
>
|
||||
<Dialog context="change-password-dialog" />
|
||||
<DialogHeader
|
||||
title="Change password"
|
||||
paragraph="Enter your old and new passwords"
|
||||
@@ -129,7 +138,14 @@ export const ChangePassword = () => {
|
||||
/>
|
||||
|
||||
<Notice
|
||||
text="Changing password is a non-undoable process. You will be logged out from all your devices. Please make sure you do not close the app while your password is changing and have good internet connection."
|
||||
text={`Changing password is an irreversible process. You will be logged out from all your devices. Please make sure you do not close the app while your password is changing and have good internet connection.`}
|
||||
type="alert"
|
||||
/>
|
||||
|
||||
<View style={{ height: 10 }} />
|
||||
|
||||
<Notice
|
||||
text={`Once your password is changed, please make sure to save the new account recovery key.`}
|
||||
type="alert"
|
||||
/>
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import { View } from "react-native";
|
||||
import ActionSheet from "react-native-actions-sheet";
|
||||
import { db } from "../../common/database";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { ToastEvent } from "../../services/event-manager";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import DialogHeader from "../dialog/dialog-header";
|
||||
@@ -43,7 +43,7 @@ export const ForgotPassword = () => {
|
||||
|
||||
const sendRecoveryEmail = async () => {
|
||||
if (!email.current || error) {
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Account email is required.",
|
||||
type: "error",
|
||||
context: "local"
|
||||
@@ -63,7 +63,7 @@ export const ForgotPassword = () => {
|
||||
SettingsService.set({
|
||||
lastRecoveryEmailTime: Date.now()
|
||||
});
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Check your email to reset password",
|
||||
message: `Recovery email has been sent to ${email.current.toLowerCase()}`,
|
||||
type: "success",
|
||||
@@ -74,7 +74,7 @@ export const ForgotPassword = () => {
|
||||
setSent(true);
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Recovery email not sent",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
|
||||
@@ -19,12 +19,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Modal, View } from "react-native";
|
||||
import { View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { MMKV } from "../../common/database/mmkv";
|
||||
import BiometricService from "../../services/biometrics";
|
||||
import {
|
||||
ToastManager,
|
||||
ToastEvent,
|
||||
eSendEvent,
|
||||
eSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
@@ -37,6 +37,7 @@ import { eLoginSessionExpired, eUserLoggedIn } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Dialog } from "../dialog";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { Toast } from "../toast";
|
||||
@@ -46,7 +47,6 @@ import Input from "../ui/input";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { LoginSteps, useLogin } from "./use-login";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
|
||||
function getObfuscatedEmail(email) {
|
||||
if (!email) return "";
|
||||
@@ -69,6 +69,9 @@ export const SessionExpired = () => {
|
||||
eSendEvent(eUserLoggedIn, true);
|
||||
setVisible(false);
|
||||
setFocused(false);
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: false
|
||||
});
|
||||
},
|
||||
true
|
||||
);
|
||||
@@ -84,8 +87,11 @@ export const SessionExpired = () => {
|
||||
MMKV.clearStore();
|
||||
clearAllStores();
|
||||
setVisible(false);
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: false
|
||||
});
|
||||
} catch (e) {
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: e.message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
@@ -102,9 +108,9 @@ export const SessionExpired = () => {
|
||||
|
||||
const open = React.useCallback(async () => {
|
||||
try {
|
||||
let res = await db.tokenManager.getToken();
|
||||
let res = await db.user.tokenManager.getToken();
|
||||
if (!res) throw new Error("no token found");
|
||||
if (db.tokenManager._isTokenExpired(res))
|
||||
if (db.user.tokenManager._isTokenExpired(res))
|
||||
throw new Error("token expired");
|
||||
Sync.run("global", false, true, async (complete) => {
|
||||
if (!complete) {
|
||||
@@ -127,6 +133,9 @@ export const SessionExpired = () => {
|
||||
email.current = user.email;
|
||||
setFocused(false);
|
||||
setVisible(true);
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: true
|
||||
});
|
||||
}
|
||||
}, [email]);
|
||||
|
||||
@@ -142,6 +151,9 @@ export const SessionExpired = () => {
|
||||
await sleep(300);
|
||||
passwordInputRef.current?.focus();
|
||||
setFocused(true);
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: true
|
||||
});
|
||||
}}
|
||||
enableSheetKeyboardHandler={true}
|
||||
visible={true}
|
||||
|
||||
@@ -21,7 +21,7 @@ import React, { useRef, useState } from "react";
|
||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { ToastEvent } from "../../services/event-manager";
|
||||
import { clearMessage, setEmailVerifyMessage } from "../../services/message";
|
||||
import PremiumService from "../../services/premium";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
@@ -52,7 +52,7 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
const { width, height } = useWindowDimensions();
|
||||
const validateInfo = () => {
|
||||
if (!password.current || !email.current || !confirmPassword.current) {
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "All fields required",
|
||||
message: "Fill all the fields and try again",
|
||||
type: "error",
|
||||
@@ -85,7 +85,7 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
}
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Signup failed",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
|
||||
@@ -24,7 +24,7 @@ import useTimer from "../../hooks/use-timer";
|
||||
import {
|
||||
eSendEvent,
|
||||
presentSheet,
|
||||
ToastManager
|
||||
ToastEvent
|
||||
} from "../../services/event-manager";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { eCloseSheet } from "../../utils/events";
|
||||
@@ -138,7 +138,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
setSending(false);
|
||||
} catch (e) {
|
||||
setSending(false);
|
||||
ToastManager.error(e, "Error sending 2FA Code", "local");
|
||||
ToastEvent.error(e, "Error sending 2FA Code", "local");
|
||||
}
|
||||
}, [currentMethod.method, mfaInfo.token, seconds, sending, start]);
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import { db } from "../../common/database";
|
||||
import { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||
import { eSendEvent, ToastEvent } from "../../services/event-manager";
|
||||
import { clearMessage } from "../../services/message";
|
||||
import PremiumService from "../../services/premium";
|
||||
import SettingsService from "../../services/settings";
|
||||
@@ -48,7 +48,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
|
||||
(!password.current && step === LoginSteps.passwordAuth) ||
|
||||
(!email.current && step === LoginSteps.emailAuth)
|
||||
) {
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "All fields required",
|
||||
message: "Fill all the fields and try again",
|
||||
type: "error",
|
||||
@@ -121,7 +121,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
|
||||
const finishWithError = async (e) => {
|
||||
if (e.message === "invalid_grant") setStep(LoginSteps.emailAuth);
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Login failed",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
@@ -135,7 +135,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
|
||||
PremiumService.setPremiumStatus();
|
||||
setUser(user);
|
||||
clearMessage();
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Login successful",
|
||||
message: `Logged in as ${user.email}`,
|
||||
type: "success",
|
||||
|
||||
40
apps/mobile/app/components/container/container-header.tsx
Normal file
40
apps/mobile/app/components/container/container-header.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
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, { PropsWithChildren } from "react";
|
||||
import { View } from "react-native";
|
||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
|
||||
export const ContainerHeader = (props: PropsWithChildren) => {
|
||||
const { colors } = useThemeColors();
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
|
||||
return !selectionMode ? (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.primary.background,
|
||||
width: "100%",
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</View>
|
||||
) : null;
|
||||
};
|
||||
@@ -36,11 +36,10 @@ import { SIZE, normalize } from "../../utils/size";
|
||||
import NativeTooltip from "../../utils/tooltip";
|
||||
import { PressableButton } from "../ui/pressable";
|
||||
|
||||
interface FloatingButtonProps {
|
||||
title: string;
|
||||
interface FloatingButton {
|
||||
title?: string;
|
||||
onPress: () => void;
|
||||
color?: string;
|
||||
shouldShow?: boolean;
|
||||
alwaysVisible?: boolean;
|
||||
}
|
||||
|
||||
@@ -49,7 +48,7 @@ const FloatingButton = ({
|
||||
onPress,
|
||||
color,
|
||||
alwaysVisible = false
|
||||
}: FloatingButtonProps) => {
|
||||
}: FloatingButton) => {
|
||||
const { colors } = useThemeColors();
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
@@ -119,7 +118,7 @@ const FloatingButton = ({
|
||||
<PressableButton
|
||||
testID={notesnook.buttons.add}
|
||||
type="accent"
|
||||
accentColor={color}
|
||||
accentColor={colors.static[color as keyof typeof colors.static]}
|
||||
customStyle={{
|
||||
...getElevationStyle(5),
|
||||
borderRadius: 100
|
||||
|
||||
@@ -21,9 +21,15 @@ import React, { PropsWithChildren } from "react";
|
||||
import { KeyboardAvoidingView, Platform } from "react-native";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import useIsFloatingKeyboard from "../../hooks/use-is-floating-keyboard";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { Header } from "../header";
|
||||
import SelectionHeader from "../selection-header";
|
||||
|
||||
export const Container = ({ children }: PropsWithChildren) => {
|
||||
const floating = useIsFloatingKeyboard();
|
||||
const introCompleted = useSettingStore(
|
||||
(state) => state.settings.introCompleted
|
||||
);
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
|
||||
return (
|
||||
@@ -37,6 +43,13 @@ export const Container = ({ children }: PropsWithChildren) => {
|
||||
paddingBottom: Platform.OS === "android" ? 0 : insets.bottom
|
||||
}}
|
||||
>
|
||||
{!introCompleted ? null : (
|
||||
<>
|
||||
<SelectionHeader />
|
||||
<Header />
|
||||
</>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
|
||||
@@ -17,16 +17,15 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { useNoteStore } from "../../stores/use-notes-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { AnnouncementDialog } from "../announcements";
|
||||
import AuthModal from "../auth/auth-modal";
|
||||
import { SessionExpired } from "../auth/session-expired";
|
||||
import { Dialog } from "../dialog";
|
||||
import JumpToSectionDialog from "../dialogs/jump-to-section";
|
||||
import { AddTopicDialog } from "../dialogs/add-topic";
|
||||
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";
|
||||
@@ -37,6 +36,7 @@ import SheetProvider from "../sheet-provider";
|
||||
import RateAppSheet from "../sheets/rate-app";
|
||||
import RecoveryKeySheet from "../sheets/recovery-key";
|
||||
import RestoreDataSheet from "../sheets/restore-data";
|
||||
import PDFPreview from "../dialogs/pdf-preview";
|
||||
|
||||
const DialogProvider = () => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -46,6 +46,7 @@ const DialogProvider = () => {
|
||||
<>
|
||||
<LoadingDialog />
|
||||
<Dialog context="global" />
|
||||
<AddTopicDialog colors={colors} />
|
||||
<PremiumDialog colors={colors} />
|
||||
<AuthModal colors={colors} />
|
||||
<MergeConflicts />
|
||||
@@ -61,7 +62,6 @@ const DialogProvider = () => {
|
||||
<AnnouncementDialog />
|
||||
<SessionExpired />
|
||||
<PDFPreview />
|
||||
<JumpToSectionDialog />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,9 +21,11 @@ import { eSendEvent } from "../../services/event-manager";
|
||||
import {
|
||||
eCloseActionSheet,
|
||||
eCloseAddNotebookDialog,
|
||||
eCloseAddTopicDialog,
|
||||
eCloseMoveNoteDialog,
|
||||
eOpenActionSheet,
|
||||
eOpenAddNotebookDialog,
|
||||
eOpenAddTopicDialog,
|
||||
eOpenMoveNoteDialog
|
||||
} from "../../utils/events";
|
||||
|
||||
@@ -50,3 +52,9 @@ export const AddNotebookEvent = (notebook) => {
|
||||
export const HideAddNotebookEvent = (notebook) => {
|
||||
eSendEvent(eCloseAddNotebookDialog, notebook);
|
||||
};
|
||||
export const AddTopicEvent = (topic) => {
|
||||
eSendEvent(eOpenAddTopicDialog, topic);
|
||||
};
|
||||
export const HideAddTopicEvent = (notebook) => {
|
||||
eSendEvent(eCloseAddTopicDialog, notebook);
|
||||
};
|
||||
|
||||
@@ -17,9 +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 React, { PropsWithChildren, useEffect, useRef, useState } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
ColorValue,
|
||||
KeyboardAvoidingView,
|
||||
Modal,
|
||||
Platform,
|
||||
@@ -36,25 +35,6 @@ import SettingsService from "../../services/settings";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { useAppState } from "../../hooks/use-app-state";
|
||||
|
||||
export interface BaseDialogProps extends PropsWithChildren {
|
||||
animation?: "fade" | "none" | "slide" | undefined;
|
||||
visible: boolean;
|
||||
onRequestClose?: () => void;
|
||||
onShow?: () => void;
|
||||
premium?: boolean;
|
||||
statusBarTranslucent?: boolean;
|
||||
transparent?: boolean;
|
||||
centered?: boolean;
|
||||
bottom?: boolean;
|
||||
background?: string | ColorValue;
|
||||
animated?: boolean;
|
||||
bounce?: boolean;
|
||||
closeOnTouch?: boolean;
|
||||
useSafeArea?: boolean;
|
||||
avoidKeyboardResize?: boolean;
|
||||
enableSheetKeyboardHandler?: boolean;
|
||||
}
|
||||
|
||||
const BaseDialog = ({
|
||||
visible,
|
||||
onRequestClose,
|
||||
@@ -66,14 +46,14 @@ const BaseDialog = ({
|
||||
transparent,
|
||||
centered = true,
|
||||
bottom = false,
|
||||
background = null,
|
||||
animated = true,
|
||||
bounce = true,
|
||||
closeOnTouch = true,
|
||||
useSafeArea = true,
|
||||
avoidKeyboardResize = false,
|
||||
enableSheetKeyboardHandler,
|
||||
background
|
||||
}: BaseDialogProps) => {
|
||||
enableSheetKeyboardHandler = false
|
||||
}) => {
|
||||
const floating = useIsFloatingKeyboard();
|
||||
const appState = useAppState();
|
||||
const lockEvents = useRef(false);
|
||||
@@ -86,6 +66,7 @@ const BaseDialog = ({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (useUserStore.getState().disableAppLockRequests) return;
|
||||
if (SettingsService.get().appLockMode === "background") {
|
||||
if (appState === "background") {
|
||||
setIntervalVisible(false);
|
||||
@@ -167,7 +148,7 @@ const BaseDialog = ({
|
||||
]}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={closeOnTouch ? onRequestClose : undefined}
|
||||
onPress={closeOnTouch ? onRequestClose : null}
|
||||
style={styles.overlayButton}
|
||||
/>
|
||||
{premium}
|
||||
@@ -25,7 +25,7 @@ type DialogInfo = {
|
||||
paragraph?: string;
|
||||
positiveText?: string;
|
||||
negativeText?: string;
|
||||
positivePress?: (value: any) => void;
|
||||
positivePress?: (value: unknown) => void;
|
||||
onClose?: () => void;
|
||||
positiveType?:
|
||||
| "transparent"
|
||||
|
||||
@@ -40,7 +40,10 @@ import { Button } from "../ui/button";
|
||||
export const Dialog = ({ context = "global" }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [inputValue, setInputValue] = useState(null);
|
||||
const [checked, setChecked] = useState(false);
|
||||
const values = useRef({
|
||||
inputValue: undefined
|
||||
});
|
||||
const inputRef = useRef();
|
||||
const [dialogInfo, setDialogInfo] = useState({
|
||||
title: "",
|
||||
@@ -76,7 +79,8 @@ export const Dialog = ({ context = "global" }) => {
|
||||
if (dialogInfo.positivePress) {
|
||||
inputRef.current?.blur();
|
||||
let result = await dialogInfo.positivePress(
|
||||
inputValue || dialogInfo.defaultValue
|
||||
values.current.inputValue || dialogInfo.defaultValue,
|
||||
checked
|
||||
);
|
||||
if (result === false) {
|
||||
return;
|
||||
@@ -91,14 +95,16 @@ export const Dialog = ({ context = "global" }) => {
|
||||
if (!data.context) data.context = "global";
|
||||
if (data.context !== context) return;
|
||||
setDialogInfo(data);
|
||||
setChecked(false);
|
||||
values.current.inputValue = data.defaultValue;
|
||||
setVisible(true);
|
||||
setInputValue(data.defaultValue);
|
||||
},
|
||||
[context]
|
||||
);
|
||||
|
||||
const hide = () => {
|
||||
setInputValue(null);
|
||||
setChecked(false);
|
||||
values.current.inputValue = undefined;
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
@@ -106,7 +112,6 @@ export const Dialog = ({ context = "global" }) => {
|
||||
if (dialogInfo.onClose) {
|
||||
await dialogInfo.onClose();
|
||||
}
|
||||
|
||||
hide();
|
||||
};
|
||||
|
||||
@@ -159,7 +164,7 @@ export const Dialog = ({ context = "global" }) => {
|
||||
fwdRef={inputRef}
|
||||
autoCapitalize="none"
|
||||
onChangeText={(value) => {
|
||||
setInputValue(value);
|
||||
values.current.inputValue = value;
|
||||
}}
|
||||
testID="input-value"
|
||||
secureTextEntry={dialogInfo.secureTextEntry}
|
||||
@@ -176,10 +181,10 @@ export const Dialog = ({ context = "global" }) => {
|
||||
<>
|
||||
<Button
|
||||
onPress={() => {
|
||||
setInputValue(!inputValue);
|
||||
setChecked(!checked);
|
||||
}}
|
||||
icon={
|
||||
inputValue
|
||||
checked
|
||||
? "check-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
@@ -187,9 +192,10 @@ export const Dialog = ({ context = "global" }) => {
|
||||
justifyContent: "flex-start"
|
||||
}}
|
||||
height={35}
|
||||
iconSize={20}
|
||||
width="100%"
|
||||
title={dialogInfo.check.info}
|
||||
type={inputValue ? dialogInfo.check.type : "gray"}
|
||||
type={checked ? dialogInfo.check.type : "gray"}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
190
apps/mobile/app/components/dialogs/add-topic/index.js
Normal file
190
apps/mobile/app/components/dialogs/add-topic/index.js
Normal file
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
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, { createRef } from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
import { useMenuStore } from "../../../stores/use-menu-store";
|
||||
import {
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent,
|
||||
ToastEvent
|
||||
} from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { db } from "../../../common/database";
|
||||
import {
|
||||
eCloseAddTopicDialog,
|
||||
eOnTopicSheetUpdate,
|
||||
eOpenAddTopicDialog
|
||||
} from "../../../utils/events";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import BaseDialog from "../../dialog/base-dialog";
|
||||
import DialogButtons from "../../dialog/dialog-buttons";
|
||||
import DialogContainer from "../../dialog/dialog-container";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import Input from "../../ui/input";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import { Toast } from "../../toast";
|
||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
|
||||
export class AddTopicDialog extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
visible: false,
|
||||
titleFocused: false,
|
||||
loading: false
|
||||
};
|
||||
|
||||
this.ref = createRef();
|
||||
this.title;
|
||||
this.titleRef = createRef();
|
||||
this.notebook = null;
|
||||
this.toEdit = null;
|
||||
}
|
||||
|
||||
addNewTopic = async () => {
|
||||
try {
|
||||
if (!this.title || this.title?.trim() === "") {
|
||||
ToastEvent.show({
|
||||
heading: "Topic title is required",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.toEdit) {
|
||||
await db.notebooks.notebook(this.notebook.id).topics.add(this.title);
|
||||
} else {
|
||||
let topic = this.toEdit;
|
||||
topic.title = this.title;
|
||||
|
||||
await db.notebooks.notebook(topic.notebookId).topics.add(topic);
|
||||
}
|
||||
this.close();
|
||||
setTimeout(() => {
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useMenuStore.getState().setMenuPins();
|
||||
});
|
||||
eSendEvent(eOnTopicSheetUpdate);
|
||||
useRelationStore.getState().update();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
eSubscribeEvent(eOpenAddTopicDialog, this.open);
|
||||
eSubscribeEvent(eCloseAddTopicDialog, this.close);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
eUnSubscribeEvent(eOpenAddTopicDialog, this.open);
|
||||
eUnSubscribeEvent(eCloseAddTopicDialog, this.close);
|
||||
}
|
||||
|
||||
open = async ({ notebookId, toEdit }) => {
|
||||
let id = notebookId;
|
||||
if (id) {
|
||||
this.notebook = await db.notebooks.notebook(id).data;
|
||||
}
|
||||
this.toEdit = toEdit;
|
||||
|
||||
if (this.toEdit) {
|
||||
this.title = this.toEdit.title;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
visible: true
|
||||
});
|
||||
};
|
||||
close = () => {
|
||||
this.title = null;
|
||||
this.notebook = null;
|
||||
this.toEdit = null;
|
||||
this.setState({
|
||||
visible: false
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { visible } = this.state;
|
||||
if (!visible) return null;
|
||||
return (
|
||||
<BaseDialog
|
||||
onShow={async () => {
|
||||
if (this.toEdit) {
|
||||
this.titleRef.current?.setNativeProps({
|
||||
text: this.toEdit.title
|
||||
});
|
||||
}
|
||||
|
||||
this.ref.current?.animateNextTransition();
|
||||
await sleep(300);
|
||||
this.titleRef.current?.focus();
|
||||
}}
|
||||
bounce={false}
|
||||
statusBarTranslucent={false}
|
||||
visible={true}
|
||||
onRequestClose={this.close}
|
||||
>
|
||||
<DialogContainer>
|
||||
<DialogHeader
|
||||
icon="book-outline"
|
||||
title={this.toEdit ? "Edit topic" : "New topic"}
|
||||
paragraph={
|
||||
this.toEdit
|
||||
? "Edit title of the topic"
|
||||
: "Add a new topic in " + this.notebook.title
|
||||
}
|
||||
padding={12}
|
||||
/>
|
||||
<Seperator half />
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
zIndex: 10
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
fwdRef={this.titleRef}
|
||||
testID="input-title"
|
||||
onChangeText={(value) => {
|
||||
this.title = value;
|
||||
}}
|
||||
blurOnSubmit={false}
|
||||
placeholder="Enter title"
|
||||
onSubmit={() => this.addNewTopic()}
|
||||
returnKeyLabel="Done"
|
||||
returnKeyType="done"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<DialogButtons
|
||||
positiveTitle={this.toEdit ? "Save" : "Add"}
|
||||
onPressNegative={() => this.close()}
|
||||
onPressPositive={() => this.addNewTopic()}
|
||||
/>
|
||||
</DialogContainer>
|
||||
<Toast context="local" />
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,22 +17,15 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { GroupHeader, Item, VirtualizedGrouping } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, {
|
||||
RefObject,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState
|
||||
} from "react";
|
||||
import { FlatList, ScrollView, View } from "react-native";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { ScrollView, View } from "react-native";
|
||||
import { DDS } from "../../../services/device-detection";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../../services/event-manager";
|
||||
import { useMessageStore } from "../../../stores/use-message-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { getElevationStyle } from "../../../utils/elevation";
|
||||
import {
|
||||
eCloseJumpToDialog,
|
||||
@@ -43,47 +36,27 @@ import { SIZE } from "../../../utils/size";
|
||||
import BaseDialog from "../../dialog/base-dialog";
|
||||
import { PressableButton } from "../../ui/pressable";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { useCallback } from "react";
|
||||
|
||||
const JumpToSectionDialog = () => {
|
||||
const scrollRef = useRef<RefObject<FlatList>>();
|
||||
const [data, setData] = useState<VirtualizedGrouping<Item>>();
|
||||
const offsets = [];
|
||||
let timeout = null;
|
||||
const JumpToSectionDialog = ({ scrollRef, data, type }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const notes = data;
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const offsets = useRef<number[]>([]);
|
||||
const timeout = useRef<NodeJS.Timeout>();
|
||||
|
||||
const onPress = (item: GroupHeader) => {
|
||||
const index = notes?.ids?.findIndex((i) => {
|
||||
if (typeof i === "object") {
|
||||
return i.title === item.title && i.type === "header";
|
||||
} else {
|
||||
false;
|
||||
}
|
||||
});
|
||||
scrollRef.current?.current?.scrollToIndex({
|
||||
index: index as number,
|
||||
const onPress = (item) => {
|
||||
let ind = notes.findIndex(
|
||||
(i) => i.title === item.title && i.type === "header"
|
||||
);
|
||||
scrollRef.current?.scrollToIndex({
|
||||
index: ind,
|
||||
animated: true
|
||||
});
|
||||
close();
|
||||
};
|
||||
|
||||
const open = useCallback(
|
||||
({
|
||||
data,
|
||||
ref
|
||||
}: {
|
||||
data: VirtualizedGrouping<Item>;
|
||||
ref: RefObject<FlatList>;
|
||||
}) => {
|
||||
setData(data);
|
||||
scrollRef.current = ref;
|
||||
setVisible(true);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eOpenJumpToDialog, open);
|
||||
eSubscribeEvent(eCloseJumpToDialog, close);
|
||||
@@ -96,52 +69,50 @@ const JumpToSectionDialog = () => {
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
const onScroll = (data: { x: number; y: number }) => {
|
||||
const y = data.y;
|
||||
const onScroll = (data) => {
|
||||
let y = data.y;
|
||||
if (timeout) {
|
||||
clearTimeout(timeout.current);
|
||||
timeout.current = undefined;
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
timeout.current = setTimeout(() => {
|
||||
setCurrentIndex(
|
||||
offsets.current?.findIndex(
|
||||
(o, i) => o <= y && offsets.current[i + 1] > y
|
||||
) || 0
|
||||
);
|
||||
timeout = setTimeout(() => {
|
||||
let index = offsets.findIndex((o, i) => o <= y && offsets[i + 1] > y);
|
||||
setCurrentIndex(index || 0);
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const open = useCallback(
|
||||
(_type) => {
|
||||
if (_type !== type) return;
|
||||
setVisible(true);
|
||||
},
|
||||
[type]
|
||||
);
|
||||
|
||||
const close = () => {
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const loadOffsets = useCallback(() => {
|
||||
notes?.ids
|
||||
.filter((i) => typeof i === "object" && i.type === "header")
|
||||
.map((item, index) => {
|
||||
if (typeof item === "string") return;
|
||||
|
||||
let offset = 35 * index;
|
||||
let ind = notes.ids.findIndex(
|
||||
(i) =>
|
||||
typeof i === "object" &&
|
||||
i.title === item.title &&
|
||||
i.type === "header"
|
||||
);
|
||||
const messageState = useMessageStore.getState().message;
|
||||
const msgOffset = messageState?.visible ? 60 : 10;
|
||||
|
||||
ind = ind + 1;
|
||||
ind = ind - (index + 1);
|
||||
offset = offset + ind * 100 + msgOffset;
|
||||
offsets.current.push(offset);
|
||||
});
|
||||
}, [notes]);
|
||||
|
||||
useEffect(() => {
|
||||
loadOffsets();
|
||||
}, [loadOffsets, notes]);
|
||||
|
||||
const loadOffsets = useCallback(() => {
|
||||
notes
|
||||
.filter((i) => i.type === "header")
|
||||
.map((item, index) => {
|
||||
let offset = 35 * index;
|
||||
let ind = notes.findIndex(
|
||||
(i) => i.title === item.title && i.type === "header"
|
||||
);
|
||||
let messageState = useMessageStore.getState().message;
|
||||
let msgOffset = messageState?.visible ? 60 : 10;
|
||||
ind = ind + 1;
|
||||
ind = ind - (index + 1);
|
||||
offset = offset + ind * 100 + msgOffset;
|
||||
offsets.push(offset);
|
||||
});
|
||||
}, [notes]);
|
||||
return !visible ? null : (
|
||||
<BaseDialog
|
||||
onShow={() => {
|
||||
@@ -178,13 +149,13 @@ const JumpToSectionDialog = () => {
|
||||
paddingBottom: 20
|
||||
}}
|
||||
>
|
||||
{notes?.ids
|
||||
.filter((i) => typeof i === "object" && i.type === "header")
|
||||
{notes
|
||||
.filter((i) => i.type === "header")
|
||||
.map((item, index) => {
|
||||
return typeof item === "object" && item.title ? (
|
||||
return item.title ? (
|
||||
<PressableButton
|
||||
key={item.title}
|
||||
onPress={() => onPress(item)}
|
||||
onPress={() => onPress(item, index)}
|
||||
type={currentIndex === index ? "selected" : "transparent"}
|
||||
customStyle={{
|
||||
minWidth: "20%",
|
||||
@@ -27,12 +27,14 @@ import { editorController } from "../../../screens/editor/tiptap/utils";
|
||||
import BiometricService from "../../../services/biometrics";
|
||||
import { DDS } from "../../../services/device-detection";
|
||||
import {
|
||||
ToastManager,
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
eUnSubscribeEvent,
|
||||
ToastEvent
|
||||
} from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import SearchService from "../../../services/search";
|
||||
import { convertNoteToText } from "../../../utils/note-to-text";
|
||||
import { getElevationStyle } from "../../../utils/elevation";
|
||||
import {
|
||||
eClearEditor,
|
||||
@@ -43,7 +45,6 @@ import {
|
||||
} from "../../../utils/events";
|
||||
import { deleteItems } from "../../../utils/functions";
|
||||
import { tabBarRef } 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";
|
||||
@@ -54,6 +55,10 @@ import Input from "../../ui/input";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
|
||||
let Keychain;
|
||||
const passInputRef = createRef();
|
||||
const confirmPassRef = createRef();
|
||||
const changePassInputRef = createRef();
|
||||
export class VaultDialog extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -83,11 +88,6 @@ export class VaultDialog extends Component {
|
||||
deleteVault: false,
|
||||
deleteAll: false
|
||||
};
|
||||
|
||||
this.passInputRef = createRef();
|
||||
this.confirmPassRef = createRef();
|
||||
this.changePassInputRef = createRef();
|
||||
|
||||
this.password = null;
|
||||
this.confirmPassword = null;
|
||||
this.newPassword = null;
|
||||
@@ -148,6 +148,9 @@ export class VaultDialog extends Component {
|
||||
* @param {import('../../../services/event-manager').vaultType} data
|
||||
*/
|
||||
open = async (data) => {
|
||||
if (!Keychain) {
|
||||
Keychain = require("react-native-keychain");
|
||||
}
|
||||
let biometry = await BiometricService.isBiometryAvailable();
|
||||
let available = false;
|
||||
let fingerprint = await BiometricService.hasInternetCredentials("nn_vault");
|
||||
@@ -196,7 +199,7 @@ export class VaultDialog extends Component {
|
||||
|
||||
close = () => {
|
||||
if (this.state.loading) {
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: this.state.title,
|
||||
message: "Please wait and do not close the app.",
|
||||
type: "success",
|
||||
@@ -209,6 +212,7 @@ export class VaultDialog extends Component {
|
||||
|
||||
this.password = null;
|
||||
this.confirmPassword = null;
|
||||
SearchService.updateAndSearch();
|
||||
this.setState({
|
||||
visible: false,
|
||||
note: {},
|
||||
@@ -232,7 +236,7 @@ export class VaultDialog extends Component {
|
||||
if (this.state.loading) return;
|
||||
|
||||
if (!this.password) {
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Password not entered",
|
||||
message: "Enter a password for the vault and try again.",
|
||||
type: "error",
|
||||
@@ -241,7 +245,7 @@ export class VaultDialog extends Component {
|
||||
return;
|
||||
}
|
||||
if (this.password && this.password.length < 3) {
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Password too short",
|
||||
message: "Password must be longer than 3 characters.",
|
||||
type: "error",
|
||||
@@ -253,7 +257,7 @@ export class VaultDialog extends Component {
|
||||
|
||||
if (!this.state.novault) {
|
||||
if (this.password !== this.confirmPassword) {
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Passwords do not match",
|
||||
type: "error",
|
||||
context: "local"
|
||||
@@ -279,7 +283,7 @@ export class VaultDialog extends Component {
|
||||
if (this.state.biometricUnlock) {
|
||||
this._enrollFingerprint(this.newPassword);
|
||||
}
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Vault password updated successfully",
|
||||
type: "success",
|
||||
context: "global"
|
||||
@@ -291,19 +295,17 @@ export class VaultDialog extends Component {
|
||||
loading: false
|
||||
});
|
||||
if (e.message === db.vault.ERRORS.wrongPassword) {
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Incorrect password",
|
||||
message: "Please enter the correct password and try again",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
} else {
|
||||
ToastManager.error(e);
|
||||
}
|
||||
});
|
||||
} else if (this.state.locked) {
|
||||
if (!this.password || this.password.trim() === 0) {
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Incorrect password",
|
||||
message: "Please enter the correct password and try again",
|
||||
type: "error",
|
||||
@@ -353,7 +355,7 @@ export class VaultDialog extends Component {
|
||||
});
|
||||
this.close();
|
||||
} else {
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Account password incorrect",
|
||||
message: "Please enter correct password for your account.",
|
||||
type: "error",
|
||||
@@ -380,7 +382,7 @@ export class VaultDialog extends Component {
|
||||
this.close();
|
||||
eSendEvent("vaultUpdated");
|
||||
} catch (e) {
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Vault password incorrect",
|
||||
message: "Please enter correct password to clear vault.",
|
||||
type: "error",
|
||||
@@ -394,7 +396,7 @@ export class VaultDialog extends Component {
|
||||
|
||||
async _lockNote() {
|
||||
if (!this.password || this.password.trim() === 0) {
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Incorrect password",
|
||||
type: "error",
|
||||
context: "local"
|
||||
@@ -406,7 +408,7 @@ export class VaultDialog extends Component {
|
||||
eSendEvent(eClearEditor);
|
||||
}
|
||||
this.close();
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
message: "Note locked successfully",
|
||||
type: "error",
|
||||
context: "local"
|
||||
@@ -419,7 +421,7 @@ export class VaultDialog extends Component {
|
||||
|
||||
async _unlockNote() {
|
||||
if (!this.password || this.password.trim() === 0) {
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Incorrect password",
|
||||
message: "Please enter the correct password and try again",
|
||||
type: "error",
|
||||
@@ -457,7 +459,7 @@ export class VaultDialog extends Component {
|
||||
async _deleteNote() {
|
||||
try {
|
||||
await db.vault.remove(this.state.note.id, this.password);
|
||||
await deleteItems([this.state.note.id], "note");
|
||||
await deleteItems(this.state.note);
|
||||
this.close();
|
||||
} catch (e) {
|
||||
this._takeErrorAction(e);
|
||||
@@ -477,7 +479,7 @@ export class VaultDialog extends Component {
|
||||
loading: false
|
||||
});
|
||||
eSendEvent("vaultUpdated");
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Biometric unlocking enabled!",
|
||||
message: "Now you can unlock notes in vault with biometrics.",
|
||||
type: "success",
|
||||
@@ -486,7 +488,7 @@ export class VaultDialog extends Component {
|
||||
this.close();
|
||||
} catch (e) {
|
||||
this.close();
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Incorrect password",
|
||||
message:
|
||||
"Please enter the correct vault password to enable biometrics.",
|
||||
@@ -515,7 +517,7 @@ export class VaultDialog extends Component {
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Note added to vault",
|
||||
type: "success",
|
||||
context: "global"
|
||||
@@ -523,7 +525,7 @@ export class VaultDialog extends Component {
|
||||
this.close();
|
||||
} else {
|
||||
eSendEvent("vaultUpdated");
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Vault created successfully",
|
||||
type: "success",
|
||||
context: "global"
|
||||
@@ -536,7 +538,7 @@ export class VaultDialog extends Component {
|
||||
db.vault
|
||||
.remove(this.state.note.id, this.password)
|
||||
.then(() => {
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Note permanently unlocked.",
|
||||
type: "success",
|
||||
context: "global"
|
||||
@@ -551,9 +553,7 @@ export class VaultDialog extends Component {
|
||||
_openInEditor(note) {
|
||||
this.close();
|
||||
InteractionManager.runAfterInteractions(async () => {
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: note
|
||||
});
|
||||
eSendEvent(eOnLoadNote, note);
|
||||
if (!DDS.isTab) {
|
||||
tabBarRef.current?.goToPage(1);
|
||||
}
|
||||
@@ -562,7 +562,7 @@ export class VaultDialog extends Component {
|
||||
|
||||
async _copyNote(note) {
|
||||
Clipboard.setString(await convertNoteToText(note));
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Note copied",
|
||||
type: "success",
|
||||
message: "Note has been copied to clipboard!",
|
||||
@@ -594,7 +594,7 @@ export class VaultDialog extends Component {
|
||||
visible: true
|
||||
});
|
||||
setTimeout(() => {
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Incorrect password",
|
||||
type: "error",
|
||||
context: "local"
|
||||
@@ -609,13 +609,13 @@ export class VaultDialog extends Component {
|
||||
try {
|
||||
await BiometricService.resetCredentials();
|
||||
eSendEvent("vaultUpdated");
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Biometric unlocking disabled!",
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
} catch (e) {
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Failed to disable Biometric unlocking.",
|
||||
message: e.message,
|
||||
type: "success",
|
||||
@@ -667,8 +667,7 @@ export class VaultDialog extends Component {
|
||||
<BaseDialog
|
||||
onShow={async () => {
|
||||
await sleep(100);
|
||||
|
||||
this.passInputRef.current?.focus();
|
||||
passInputRef.current?.focus();
|
||||
}}
|
||||
statusBarTranslucent={false}
|
||||
onRequestClose={this.close}
|
||||
@@ -703,7 +702,7 @@ export class VaultDialog extends Component {
|
||||
!this.state.revokeFingerprintAccess ? (
|
||||
<>
|
||||
<Input
|
||||
fwdRef={this.passInputRef}
|
||||
fwdRef={passInputRef}
|
||||
editable={!loading}
|
||||
autoCapitalize="none"
|
||||
testID={notesnook.ids.dialogs.vault.pwd}
|
||||
@@ -720,7 +719,7 @@ export class VaultDialog extends Component {
|
||||
}
|
||||
onSubmit={() => {
|
||||
changePassword
|
||||
? this.confirmPassRef.current?.focus()
|
||||
? changePassInputRef.current?.focus()
|
||||
: this.onPress;
|
||||
}}
|
||||
autoComplete="password"
|
||||
@@ -772,7 +771,7 @@ export class VaultDialog extends Component {
|
||||
<>
|
||||
<Seperator half />
|
||||
<Input
|
||||
ref={this.confirmPassRef}
|
||||
ref={changePassInputRef}
|
||||
editable={!loading}
|
||||
testID={notesnook.ids.dialogs.vault.changePwd}
|
||||
autoCapitalize="none"
|
||||
@@ -792,7 +791,7 @@ export class VaultDialog extends Component {
|
||||
{!novault ? (
|
||||
<View>
|
||||
<Input
|
||||
fwdRef={this.passInputRef}
|
||||
fwdRef={passInputRef}
|
||||
autoCapitalize="none"
|
||||
testID={notesnook.ids.dialogs.vault.pwd}
|
||||
onChangeText={(value) => {
|
||||
@@ -803,13 +802,13 @@ export class VaultDialog extends Component {
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
onSubmit={() => {
|
||||
this.confirmPassRef.current?.focus();
|
||||
confirmPassRef.current?.focus();
|
||||
}}
|
||||
placeholder="Password"
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={this.confirmPassRef}
|
||||
fwdRef={confirmPassRef}
|
||||
autoCapitalize="none"
|
||||
testID={notesnook.ids.dialogs.vault.pwdAlt}
|
||||
secureTextEntry
|
||||
|
||||
@@ -17,73 +17,42 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Platform, StyleSheet, View } from "react-native";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { SearchBar } from "../../screens/search/search-bar";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import useNavigationStore, {
|
||||
RouteName
|
||||
} from "../../stores/use-navigation-store";
|
||||
import useNavigationStore from "../../stores/use-navigation-store";
|
||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { eScrollEvent } from "../../utils/events";
|
||||
import { LeftMenus } from "./left-menus";
|
||||
import { RightMenus } from "./right-menus";
|
||||
import { Title } from "./title";
|
||||
|
||||
type HeaderRightButton = {
|
||||
title: string;
|
||||
onPress: () => void;
|
||||
};
|
||||
|
||||
export const Header = ({
|
||||
renderedInRoute,
|
||||
onLeftMenuButtonPress,
|
||||
title,
|
||||
titleHiddenOnRender,
|
||||
headerRightButtons,
|
||||
id,
|
||||
accentColor,
|
||||
isBeta,
|
||||
canGoBack,
|
||||
onPressDefaultRightButton,
|
||||
hasSearch,
|
||||
onSearch
|
||||
}: {
|
||||
onLeftMenuButtonPress?: () => void;
|
||||
renderedInRoute: RouteName;
|
||||
id?: string;
|
||||
title: string;
|
||||
headerRightButtons?: HeaderRightButton[];
|
||||
titleHiddenOnRender?: boolean;
|
||||
accentColor?: string;
|
||||
isBeta?: boolean;
|
||||
canGoBack?: boolean;
|
||||
onPressDefaultRightButton?: () => void;
|
||||
hasSearch?: boolean;
|
||||
onSearch?: () => void;
|
||||
}) => {
|
||||
const _Header = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const [borderHidden, setBorderHidden] = useState(true);
|
||||
const [hide, setHide] = useState(true);
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
const isFocused = useNavigationStore((state) => state.focusedRouteId === id);
|
||||
const currentScreen = useNavigationStore(
|
||||
(state) => state.currentScreen?.name
|
||||
);
|
||||
|
||||
const onScroll = useCallback(
|
||||
(data: { x: number; y: number; id?: string; route: string }) => {
|
||||
if (data.route !== renderedInRoute || data.id !== id) return;
|
||||
(data: { x: number; y: number }) => {
|
||||
if (data.y > 150) {
|
||||
if (!borderHidden) return;
|
||||
setBorderHidden(false);
|
||||
if (!hide) return;
|
||||
setHide(false);
|
||||
} else {
|
||||
if (borderHidden) return;
|
||||
setBorderHidden(true);
|
||||
if (hide) return;
|
||||
setHide(true);
|
||||
}
|
||||
},
|
||||
[borderHidden, id, renderedInRoute]
|
||||
[hide]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -91,9 +60,9 @@ export const Header = ({
|
||||
return () => {
|
||||
eUnSubscribeEvent(eScrollEvent, onScroll);
|
||||
};
|
||||
}, [borderHidden, onScroll]);
|
||||
}, [hide, onScroll]);
|
||||
|
||||
return selectionMode && isFocused ? null : (
|
||||
return selectionMode ? null : (
|
||||
<>
|
||||
<View
|
||||
style={[
|
||||
@@ -103,42 +72,29 @@ export const Header = ({
|
||||
backgroundColor: colors.primary.background,
|
||||
overflow: "hidden",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: borderHidden
|
||||
borderBottomColor: hide
|
||||
? "transparent"
|
||||
: colors.secondary.background,
|
||||
justifyContent: "space-between"
|
||||
}
|
||||
]}
|
||||
>
|
||||
<>
|
||||
<View style={styles.leftBtnContainer}>
|
||||
<LeftMenus
|
||||
canGoBack={canGoBack}
|
||||
onLeftButtonPress={onLeftMenuButtonPress}
|
||||
/>
|
||||
|
||||
<Title
|
||||
isHiddenOnRender={titleHiddenOnRender}
|
||||
renderedInRoute={renderedInRoute}
|
||||
id={id}
|
||||
accentColor={accentColor}
|
||||
title={title}
|
||||
isBeta={isBeta}
|
||||
/>
|
||||
</View>
|
||||
<RightMenus
|
||||
renderedInRoute={renderedInRoute}
|
||||
id={id}
|
||||
headerRightButtons={headerRightButtons}
|
||||
onPressDefaultRightButton={onPressDefaultRightButton}
|
||||
search={hasSearch}
|
||||
onSearch={onSearch}
|
||||
/>
|
||||
</>
|
||||
{currentScreen === "Search" ? (
|
||||
<SearchBar />
|
||||
) : (
|
||||
<>
|
||||
<View style={styles.leftBtnContainer}>
|
||||
<LeftMenus />
|
||||
<Title />
|
||||
</View>
|
||||
<RightMenus />
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export const Header = React.memo(_Header, () => true);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
|
||||
@@ -17,29 +17,23 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import Navigation from "../../services/navigation";
|
||||
import useNavigationStore from "../../stores/use-navigation-store";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { tabBarRef } from "../../utils/global-refs";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
|
||||
export const LeftMenus = ({
|
||||
canGoBack,
|
||||
onLeftButtonPress
|
||||
}: {
|
||||
canGoBack?: boolean;
|
||||
onLeftButtonPress?: () => void;
|
||||
}) => {
|
||||
export const LeftMenus = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const canGoBack = useNavigationStore((state) => state.canGoBack);
|
||||
const isTablet = deviceMode === "tablet";
|
||||
|
||||
const _onLeftButtonPress = () => {
|
||||
if (onLeftButtonPress) return onLeftButtonPress();
|
||||
|
||||
const onLeftButtonPress = () => {
|
||||
if (!canGoBack) {
|
||||
if (tabBarRef.current?.isDrawerOpen()) {
|
||||
Navigation.closeDrawer();
|
||||
@@ -60,17 +54,21 @@ export const LeftMenus = ({
|
||||
height: 40,
|
||||
width: 40,
|
||||
borderRadius: 100,
|
||||
marginLeft: -5,
|
||||
marginRight: DDS.isLargeTablet() ? 10 : 7
|
||||
}}
|
||||
left={40}
|
||||
top={40}
|
||||
right={DDS.isLargeTablet() ? 10 : 10}
|
||||
onPress={_onLeftButtonPress}
|
||||
onPress={onLeftButtonPress}
|
||||
onLongPress={() => {
|
||||
Navigation.popToTop();
|
||||
}}
|
||||
name={canGoBack ? "arrow-left" : "menu"}
|
||||
color={colors.primary.paragraph}
|
||||
iconStyle={{
|
||||
marginLeft: canGoBack ? -5 : 0
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,46 +20,43 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import React, { useRef } from "react";
|
||||
import { Platform, StyleSheet, View } from "react-native";
|
||||
//@ts-ignore
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import Menu from "react-native-reanimated-material-menu";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import Navigation from "../../services/navigation";
|
||||
import SearchService from "../../services/search";
|
||||
import {
|
||||
HeaderRightButton,
|
||||
RouteName
|
||||
} from "../../stores/use-navigation-store";
|
||||
import useNavigationStore from "../../stores/use-navigation-store";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
|
||||
export const RightMenus = ({
|
||||
headerRightButtons,
|
||||
renderedInRoute,
|
||||
id,
|
||||
onPressDefaultRightButton,
|
||||
search,
|
||||
onSearch
|
||||
}: {
|
||||
headerRightButtons?: HeaderRightButton[];
|
||||
renderedInRoute: RouteName;
|
||||
id?: string;
|
||||
onPressDefaultRightButton?: () => void;
|
||||
search?: boolean;
|
||||
onSearch?: () => void;
|
||||
}) => {
|
||||
export const RightMenus = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const { colors: contextMenuColors } = useThemeColors("contextMenu");
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const menuRef = useRef<Menu>(null);
|
||||
const buttons = useNavigationStore((state) => state.headerRightButtons);
|
||||
const currentScreen = useNavigationStore((state) => state.currentScreen.name);
|
||||
const buttonAction = useNavigationStore((state) => state.buttonAction);
|
||||
const menuRef = useRef<{
|
||||
show: () => void;
|
||||
hide: () => void;
|
||||
}>();
|
||||
|
||||
return (
|
||||
<View style={styles.rightBtnContainer}>
|
||||
{search ? (
|
||||
{!currentScreen.startsWith("Settings") ? (
|
||||
<IconButton
|
||||
onPress={onSearch}
|
||||
onPress={async () => {
|
||||
SearchService.prepareSearch();
|
||||
Navigation.navigate(
|
||||
{
|
||||
name: "Search"
|
||||
},
|
||||
{}
|
||||
);
|
||||
}}
|
||||
testID="icon-search"
|
||||
name="magnify"
|
||||
color={colors.primary.paragraph}
|
||||
@@ -69,9 +66,9 @@ export const RightMenus = ({
|
||||
|
||||
{deviceMode !== "mobile" ? (
|
||||
<Button
|
||||
onPress={onPressDefaultRightButton}
|
||||
onPress={buttonAction}
|
||||
testID={notesnook.ids.default.addBtn}
|
||||
icon={renderedInRoute === "Trash" ? "delete" : "plus"}
|
||||
icon={currentScreen === "Trash" ? "delete" : "plus"}
|
||||
iconSize={SIZE.xl}
|
||||
type="shade"
|
||||
hitSlop={{
|
||||
@@ -92,7 +89,7 @@ export const RightMenus = ({
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{headerRightButtons && headerRightButtons.length > 0 ? (
|
||||
{buttons && buttons.length > 0 ? (
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
animationDuration={200}
|
||||
@@ -114,7 +111,7 @@ export const RightMenus = ({
|
||||
/>
|
||||
}
|
||||
>
|
||||
{headerRightButtons.map((item) => (
|
||||
{buttons.map((item) => (
|
||||
<Button
|
||||
style={{
|
||||
width: 150,
|
||||
|
||||
@@ -20,74 +20,103 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import Notebook from "../../screens/notebook";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import useNavigationStore from "../../stores/use-navigation-store";
|
||||
import { eScrollEvent } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import Tag from "../ui/tag";
|
||||
import Heading from "../ui/typography/heading";
|
||||
|
||||
export const Title = ({
|
||||
title,
|
||||
isHiddenOnRender,
|
||||
accentColor,
|
||||
isBeta,
|
||||
renderedInRoute,
|
||||
id
|
||||
}: {
|
||||
title: string;
|
||||
isHiddenOnRender?: boolean;
|
||||
accentColor?: string;
|
||||
isBeta?: boolean;
|
||||
renderedInRoute: string;
|
||||
id?: string;
|
||||
}) => {
|
||||
const titleState: { [name: string]: boolean } = {};
|
||||
|
||||
export const Title = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const [visible, setVisible] = useState(isHiddenOnRender);
|
||||
const isTag = title.startsWith("#");
|
||||
const currentScreen = useNavigationStore((state) => state.currentScreen);
|
||||
const isNotebook = currentScreen.name === "Notebook";
|
||||
const isTopic = currentScreen?.name === "TopicNotes";
|
||||
const [hide, setHide] = useState(
|
||||
isNotebook
|
||||
? typeof titleState[currentScreen.id as string] === "boolean"
|
||||
? titleState[currentScreen.id as string]
|
||||
: true
|
||||
: false
|
||||
);
|
||||
const isHidden = titleState[currentScreen.id as string];
|
||||
const notebook =
|
||||
isTopic && currentScreen.notebookId
|
||||
? db.notebooks?.notebook(currentScreen.notebookId)?.data
|
||||
: null;
|
||||
const title = currentScreen.title;
|
||||
const isTag = currentScreen?.name === "TaggedNotes";
|
||||
|
||||
const onScroll = useCallback(
|
||||
(data: { x: number; y: number; id?: string; route: string }) => {
|
||||
if (data.route !== renderedInRoute || data.id !== id) return;
|
||||
(data: { x: number; y: number }) => {
|
||||
if (currentScreen.name !== "Notebook") {
|
||||
setHide(false);
|
||||
return;
|
||||
}
|
||||
if (data.y > 150) {
|
||||
if (!visible) return;
|
||||
setVisible(false);
|
||||
if (!hide) return;
|
||||
titleState[currentScreen.id as string] = false;
|
||||
setHide(false);
|
||||
} else {
|
||||
if (visible) return;
|
||||
setVisible(true);
|
||||
if (hide) return;
|
||||
titleState[currentScreen.id as string] = true;
|
||||
setHide(true);
|
||||
}
|
||||
},
|
||||
[id, renderedInRoute, visible]
|
||||
[currentScreen.id, currentScreen.name, hide]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentScreen.name === "Notebook") {
|
||||
const value =
|
||||
typeof titleState[currentScreen.id as string] === "boolean"
|
||||
? titleState[currentScreen.id as string]
|
||||
: true;
|
||||
setHide(value);
|
||||
} else {
|
||||
setHide(titleState[currentScreen.id as string]);
|
||||
}
|
||||
}, [currentScreen.id, currentScreen.name]);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eScrollEvent, onScroll);
|
||||
return () => {
|
||||
eUnSubscribeEvent(eScrollEvent, onScroll);
|
||||
};
|
||||
}, [visible, onScroll]);
|
||||
}, [hide, onScroll]);
|
||||
|
||||
function navigateToNotebook() {
|
||||
if (!isTopic) return;
|
||||
Notebook.navigate(notebook, true);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{!visible ? (
|
||||
{!hide && !isHidden ? (
|
||||
<Heading
|
||||
onPress={navigateToNotebook}
|
||||
numberOfLines={1}
|
||||
size={SIZE.lg}
|
||||
style={{
|
||||
flexWrap: "wrap",
|
||||
marginTop: Platform.OS === "ios" ? -1 : 0
|
||||
}}
|
||||
color={accentColor || colors.primary.heading}
|
||||
color={currentScreen.color || colors.primary.heading}
|
||||
>
|
||||
{isTag ? (
|
||||
<Heading size={SIZE.xl} color={colors.primary.accent}>
|
||||
#
|
||||
</Heading>
|
||||
) : null}
|
||||
{isTag ? title.slice(1) : title}{" "}
|
||||
{title}{" "}
|
||||
<Tag
|
||||
visible={isBeta}
|
||||
visible={currentScreen.beta}
|
||||
text="BETA"
|
||||
style={{
|
||||
backgroundColor: "transparent"
|
||||
|
||||
@@ -32,22 +32,23 @@ import {
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { ProgressBarComponent } from "../ui/svg/lazy";
|
||||
import type { ImageAttributes } from "@notesnook/editor/dist/extensions/image/index";
|
||||
|
||||
const ImagePreview = () => {
|
||||
const { colors } = useThemeColors("dialog");
|
||||
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [image, setImage] = useState<string>();
|
||||
const [image, setImage] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent("ImagePreview", open);
|
||||
|
||||
return () => {
|
||||
eUnSubscribeEvent("ImagePreview", open);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const open = async (image: ImageAttributes) => {
|
||||
const open = async (image) => {
|
||||
setVisible(true);
|
||||
setLoading(true);
|
||||
setTimeout(async () => {
|
||||
@@ -62,7 +63,6 @@ const ImagePreview = () => {
|
||||
});
|
||||
}
|
||||
if (!hash) return;
|
||||
//@ts-ignore // FIX ME
|
||||
const uri = await downloadAttachment(hash, false, {
|
||||
silent: true,
|
||||
cache: true
|
||||
@@ -74,7 +74,7 @@ const ImagePreview = () => {
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
setImage(undefined);
|
||||
setImage(null);
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
@@ -88,7 +88,7 @@ const ImagePreview = () => {
|
||||
backgroundColor: "black"
|
||||
}}
|
||||
>
|
||||
{loading || !image ? (
|
||||
{loading ? (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
@@ -77,9 +77,14 @@ export const WelcomeNotice = () => {
|
||||
SettingsService.set({
|
||||
introCompleted: true
|
||||
});
|
||||
Navigation.replace("Notes", {
|
||||
canGoBack: false
|
||||
});
|
||||
Navigation.replace(
|
||||
{
|
||||
name: "Notes"
|
||||
},
|
||||
{
|
||||
canGoBack: false
|
||||
}
|
||||
);
|
||||
}, 1000);
|
||||
}}
|
||||
style={{
|
||||
@@ -101,9 +106,14 @@ export const WelcomeNotice = () => {
|
||||
SettingsService.set({
|
||||
introCompleted: true
|
||||
});
|
||||
Navigation.replace("Notes", {
|
||||
canGoBack: false
|
||||
});
|
||||
Navigation.replace(
|
||||
{
|
||||
name: "Notes"
|
||||
},
|
||||
{
|
||||
canGoBack: false
|
||||
}
|
||||
);
|
||||
await sleep(1000);
|
||||
eSendEvent(eCloseLoading);
|
||||
}}
|
||||
|
||||
@@ -17,39 +17,57 @@ 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 from "react";
|
||||
import { View } from "react-native";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useMessageStore } from "../../../stores/use-message-store";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { ColorValues } from "../../../utils/colors";
|
||||
import { Announcement } from "../../announcements/announcement";
|
||||
import { Card } from "../../list/card";
|
||||
|
||||
export type ListHeaderProps = {
|
||||
noAnnouncement?: boolean;
|
||||
color?: string;
|
||||
messageCard?: boolean;
|
||||
screen?: string;
|
||||
shouldShow?: boolean;
|
||||
};
|
||||
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
export const Header = React.memo(
|
||||
({
|
||||
type,
|
||||
messageCard = true,
|
||||
color,
|
||||
shouldShow = false,
|
||||
noAnnouncement,
|
||||
screen
|
||||
}: ListHeaderProps) => {
|
||||
warning
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const announcements = useMessageStore((state) => state.announcements);
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
|
||||
return selectionMode ? null : (
|
||||
<>
|
||||
{announcements.length !== 0 && !noAnnouncement ? (
|
||||
{warning ? (
|
||||
<View
|
||||
style={{
|
||||
padding: 12,
|
||||
backgroundColor: colors.error.background,
|
||||
width: "95%",
|
||||
alignSelf: "center",
|
||||
borderRadius: 5,
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="sync-alert"
|
||||
size={SIZE.md}
|
||||
color={colors.error.icon}
|
||||
f
|
||||
/>
|
||||
<Paragraph style={{ marginLeft: 5 }} color={colors.error.icon}>
|
||||
{warning.title}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : announcements.length !== 0 && !noAnnouncement ? (
|
||||
<Announcement color={color || colors.primary.accent} />
|
||||
) : (screen as any) === "Search" ? null : !shouldShow ? (
|
||||
) : type === "search" ? null : !shouldShow ? (
|
||||
<View
|
||||
style={{
|
||||
marginBottom: 5,
|
||||
@@ -60,7 +78,11 @@ export const Header = React.memo(
|
||||
}}
|
||||
>
|
||||
{messageCard ? (
|
||||
<Card color={color || colors.primary.accent} />
|
||||
<Card
|
||||
color={
|
||||
ColorValues[color?.toLowerCase()] || colors.primary.accent
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
@@ -17,33 +17,27 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Notebook } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { db } from "../../../common/database";
|
||||
import { ToastManager } from "../../../services/event-manager";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useMenuStore } from "../../../stores/use-menu-store";
|
||||
import { ToastEvent } from "../../../services/event-manager";
|
||||
import { getTotalNotes } from "@notesnook/common";
|
||||
import { db } from "../../../common/database";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { getFormattedDate } from "@notesnook/common";
|
||||
|
||||
export const NotebookHeader = ({
|
||||
notebook,
|
||||
onEditNotebook,
|
||||
totalNotes = 0
|
||||
}: {
|
||||
notebook: Notebook;
|
||||
onEditNotebook: () => void;
|
||||
totalNotes: number;
|
||||
}) => {
|
||||
export const NotebookHeader = ({ notebook, onEditNotebook }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [isPinnedToMenu, setIsPinnedToMenu] = useState(
|
||||
db.shortcuts.exists(notebook.id)
|
||||
);
|
||||
const setMenuPins = useMenuStore((state) => state.setMenuPins);
|
||||
const totalNotes = getTotalNotes(notebook);
|
||||
const shortcutRef = useRef();
|
||||
|
||||
const onPinNotebook = async () => {
|
||||
try {
|
||||
@@ -56,7 +50,7 @@ export const NotebookHeader = ({
|
||||
type: "notebook"
|
||||
}
|
||||
});
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Shortcut created",
|
||||
type: "success"
|
||||
});
|
||||
@@ -82,7 +76,7 @@ export const NotebookHeader = ({
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
|
||||
{getFormattedDate(notebook.dateModified, "date-time")}
|
||||
{new Date(notebook.dateEdited).toLocaleString()}
|
||||
</Paragraph>
|
||||
<View
|
||||
style={{
|
||||
@@ -109,6 +103,7 @@ export const NotebookHeader = ({
|
||||
name={isPinnedToMenu ? "link-variant-off" : "link-variant"}
|
||||
onPress={onPinNotebook}
|
||||
tooltipText={"Create shortcut in side menu"}
|
||||
fwdRef={shortcutRef}
|
||||
customStyle={{
|
||||
marginRight: 15,
|
||||
width: 40,
|
||||
@@ -143,15 +138,15 @@ export const NotebookHeader = ({
|
||||
style={{
|
||||
marginTop: 10,
|
||||
fontStyle: "italic",
|
||||
fontFamily: undefined
|
||||
fontFamily: null
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{/* {notebook.topics.length === 1
|
||||
{notebook.topics.length === 1
|
||||
? "1 topic"
|
||||
: `${notebook.topics.length} topics`}
|
||||
,{" "} */}
|
||||
,{" "}
|
||||
{notebook && totalNotes > 1
|
||||
? totalNotes + " notes"
|
||||
: totalNotes === 1
|
||||
@@ -17,52 +17,39 @@ 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 { GroupHeader, GroupOptions, ItemType } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import React, { useRef } from "react";
|
||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
|
||||
import { presentSheet } from "../../../services/event-manager";
|
||||
import { eSendEvent, presentSheet } from "../../../services/event-manager";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { RouteName } from "../../../stores/use-navigation-store";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import { ColorValues } from "../../../utils/colors";
|
||||
import { GROUP } from "../../../utils/constants";
|
||||
import { eOpenJumpToDialog } from "../../../utils/events";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import Sort from "../../sheets/sort";
|
||||
import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
|
||||
type SectionHeaderProps = {
|
||||
item: GroupHeader;
|
||||
index: number;
|
||||
dataType: ItemType;
|
||||
color?: string;
|
||||
screen?: RouteName;
|
||||
groupOptions: GroupOptions;
|
||||
onOpenJumpToDialog: () => void;
|
||||
};
|
||||
|
||||
export const SectionHeader = React.memo<
|
||||
React.FunctionComponent<SectionHeaderProps>
|
||||
>(
|
||||
function SectionHeader({
|
||||
item,
|
||||
index,
|
||||
dataType,
|
||||
color,
|
||||
screen,
|
||||
groupOptions,
|
||||
onOpenJumpToDialog
|
||||
}: SectionHeaderProps) {
|
||||
export const SectionHeader = React.memo(
|
||||
function SectionHeader({ item, index, type, color, screen, groupOptions }) {
|
||||
const { colors } = useThemeColors();
|
||||
const { fontScale } = useWindowDimensions();
|
||||
let groupBy = Object.keys(GROUP).find(
|
||||
(key) => GROUP[key as keyof typeof GROUP] === groupOptions.groupBy
|
||||
(key) => GROUP[key] === groupOptions.groupBy
|
||||
);
|
||||
const isCompactModeEnabled = useIsCompactModeEnabled(
|
||||
dataType as "note" | "notebook"
|
||||
const jumpToRef = useRef();
|
||||
const sortRef = useRef();
|
||||
const compactModeRef = useRef();
|
||||
|
||||
const notebooksListMode = useSettingStore(
|
||||
(state) => state.settings.notebooksListMode
|
||||
);
|
||||
const notesListMode = useSettingStore(
|
||||
(state) => state.settings.notesListMode
|
||||
);
|
||||
const listMode = type === "notebooks" ? notebooksListMode : notesListMode;
|
||||
|
||||
groupBy = !groupBy
|
||||
? "Default"
|
||||
@@ -85,8 +72,9 @@ export const SectionHeader = React.memo<
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
onOpenJumpToDialog();
|
||||
eSendEvent(eOpenJumpToDialog, type);
|
||||
}}
|
||||
ref={jumpToRef}
|
||||
activeOpacity={0.9}
|
||||
hitSlop={{ top: 10, left: 10, right: 30, bottom: 15 }}
|
||||
style={{
|
||||
@@ -95,10 +83,7 @@ export const SectionHeader = React.memo<
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
color={
|
||||
ColorValues[color?.toLowerCase() as keyof typeof ColorValues] ||
|
||||
colors.primary.accent
|
||||
}
|
||||
color={ColorValues[color?.toLowerCase()] || colors.primary.accent}
|
||||
size={SIZE.sm}
|
||||
style={{
|
||||
minWidth: 60,
|
||||
@@ -116,17 +101,16 @@ export const SectionHeader = React.memo<
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{index === 0 && (
|
||||
{index === 0 ? (
|
||||
<>
|
||||
<Button
|
||||
onPress={() => {
|
||||
console.log("Opening Sort sheet", screen, dataType);
|
||||
presentSheet({
|
||||
component: <Sort screen={screen} type={dataType} />
|
||||
component: <Sort screen={screen} type={type} />
|
||||
});
|
||||
}}
|
||||
hidden={screen === "Reminders"}
|
||||
tooltipText="Change sorting of items in list"
|
||||
fwdRef={sortRef}
|
||||
title={groupBy}
|
||||
icon={
|
||||
groupOptions.sortDirection === "asc"
|
||||
@@ -139,9 +123,7 @@ export const SectionHeader = React.memo<
|
||||
paddingHorizontal: 0,
|
||||
backgroundColor: "transparent",
|
||||
marginRight:
|
||||
dataType === "note" ||
|
||||
screen === "Notes" ||
|
||||
dataType === "notebook"
|
||||
type === "notes" || type === "home" || type === "notebooks"
|
||||
? 10
|
||||
: 0
|
||||
}}
|
||||
@@ -155,41 +137,35 @@ export const SectionHeader = React.memo<
|
||||
height: 25
|
||||
}}
|
||||
hidden={
|
||||
dataType !== "note" &&
|
||||
dataType !== "notebook" &&
|
||||
screen !== "Notes"
|
||||
type !== "notes" && type !== "notebooks" && type !== "home"
|
||||
}
|
||||
testID="icon-compact-mode"
|
||||
tooltipText={
|
||||
isCompactModeEnabled
|
||||
listMode == "compact"
|
||||
? "Switch to normal mode"
|
||||
: "Switch to compact mode"
|
||||
}
|
||||
fwdRef={compactModeRef}
|
||||
color={colors.secondary.icon}
|
||||
name={isCompactModeEnabled ? "view-list" : "view-list-outline"}
|
||||
name={listMode == "compact" ? "view-list" : "view-list-outline"}
|
||||
onPress={() => {
|
||||
SettingsService.set({
|
||||
[dataType !== "notebook"
|
||||
? "notesListMode"
|
||||
: "notebooksListMode"]: !isCompactModeEnabled
|
||||
? "compact"
|
||||
: "normal"
|
||||
});
|
||||
let settings = {};
|
||||
settings[
|
||||
type !== "notebooks" ? "notesListMode" : "notebooksListMode"
|
||||
] = listMode === "normal" ? "compact" : "normal";
|
||||
|
||||
SettingsService.set(settings);
|
||||
}}
|
||||
size={SIZE.lg - 2}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
},
|
||||
(prev, next) => {
|
||||
if (prev.item.title !== next.item.title) return false;
|
||||
if (prev.groupOptions?.groupBy !== next.groupOptions.groupBy) return false;
|
||||
if (prev.groupOptions?.sortDirection !== next.groupOptions.sortDirection)
|
||||
return false;
|
||||
if (prev.groupOptions?.sortBy !== next.groupOptions.sortBy) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -17,30 +17,21 @@ 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 {
|
||||
BaseTrashItem,
|
||||
Color,
|
||||
Note,
|
||||
Reminder,
|
||||
TrashItem
|
||||
} from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { EntityLevel, decode } from "entities";
|
||||
import { getUpcomingReminder } from "@notesnook/core/dist/collections/reminders";
|
||||
import { decode, EntityLevel } from "entities";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
|
||||
import NotebookScreen from "../../../screens/notebook";
|
||||
import { db } from "../../../common/database";
|
||||
import Notebook from "../../../screens/notebook";
|
||||
import { TaggedNotes } from "../../../screens/notes/tagged";
|
||||
import { useEditorStore } from "../../../stores/use-editor-store";
|
||||
import { TopicNotes } from "../../../screens/notes/topic-notes";
|
||||
import useNavigationStore from "../../../stores/use-navigation-store";
|
||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { ColorValues } from "../../../utils/colors";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import {
|
||||
NotebooksWithDateEdited,
|
||||
TagsWithDateEdited
|
||||
} from "../../list/list-item.wrapper";
|
||||
import { Properties } from "../../properties";
|
||||
import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
@@ -48,41 +39,80 @@ import { ReminderTime } from "../../ui/reminder-time";
|
||||
import { TimeSince } from "../../ui/time-since";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
|
||||
import { useEditorStore } from "../../../stores/use-editor-store";
|
||||
|
||||
type NoteItemProps = {
|
||||
item: Note | BaseTrashItem<Note>;
|
||||
index: number;
|
||||
tags?: TagsWithDateEdited;
|
||||
notebooks?: NotebooksWithDateEdited;
|
||||
color?: Color;
|
||||
reminder?: Reminder;
|
||||
attachmentsCount: number;
|
||||
date: number;
|
||||
isTrash?: boolean;
|
||||
noOpen?: boolean;
|
||||
function navigateToTag(item) {
|
||||
const tag = db.tags.tag(item.id);
|
||||
if (!tag) return;
|
||||
TaggedNotes.navigate(tag, true);
|
||||
}
|
||||
|
||||
const showActionSheet = (item) => {
|
||||
Properties.present(item);
|
||||
};
|
||||
|
||||
function getNotebook(item) {
|
||||
const isTrash = item.type === "trash";
|
||||
const currentId = useNavigationStore.getState().currentScreen.id;
|
||||
if (isTrash) return [];
|
||||
const items = [];
|
||||
const notebooks = db.relations.to(item, "notebook") || [];
|
||||
|
||||
for (let notebook of notebooks) {
|
||||
if (items.length > 1) break;
|
||||
if (notebook.id === currentId) continue;
|
||||
items.push(notebook);
|
||||
}
|
||||
|
||||
if (item.notebooks) {
|
||||
for (let nb of item.notebooks) {
|
||||
if (items.length > 1) break;
|
||||
const notebook = db.notebooks?.notebook(nb.id)?.data;
|
||||
if (!notebook) continue;
|
||||
for (let topicId of nb.topics) {
|
||||
if (items.length > 1) break;
|
||||
if (topicId === currentId) continue;
|
||||
const topic = notebook.topics.find((t) => t.id === topicId);
|
||||
if (!topic) continue;
|
||||
items.push(topic);
|
||||
}
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
function getTags(item) {
|
||||
const noteTags = item.tags?.slice(0, 3) || [];
|
||||
const tags = [];
|
||||
for (const tagName of noteTags) {
|
||||
const tag = db.tags.tag(tagName);
|
||||
if (!tag) continue;
|
||||
tags.push(tag);
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
const NoteItem = ({
|
||||
item,
|
||||
isTrash,
|
||||
date,
|
||||
color,
|
||||
notebooks,
|
||||
reminder,
|
||||
tags,
|
||||
attachmentsCount,
|
||||
noOpen
|
||||
}: NoteItemProps) => {
|
||||
const currentEditingNote = useEditorStore(
|
||||
(state) => state.currentEditingNote
|
||||
dateBy = "dateCreated",
|
||||
noOpen = false
|
||||
}) => {
|
||||
const isEditingNote = useEditorStore(
|
||||
(state) => state.currentEditingNote === item.id
|
||||
);
|
||||
const { colors } = useThemeColors();
|
||||
const compactMode = useIsCompactModeEnabled(
|
||||
(item as TrashItem).itemType || item.type
|
||||
);
|
||||
const compactMode = useIsCompactModeEnabled(item);
|
||||
const attachmentCount = db.attachments?.ofNote(item.id, "all")?.length || 0;
|
||||
const _update = useRelationStore((state) => state.updater);
|
||||
const primaryColors =
|
||||
currentEditingNote === item.id ? colors.selected : colors.primary;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const notebooks = React.useMemo(() => getNotebook(item), [item, _update]);
|
||||
const reminders = db.relations.from(item, "reminder");
|
||||
const reminder = getUpcomingReminder(reminders);
|
||||
const noteColor = ColorValues[item.color?.toLowerCase()];
|
||||
const tags = getTags(item);
|
||||
const primaryColors = isEditingNote ? colors.selected : colors.primary;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -103,45 +133,44 @@ const NoteItem = ({
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
>
|
||||
{notebooks?.items
|
||||
?.filter(
|
||||
(item) =>
|
||||
item.id !== useNavigationStore.getState().focusedRouteId
|
||||
)
|
||||
.map((item) => (
|
||||
<Button
|
||||
title={
|
||||
item.title.length > 25
|
||||
? item.title.slice(0, 25) + "..."
|
||||
: item.title
|
||||
{notebooks?.map((item) => (
|
||||
<Button
|
||||
title={
|
||||
item.title.length > 25
|
||||
? item.title.slice(0, 25) + "..."
|
||||
: item.title
|
||||
}
|
||||
tooltipText={item.title}
|
||||
key={item.id}
|
||||
height={25}
|
||||
icon={item.type === "topic" ? "bookmark" : "book-outline"}
|
||||
type="grayBg"
|
||||
fontSize={SIZE.xs}
|
||||
iconSize={SIZE.sm}
|
||||
textStyle={{
|
||||
marginRight: 0
|
||||
}}
|
||||
style={{
|
||||
borderRadius: 5,
|
||||
marginRight: 5,
|
||||
borderWidth: 0.5,
|
||||
borderColor: primaryColors.border,
|
||||
paddingHorizontal: 6,
|
||||
marginBottom: 5
|
||||
}}
|
||||
onPress={() => {
|
||||
if (item.type === "topic") {
|
||||
TopicNotes.navigate(item, true);
|
||||
} else {
|
||||
Notebook.navigate(item);
|
||||
}
|
||||
tooltipText={item.title}
|
||||
key={item.id}
|
||||
height={25}
|
||||
icon={item.type === "notebook" ? "bookmark" : "book-outline"}
|
||||
type="grayBg"
|
||||
fontSize={SIZE.xs}
|
||||
iconSize={SIZE.sm}
|
||||
textStyle={{
|
||||
marginRight: 0
|
||||
}}
|
||||
style={{
|
||||
borderRadius: 5,
|
||||
marginRight: 5,
|
||||
borderWidth: 0.5,
|
||||
borderColor: primaryColors.border,
|
||||
paddingHorizontal: 6,
|
||||
marginBottom: 5
|
||||
}}
|
||||
onPress={() => {
|
||||
NotebookScreen.navigate(item, true);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
<ReminderTime
|
||||
reminder={reminder}
|
||||
color={color?.colorCode}
|
||||
color={noteColor}
|
||||
onPress={() => {
|
||||
Properties.present(reminder);
|
||||
}}
|
||||
@@ -155,7 +184,9 @@ const NoteItem = ({
|
||||
{compactMode ? (
|
||||
<Paragraph
|
||||
numberOfLines={1}
|
||||
color={color?.colorCode || primaryColors.heading}
|
||||
color={
|
||||
ColorValues[item.color?.toLowerCase()] || primaryColors.heading
|
||||
}
|
||||
style={{
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
@@ -166,7 +197,9 @@ const NoteItem = ({
|
||||
) : (
|
||||
<Heading
|
||||
numberOfLines={1}
|
||||
color={color?.colorCode || primaryColors.heading}
|
||||
color={
|
||||
ColorValues[item.color?.toLowerCase()] || primaryColors.heading
|
||||
}
|
||||
style={{
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
@@ -219,11 +252,13 @@ const NoteItem = ({
|
||||
color: colors.secondary.paragraph,
|
||||
marginRight: 6
|
||||
}}
|
||||
time={date}
|
||||
updateFrequency={Date.now() - date < 60000 ? 2000 : 60000}
|
||||
time={item[dateBy]}
|
||||
updateFrequency={
|
||||
Date.now() - item[dateBy] < 60000 ? 2000 : 60000
|
||||
}
|
||||
/>
|
||||
|
||||
{attachmentsCount > 0 ? (
|
||||
{attachmentCount > 0 ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
@@ -240,7 +275,7 @@ const NoteItem = ({
|
||||
color={colors.secondary.paragraph}
|
||||
size={SIZE.xs}
|
||||
>
|
||||
{attachmentsCount}
|
||||
{attachmentCount}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null}
|
||||
@@ -253,7 +288,10 @@ const NoteItem = ({
|
||||
style={{
|
||||
marginRight: 6
|
||||
}}
|
||||
color={color?.colorCode || primaryColors.accent}
|
||||
color={
|
||||
ColorValues[item.color?.toLowerCase()] ||
|
||||
primaryColors.accent
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -282,10 +320,10 @@ const NoteItem = ({
|
||||
) : null}
|
||||
|
||||
{!isTrash && !compactMode && tags
|
||||
? tags.items?.map((item) =>
|
||||
? tags.map((item) =>
|
||||
item.id ? (
|
||||
<Button
|
||||
title={"#" + item.title}
|
||||
title={"#" + item.alias}
|
||||
key={item.id}
|
||||
height={23}
|
||||
type="gray"
|
||||
@@ -299,9 +337,9 @@ const NoteItem = ({
|
||||
paddingHorizontal: 6,
|
||||
marginRight: 4,
|
||||
zIndex: 10,
|
||||
maxWidth: tags.items?.length > 1 ? 130 : null
|
||||
maxWidth: tags.length > 1 ? 130 : null
|
||||
}}
|
||||
onPress={() => TaggedNotes.navigate(item, true)}
|
||||
onPress={() => navigateToTag(item)}
|
||||
/>
|
||||
) : null
|
||||
)
|
||||
@@ -329,8 +367,7 @@ const NoteItem = ({
|
||||
marginRight: 6
|
||||
}}
|
||||
>
|
||||
{(item as TrashItem).itemType[0].toUpperCase() +
|
||||
(item as TrashItem).itemType.slice(1)}
|
||||
{item.itemType[0].toUpperCase() + item.itemType.slice(1)}
|
||||
</Paragraph>
|
||||
</>
|
||||
)}
|
||||
@@ -386,8 +423,8 @@ const NoteItem = ({
|
||||
color: colors.secondary.paragraph,
|
||||
marginRight: 6
|
||||
}}
|
||||
time={date}
|
||||
updateFrequency={Date.now() - date < 60000 ? 2000 : 60000}
|
||||
time={item[dateBy]}
|
||||
updateFrequency={Date.now() - item[dateBy] < 60000 ? 2000 : 60000}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
@@ -397,7 +434,7 @@ const NoteItem = ({
|
||||
color={primaryColors.paragraph}
|
||||
name="dots-horizontal"
|
||||
size={SIZE.xl}
|
||||
onPress={() => !noOpen && Properties.present(item)}
|
||||
onPress={() => !noOpen && showActionSheet(item, isTrash)}
|
||||
customStyle={{
|
||||
justifyContent: "center",
|
||||
height: 35,
|
||||
@@ -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 { BaseTrashItem, Color, Note, Reminder } from "@notesnook/core";
|
||||
import React from "react";
|
||||
import NoteItem from ".";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
@@ -33,43 +32,50 @@ import { useEditorStore } from "../../../stores/use-editor-store";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { eOnLoadNote, eShowMergeDialog } from "../../../utils/events";
|
||||
import { tabBarRef } from "../../../utils/global-refs";
|
||||
import type {
|
||||
NotebooksWithDateEdited,
|
||||
TagsWithDateEdited
|
||||
} from "../../list/list-item.wrapper";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
import NotePreview from "../../note-history/preview";
|
||||
import SelectionWrapper from "../selection-wrapper";
|
||||
|
||||
export const openNote = async (
|
||||
item: Note,
|
||||
isTrash?: boolean,
|
||||
isSheet?: boolean
|
||||
) => {
|
||||
let note: Note = item;
|
||||
const present = () =>
|
||||
presentDialog({
|
||||
title: "Note not synced",
|
||||
negativeText: "Ok",
|
||||
paragraph: "Please sync again to open this note for editing"
|
||||
});
|
||||
|
||||
export const openNote = async (item, isTrash, setSelectedItem, isSheet) => {
|
||||
let _note = item;
|
||||
if (isSheet) hideSheet();
|
||||
|
||||
if (!isTrash) {
|
||||
note = (await db.notes.note(item.id)) as Note;
|
||||
_note = db.notes.note(item.id).data;
|
||||
if (!db.notes.note(item.id)?.synced()) {
|
||||
present();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!db.trash.synced(item.id)) {
|
||||
present();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { selectedItemsList, selectionMode, clearSelection, setSelectedItem } =
|
||||
const { selectedItemsList, selectionMode, clearSelection } =
|
||||
useSelectionStore.getState();
|
||||
|
||||
if (selectedItemsList.length > 0 && selectionMode === item.type) {
|
||||
setSelectedItem(note.id);
|
||||
if (selectedItemsList.length > 0 && selectionMode) {
|
||||
setSelectedItem && setSelectedItem(_note);
|
||||
return;
|
||||
} else {
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
if (note.conflicted) {
|
||||
eSendEvent(eShowMergeDialog, note);
|
||||
if (_note.conflicted) {
|
||||
eSendEvent(eShowMergeDialog, _note);
|
||||
return;
|
||||
}
|
||||
|
||||
if (note.locked) {
|
||||
if (_note.locked) {
|
||||
openVault({
|
||||
item: note,
|
||||
item: _note,
|
||||
novault: true,
|
||||
locked: true,
|
||||
goToEditor: true,
|
||||
@@ -79,81 +85,48 @@ export const openNote = async (
|
||||
return;
|
||||
}
|
||||
if (isTrash) {
|
||||
if (!note.contentId) return;
|
||||
|
||||
const content = await db.content.get(note.contentId as string);
|
||||
const content = await db.content.get(item.contentId);
|
||||
presentSheet({
|
||||
component: (
|
||||
<NotePreview note={item} content={{ type: "tiptap", data: content }} />
|
||||
)
|
||||
});
|
||||
} else {
|
||||
useEditorStore.getState().setReadonly(note?.readonly);
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: note
|
||||
});
|
||||
useEditorStore.getState().setReadonly(_note?.readonly);
|
||||
eSendEvent(eOnLoadNote, _note);
|
||||
if (!DDS.isTab) {
|
||||
tabBarRef.current?.goToPage(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
type NoteWrapperProps = {
|
||||
item: Note | BaseTrashItem<Note>;
|
||||
index: number;
|
||||
tags?: TagsWithDateEdited;
|
||||
notebooks?: NotebooksWithDateEdited;
|
||||
color?: Color;
|
||||
reminder?: Reminder;
|
||||
attachmentsCount: number;
|
||||
date: number;
|
||||
isRenderedInActionSheet: boolean;
|
||||
};
|
||||
|
||||
export const NoteWrapper = React.memo<
|
||||
React.FunctionComponent<NoteWrapperProps>
|
||||
>(
|
||||
function NoteWrapper({
|
||||
item,
|
||||
index,
|
||||
isRenderedInActionSheet,
|
||||
...restProps
|
||||
}: NoteWrapperProps) {
|
||||
export const NoteWrapper = React.memo(
|
||||
function NoteWrapper({ item, index, dateBy, isSheet }) {
|
||||
const isTrash = item.type === "trash";
|
||||
const setSelectedItem = useSelectionStore((state) => state.setSelectedItem);
|
||||
|
||||
return (
|
||||
<SelectionWrapper
|
||||
index={index}
|
||||
height={100}
|
||||
testID={notesnook.ids.note.get(index)}
|
||||
onPress={() => openNote(item as Note, isTrash, isRenderedInActionSheet)}
|
||||
isSheet={isRenderedInActionSheet}
|
||||
onPress={() => openNote(item, isTrash, setSelectedItem, isSheet)}
|
||||
isSheet={isSheet}
|
||||
item={item}
|
||||
color={restProps.color?.colorCode}
|
||||
>
|
||||
<NoteItem {...restProps} item={item} index={index} isTrash={isTrash} />
|
||||
<NoteItem item={item} dateBy={dateBy} isTrash={isTrash} />
|
||||
</SelectionWrapper>
|
||||
);
|
||||
},
|
||||
(prev, next) => {
|
||||
if (prev.date !== next.date) {
|
||||
if (prev.dateBy !== next.dateBy) {
|
||||
return false;
|
||||
}
|
||||
if (prev.item?.dateEdited !== next.item?.dateEdited) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
prev.tags?.dateEdited !== next.tags?.dateEdited ||
|
||||
prev.tags?.items?.length !== next.tags?.items?.length
|
||||
)
|
||||
return false;
|
||||
|
||||
if (
|
||||
prev.notebooks?.dateEdited !== next.notebooks?.dateEdited ||
|
||||
prev.notebooks?.items?.length !== next.notebooks?.items?.length
|
||||
)
|
||||
return false;
|
||||
|
||||
if (prev.color !== next.color) return false;
|
||||
if (prev.reminder?.id !== next.reminder?.id) return false;
|
||||
if (prev.attachmentsCount !== next.attachmentsCount) return false;
|
||||
if (prev.item?.dateModified !== next.item?.dateModified) {
|
||||
if (prev.item !== next.item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -17,38 +17,41 @@ 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 { getFormattedDate } from "@notesnook/common";
|
||||
import { BaseTrashItem, Notebook, TrashItem } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
|
||||
import { TopicNotes } from "../../../screens/notes/topic-notes";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Properties } from "../../properties";
|
||||
import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { getFormattedDate } from "@notesnook/common";
|
||||
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
|
||||
|
||||
type NotebookItemProps = {
|
||||
item: Notebook | BaseTrashItem<Notebook>;
|
||||
totalNotes: number;
|
||||
date: number;
|
||||
index: number;
|
||||
isTrash: boolean;
|
||||
const showActionSheet = (item) => {
|
||||
Properties.present(item);
|
||||
};
|
||||
|
||||
const navigateToTopic = (topic) => {
|
||||
if (useSelectionStore.getState().selectedItemsList.length > 0) return;
|
||||
TopicNotes.navigate(topic, true);
|
||||
};
|
||||
|
||||
export const NotebookItem = ({
|
||||
item,
|
||||
isTopic = false,
|
||||
isTrash,
|
||||
date,
|
||||
dateBy,
|
||||
totalNotes
|
||||
}: NotebookItemProps) => {
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const compactMode = useIsCompactModeEnabled(
|
||||
(item as TrashItem).itemType || item.type
|
||||
);
|
||||
const compactMode = useIsCompactModeEnabled(item);
|
||||
const topics = item.topics?.slice(0, 3) || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -80,7 +83,7 @@ export const NotebookItem = ({
|
||||
</Heading>
|
||||
)}
|
||||
|
||||
{!item.description || compactMode ? null : (
|
||||
{isTopic || !item.description || compactMode ? null : (
|
||||
<Paragraph
|
||||
size={SIZE.sm}
|
||||
numberOfLines={2}
|
||||
@@ -92,6 +95,42 @@ export const NotebookItem = ({
|
||||
</Paragraph>
|
||||
)}
|
||||
|
||||
{isTopic || compactMode ? null : (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
>
|
||||
{topics.map((topic) => (
|
||||
<Button
|
||||
title={topic.title}
|
||||
key={topic.id}
|
||||
height={null}
|
||||
textStyle={{
|
||||
marginRight: 0
|
||||
}}
|
||||
type="grayBg"
|
||||
fontSize={SIZE.xs}
|
||||
icon="bookmark-outline"
|
||||
iconSize={SIZE.sm}
|
||||
style={{
|
||||
borderRadius: 5,
|
||||
maxWidth: 120,
|
||||
borderWidth: 0.5,
|
||||
paddingVertical: 2.5,
|
||||
borderColor: colors.primary.border,
|
||||
paddingHorizontal: 6,
|
||||
marginVertical: 5,
|
||||
marginRight: 5
|
||||
}}
|
||||
onPress={() => navigateToTopic(topic)}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{!compactMode ? (
|
||||
<View
|
||||
style={{
|
||||
@@ -113,9 +152,7 @@ export const NotebookItem = ({
|
||||
}}
|
||||
>
|
||||
{"Deleted on " +
|
||||
new Date((item as TrashItem).dateDeleted)
|
||||
.toISOString()
|
||||
.slice(0, 10)}
|
||||
new Date(item.dateDeleted).toISOString().slice(0, 10)}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
color={colors.primary.accent}
|
||||
@@ -125,8 +162,7 @@ export const NotebookItem = ({
|
||||
marginRight: 6
|
||||
}}
|
||||
>
|
||||
{(item as TrashItem).itemType[0].toUpperCase() +
|
||||
(item as TrashItem).itemType.slice(1)}
|
||||
{item.itemType[0].toUpperCase() + item.itemType.slice(1)}
|
||||
</Paragraph>
|
||||
</>
|
||||
) : (
|
||||
@@ -137,7 +173,7 @@ export const NotebookItem = ({
|
||||
marginRight: 6
|
||||
}}
|
||||
>
|
||||
{getFormattedDate(date, "date")}
|
||||
{getFormattedDate(item[dateBy], "date")}
|
||||
</Paragraph>
|
||||
)}
|
||||
<Paragraph
|
||||
@@ -197,7 +233,7 @@ export const NotebookItem = ({
|
||||
name="dots-horizontal"
|
||||
testID={notesnook.ids.notebook.menu}
|
||||
size={SIZE.xl}
|
||||
onPress={() => Properties.present(item)}
|
||||
onPress={() => showActionSheet(item)}
|
||||
customStyle={{
|
||||
justifyContent: "center",
|
||||
height: 35,
|
||||
@@ -17,24 +17,41 @@ 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 { BaseTrashItem, Notebook } from "@notesnook/core";
|
||||
import React from "react";
|
||||
import { NotebookItem } from ".";
|
||||
import { db } from "../../../common/database";
|
||||
import NotebookScreen from "../../../screens/notebook";
|
||||
import { ToastManager } from "../../../services/event-manager";
|
||||
import { TopicNotes } from "../../../screens/notes/topic-notes";
|
||||
import { ToastEvent } from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { useTrashStore } from "../../../stores/use-trash-store";
|
||||
import { db } from "../../../common/database";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
import SelectionWrapper from "../selection-wrapper";
|
||||
|
||||
export const openNotebook = (item: Notebook | BaseTrashItem<Notebook>) => {
|
||||
const navigateToNotebook = (item, canGoBack) => {
|
||||
if (!item) return;
|
||||
|
||||
Navigation.navigate(
|
||||
{
|
||||
title: item.title,
|
||||
name: "Notebook",
|
||||
id: item.id,
|
||||
type: "notebook"
|
||||
},
|
||||
{
|
||||
title: item.title,
|
||||
item: item,
|
||||
canGoBack
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const openNotebookTopic = (item) => {
|
||||
const isTrash = item.type === "trash";
|
||||
const { selectedItemsList, setSelectedItem, selectionMode, clearSelection } =
|
||||
useSelectionStore.getState();
|
||||
if (selectedItemsList.length > 0 && selectionMode === item.type) {
|
||||
setSelectedItem(item.id);
|
||||
if (selectedItemsList.length > 0 && selectionMode) {
|
||||
setSelectedItem(item);
|
||||
return;
|
||||
} else {
|
||||
clearSelection();
|
||||
@@ -42,15 +59,15 @@ export const openNotebook = (item: Notebook | BaseTrashItem<Notebook>) => {
|
||||
|
||||
if (isTrash) {
|
||||
presentDialog({
|
||||
title: `Restore notebook`,
|
||||
paragraph: `Restore or delete notebook forever`,
|
||||
title: `Restore ${item.itemType}`,
|
||||
paragraph: `Restore or delete ${item.itemType} forever`,
|
||||
positiveText: "Restore",
|
||||
negativeText: "Delete",
|
||||
positivePress: async () => {
|
||||
await db.trash.restore(item.id);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useSelectionStore.getState().setSelectionMode(undefined);
|
||||
ToastManager.show({
|
||||
useSelectionStore.getState().setSelectionMode(false);
|
||||
ToastEvent.show({
|
||||
heading: "Restore successful",
|
||||
type: "success"
|
||||
});
|
||||
@@ -58,8 +75,8 @@ export const openNotebook = (item: Notebook | BaseTrashItem<Notebook>) => {
|
||||
onClose: async () => {
|
||||
await db.trash.delete(item.id);
|
||||
useTrashStore.getState().setTrash();
|
||||
useSelectionStore.getState().setSelectionMode(undefined);
|
||||
ToastManager.show({
|
||||
useSelectionStore.getState().setSelectionMode(false);
|
||||
ToastEvent.show({
|
||||
heading: "Permanently deleted items",
|
||||
type: "success",
|
||||
context: "local"
|
||||
@@ -68,30 +85,29 @@ export const openNotebook = (item: Notebook | BaseTrashItem<Notebook>) => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
NotebookScreen.navigate(item, true);
|
||||
};
|
||||
|
||||
type NotebookWrapperProps = {
|
||||
item: Notebook | BaseTrashItem<Notebook>;
|
||||
totalNotes: number;
|
||||
date: number;
|
||||
index: number;
|
||||
if (item.type === "topic") {
|
||||
TopicNotes.navigate(item, true);
|
||||
} else {
|
||||
navigateToNotebook(item, true);
|
||||
}
|
||||
};
|
||||
|
||||
export const NotebookWrapper = React.memo(
|
||||
function NotebookWrapper({
|
||||
item,
|
||||
index,
|
||||
date,
|
||||
totalNotes
|
||||
}: NotebookWrapperProps) {
|
||||
function NotebookWrapper({ item, index, dateBy, totalNotes }) {
|
||||
const isTrash = item.type === "trash";
|
||||
|
||||
return (
|
||||
<SelectionWrapper onPress={() => openNotebook(item)} item={item}>
|
||||
<SelectionWrapper
|
||||
pinned={item.pinned}
|
||||
index={index}
|
||||
onPress={() => openNotebookTopic(item)}
|
||||
height={item.type === "topic" ? 80 : 110}
|
||||
item={item}
|
||||
>
|
||||
<NotebookItem
|
||||
isTopic={item.type === "topic"}
|
||||
item={item}
|
||||
date={date}
|
||||
dateBy={dateBy}
|
||||
index={index}
|
||||
isTrash={isTrash}
|
||||
totalNotes={totalNotes}
|
||||
@@ -101,8 +117,17 @@ export const NotebookWrapper = React.memo(
|
||||
},
|
||||
(prev, next) => {
|
||||
if (prev.totalNotes !== next.totalNotes) return false;
|
||||
if (prev.date !== next.date) return false;
|
||||
if (prev.item?.dateModified !== next.item?.dateModified) return false;
|
||||
if (prev.item.title !== next.item.title) return false;
|
||||
if (prev.dateBy !== next.dateBy) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (prev.item?.dateEdited !== next.item?.dateEdited) {
|
||||
return false;
|
||||
}
|
||||
if (JSON.stringify(prev.item) !== JSON.stringify(next.item)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -16,21 +16,20 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { Reminder } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import type { Reminder } from "../../../services/notifications";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Properties } from "../../properties";
|
||||
import ReminderSheet from "../../sheets/reminder";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import { ReminderTime } from "../../ui/reminder-time";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import SelectionWrapper from "../selection-wrapper";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import ReminderSheet from "../../sheets/reminder";
|
||||
import { ReminderTime } from "../../ui/reminder-time";
|
||||
|
||||
const ReminderItem = React.memo(
|
||||
({
|
||||
@@ -44,23 +43,17 @@ const ReminderItem = React.memo(
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const openReminder = () => {
|
||||
const {
|
||||
selectedItemsList,
|
||||
setSelectedItem,
|
||||
selectionMode,
|
||||
clearSelection
|
||||
} = useSelectionStore.getState();
|
||||
if (selectedItemsList.length > 0 && selectionMode === item.type) {
|
||||
setSelectedItem(item.id);
|
||||
return;
|
||||
} else {
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
ReminderSheet.present(item, undefined, isSheet);
|
||||
};
|
||||
return (
|
||||
<SelectionWrapper onPress={openReminder} item={item} isSheet={isSheet}>
|
||||
<SelectionWrapper
|
||||
//@ts-ignore
|
||||
index={index}
|
||||
height={100}
|
||||
onPress={openReminder}
|
||||
item={item}
|
||||
isSheet={isSheet}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
@@ -138,11 +131,7 @@ const ReminderItem = React.memo(
|
||||
height: 30
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="reload"
|
||||
size={SIZE.md}
|
||||
color={colors.primary.accent}
|
||||
/>
|
||||
<Icon name="reload" size={SIZE.md} color={colors.primary.accent} />
|
||||
<Paragraph
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
|
||||
@@ -22,17 +22,16 @@ import React from "react";
|
||||
import { View } from "react-native";
|
||||
import useIsSelected from "../../../hooks/use-selected";
|
||||
import { useEditorStore } from "../../../stores/use-editor-store";
|
||||
import { Item } from "@notesnook/core";
|
||||
|
||||
export const Filler = ({ item, color }: { item: Item; color?: string }) => {
|
||||
export const Filler = ({ item }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const currentEditingNote = useEditorStore(
|
||||
(state) => state.currentEditingNote
|
||||
const isEditingNote = useEditorStore(
|
||||
(state) => state.currentEditingNote === item.id
|
||||
);
|
||||
|
||||
const [selected] = useIsSelected(item);
|
||||
|
||||
return currentEditingNote === item.id || selected ? (
|
||||
return isEditingNote || selected ? (
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
@@ -40,12 +39,11 @@ export const Filler = ({ item, color }: { item: Item; color?: string }) => {
|
||||
height: "150%",
|
||||
backgroundColor: colors.selected.background,
|
||||
borderLeftWidth: 5,
|
||||
borderLeftColor:
|
||||
currentEditingNote === item.id
|
||||
? color
|
||||
? color
|
||||
: colors.selected.accent
|
||||
: "transparent"
|
||||
borderLeftColor: isEditingNote
|
||||
? item.color
|
||||
? colors.static[item.color]
|
||||
: colors.selected.accent
|
||||
: "transparent"
|
||||
}}
|
||||
collapsable={false}
|
||||
/>
|
||||
@@ -18,45 +18,37 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { PropsWithChildren, useRef } from "react";
|
||||
import React, { useRef } from "react";
|
||||
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { PressableButton } from "../../ui/pressable";
|
||||
import { Filler } from "./back-fill";
|
||||
import { SelectionIcon } from "./selection";
|
||||
import { Item, TrashItem } from "@notesnook/core";
|
||||
|
||||
type SelectionWrapperProps = PropsWithChildren<{
|
||||
item: Item;
|
||||
onPress: () => void;
|
||||
testID?: string;
|
||||
isSheet?: boolean;
|
||||
color?: string;
|
||||
}>;
|
||||
|
||||
const SelectionWrapper = ({
|
||||
children,
|
||||
item,
|
||||
background,
|
||||
onPress,
|
||||
testID,
|
||||
isSheet,
|
||||
children,
|
||||
color
|
||||
}: SelectionWrapperProps) => {
|
||||
isSheet
|
||||
}) => {
|
||||
const itemId = useRef(item.id);
|
||||
const { colors, isDark } = useThemeColors();
|
||||
const compactMode = useIsCompactModeEnabled(
|
||||
(item as TrashItem).itemType || item.type
|
||||
);
|
||||
const compactMode = useIsCompactModeEnabled(item);
|
||||
|
||||
if (item.id !== itemId.current) {
|
||||
itemId.current = item.id;
|
||||
}
|
||||
|
||||
const onLongPress = () => {
|
||||
if (useSelectionStore.getState().selectionMode !== item.type) {
|
||||
useSelectionStore.getState().setSelectionMode(item.type);
|
||||
if (!useSelectionStore.getState().selectionMode) {
|
||||
useSelectionStore.setState({
|
||||
selectedItemsList: []
|
||||
});
|
||||
useSelectionStore.getState().setSelectionMode(true);
|
||||
}
|
||||
useSelectionStore.getState().setSelectedItem(item.id);
|
||||
useSelectionStore.getState().setSelectedItem(item);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -80,8 +72,14 @@ const SelectionWrapper = ({
|
||||
marginBottom: isSheet ? 12 : undefined
|
||||
}}
|
||||
>
|
||||
{item.type === "note" ? <Filler item={item} color={color} /> : null}
|
||||
<SelectionIcon item={item} />
|
||||
{item.type === "note" ? (
|
||||
<Filler background={background} item={item} />
|
||||
) : null}
|
||||
<SelectionIcon
|
||||
compactMode={compactMode}
|
||||
item={item}
|
||||
onLongPress={onLongPress}
|
||||
/>
|
||||
{children}
|
||||
</PressableButton>
|
||||
);
|
||||
@@ -25,16 +25,13 @@ import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enab
|
||||
import useIsSelected from "../../../hooks/use-selected";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Item, TrashItem } from "@notesnook/core";
|
||||
|
||||
export const SelectionIcon = ({ item }: { item: Item }) => {
|
||||
export const SelectionIcon = ({ item }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
const [selected] = useIsSelected(item);
|
||||
|
||||
const compactMode = useIsCompactModeEnabled(
|
||||
(item as TrashItem).itemType || item.type
|
||||
);
|
||||
const compactMode = useIsCompactModeEnabled(item);
|
||||
|
||||
return selectionMode ? (
|
||||
<View
|
||||
@@ -17,58 +17,44 @@ 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 { Tag } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { TaggedNotes } from "../../../screens/notes/tagged";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Properties } from "../../properties";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import { PressableButton } from "../../ui/pressable";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import SelectionWrapper from "../selection-wrapper";
|
||||
|
||||
const TagItem = React.memo(
|
||||
({
|
||||
item,
|
||||
index,
|
||||
totalNotes
|
||||
}: {
|
||||
item: Tag;
|
||||
index: number;
|
||||
totalNotes: number;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
({ item, index }) => {
|
||||
const { colors, isDark } = useThemeColors();
|
||||
const onPress = () => {
|
||||
const {
|
||||
selectedItemsList,
|
||||
setSelectedItem,
|
||||
selectionMode,
|
||||
clearSelection
|
||||
} = useSelectionStore.getState();
|
||||
if (selectedItemsList.length > 0 && selectionMode === item.type) {
|
||||
setSelectedItem(item.id);
|
||||
return;
|
||||
} else {
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
TaggedNotes.navigate(item, true);
|
||||
};
|
||||
|
||||
return (
|
||||
<SelectionWrapper
|
||||
<PressableButton
|
||||
onPress={onPress}
|
||||
item={item}
|
||||
selectedColor={colors.secondary.background}
|
||||
testID={notesnook.ids.tag.get(index)}
|
||||
alpha={!isDark ? -0.02 : 0.02}
|
||||
opacity={1}
|
||||
customStyle={{
|
||||
paddingHorizontal: 12,
|
||||
flexDirection: "row",
|
||||
paddingVertical: 12,
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
flexShrink: 1
|
||||
maxWidth: "92%"
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.md}>
|
||||
@@ -80,7 +66,7 @@ const TagItem = React.memo(
|
||||
>
|
||||
#
|
||||
</Heading>
|
||||
{item.title}
|
||||
{item.alias}
|
||||
</Heading>
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
@@ -89,10 +75,10 @@ const TagItem = React.memo(
|
||||
marginTop: 5
|
||||
}}
|
||||
>
|
||||
{totalNotes > 1
|
||||
? totalNotes + " notes"
|
||||
: totalNotes === 1
|
||||
? totalNotes + " note"
|
||||
{item && item.noteIds.length && item.noteIds.length > 1
|
||||
? item.noteIds.length + " notes"
|
||||
: item.noteIds.length === 1
|
||||
? item.noteIds.length + " note"
|
||||
: null}
|
||||
</Paragraph>
|
||||
</View>
|
||||
@@ -113,11 +99,14 @@ const TagItem = React.memo(
|
||||
alignItems: "center"
|
||||
}}
|
||||
/>
|
||||
</SelectionWrapper>
|
||||
</PressableButton>
|
||||
);
|
||||
},
|
||||
(prev, next) => {
|
||||
if (prev.item?.dateModified !== next.item?.dateModified) {
|
||||
if (prev.item?.dateEdited !== next.item?.dateEdited) {
|
||||
return false;
|
||||
}
|
||||
if (JSON.stringify(prev.item) !== JSON.stringify(next.item)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -27,15 +27,14 @@ import { SIZE } from "../../utils/size";
|
||||
import { PressableButton } from "../ui/pressable";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
|
||||
export const Card = ({ color }: { color?: string }) => {
|
||||
export const Card = ({ color, warning }) => {
|
||||
const { colors } = useThemeColors();
|
||||
color = color ? color : colors.primary.accent;
|
||||
const messageBoardState = useMessageStore((state) => state.message);
|
||||
const announcements = useMessageStore((state) => state.announcements);
|
||||
const announcement = useMessageStore((state) => state.announcement);
|
||||
const fontScale = Dimensions.get("window").fontScale;
|
||||
|
||||
return !messageBoardState.visible ||
|
||||
(announcements && announcements.length) ? null : (
|
||||
return !messageBoardState.visible || announcement || warning ? null : (
|
||||
<View
|
||||
style={{
|
||||
width: "95%"
|
||||
167
apps/mobile/app/components/list/empty.js
Normal file
167
apps/mobile/app/components/list/empty.js
Normal file
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
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 { ActivityIndicator, useWindowDimensions, View } from "react-native";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { useTip } from "../../services/tip-manager";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { ColorValues } from "../../utils/colors";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Tip } from "../tip";
|
||||
import { Button } from "../ui/button";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
|
||||
export const Empty = React.memo(
|
||||
function Empty({
|
||||
loading = true,
|
||||
placeholderData,
|
||||
headerProps,
|
||||
type,
|
||||
screen
|
||||
}) {
|
||||
const { colors } = useThemeColors();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const { height } = useWindowDimensions();
|
||||
const introCompleted = useSettingStore(
|
||||
(state) => state.settings.introCompleted
|
||||
);
|
||||
|
||||
const tip = useTip(
|
||||
screen === "Notes" && introCompleted
|
||||
? "first-note"
|
||||
: placeholderData.type || type,
|
||||
screen === "Notes" ? "notes" : null
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
height: height - (140 + insets.top),
|
||||
width: "80%",
|
||||
justifyContent: "center",
|
||||
alignSelf: "center"
|
||||
}
|
||||
]}
|
||||
>
|
||||
{!loading ? (
|
||||
<>
|
||||
<Tip
|
||||
color={
|
||||
ColorValues[headerProps.color?.toLowerCase()]
|
||||
? headerProps.color
|
||||
: "accent"
|
||||
}
|
||||
tip={tip || { text: placeholderData.paragraph }}
|
||||
style={{
|
||||
backgroundColor: "transparent",
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
/>
|
||||
{placeholderData.button && (
|
||||
<Button
|
||||
testID={notesnook.buttons.add}
|
||||
type="grayAccent"
|
||||
title={placeholderData.button}
|
||||
iconPosition="right"
|
||||
icon="arrow-right"
|
||||
onPress={placeholderData.action}
|
||||
buttonType={{
|
||||
text:
|
||||
colors.static[headerProps.color] || colors.primary.accent
|
||||
}}
|
||||
style={{
|
||||
alignSelf: "flex-start",
|
||||
borderRadius: 5,
|
||||
height: 40
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
alignItems: "flex-start",
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<Heading>{placeholderData.heading}</Heading>
|
||||
<Paragraph size={SIZE.sm} textBreakStrategy="balanced">
|
||||
{placeholderData.loading}
|
||||
</Paragraph>
|
||||
<Seperator />
|
||||
<ActivityIndicator
|
||||
size={SIZE.lg}
|
||||
color={
|
||||
ColorValues[headerProps.color?.toLowerCase()] ||
|
||||
colors.primary.accent
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
},
|
||||
(prev, next) => {
|
||||
if (prev.loading === next.loading) return true;
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Make a tips manager.
|
||||
* The tip manager stores many tips. Each tip has following values
|
||||
* 1. Text
|
||||
* 2. contexts: An array of context strings. // Places where the tip can be shown
|
||||
* 3. Button if any.
|
||||
* 4. Image/Gif asset.
|
||||
*
|
||||
* Tip manager adds the following methods -> get(context). Returns a random tip for the following context.
|
||||
*
|
||||
* Tips can be shown in a sheet or in a list. For sheets, GeneralSheet can be used to
|
||||
* render tips.
|
||||
*
|
||||
* Where can the tips be shown and how?
|
||||
* 1. When transitioning, show tips in a sheet. Make sure its useful
|
||||
* 2. Replace placeholders with tips.
|
||||
* 3. Show tips in editor placeholder.
|
||||
* 4. Show tips between list items?
|
||||
*
|
||||
* Tooltips.
|
||||
* Small tooltips can be shown in initial render first time.
|
||||
* Especially for items that are not shown on blank page. Should be
|
||||
* in places where it makes sense and does not interrupt the user.
|
||||
*
|
||||
* Can also be shown when first time entering a screen that
|
||||
* has something that the user might not know of. Like sorting and side menu.
|
||||
*
|
||||
* Todo:
|
||||
* 1. Make a tip manager.
|
||||
* 2. Make a list of tips.
|
||||
* 3. Add images for those tips.
|
||||
* 4. Show tips
|
||||
*/
|
||||
@@ -1,140 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { ActivityIndicator, useWindowDimensions, View } from "react-native";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { TTip, useTip } from "../../services/tip-manager";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Tip } from "../tip";
|
||||
import { Button } from "../ui/button";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
|
||||
export type PlaceholderData = {
|
||||
title: string;
|
||||
paragraph: string;
|
||||
button?: string;
|
||||
action?: () => void;
|
||||
loading?: string;
|
||||
type?: string;
|
||||
};
|
||||
|
||||
type EmptyListProps = {
|
||||
loading?: boolean;
|
||||
placeholder?: PlaceholderData;
|
||||
title?: string;
|
||||
color?: string;
|
||||
dataType: string;
|
||||
screen?: string;
|
||||
};
|
||||
|
||||
export const Empty = React.memo(function Empty({
|
||||
loading = true,
|
||||
placeholder,
|
||||
title,
|
||||
color,
|
||||
dataType,
|
||||
screen
|
||||
}: EmptyListProps) {
|
||||
const { colors } = useThemeColors();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const { height } = useWindowDimensions();
|
||||
const introCompleted = useSettingStore(
|
||||
(state) => state.settings.introCompleted
|
||||
);
|
||||
const tip = useTip(
|
||||
screen === "Notes" && introCompleted
|
||||
? "first-note"
|
||||
: placeholder?.type || ((dataType + "s") as any),
|
||||
screen === "Notes" ? "notes" : "list"
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
height: height - (140 + insets.top),
|
||||
width: "80%",
|
||||
justifyContent: "center",
|
||||
alignSelf: "center"
|
||||
}
|
||||
]}
|
||||
>
|
||||
{!loading ? (
|
||||
<>
|
||||
<Tip
|
||||
color={color}
|
||||
tip={
|
||||
screen !== "Search"
|
||||
? tip || ({ text: placeholder?.paragraph } as TTip)
|
||||
: ({ text: placeholder?.paragraph } as TTip)
|
||||
}
|
||||
style={{
|
||||
backgroundColor: "transparent",
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
/>
|
||||
{placeholder?.button && (
|
||||
<Button
|
||||
testID={notesnook.buttons.add}
|
||||
type="grayAccent"
|
||||
title={placeholder?.button}
|
||||
iconPosition="right"
|
||||
icon="arrow-right"
|
||||
onPress={placeholder?.action}
|
||||
buttonType={{
|
||||
text: color || colors.primary.accent
|
||||
}}
|
||||
style={{
|
||||
alignSelf: "flex-start",
|
||||
borderRadius: 5,
|
||||
height: 40
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
alignItems: "flex-start",
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<Heading>{placeholder?.title}</Heading>
|
||||
<Paragraph size={SIZE.sm} textBreakStrategy="balanced">
|
||||
{placeholder?.loading}
|
||||
</Paragraph>
|
||||
<Seperator />
|
||||
<ActivityIndicator
|
||||
size={SIZE.lg}
|
||||
color={color || colors.primary.accent}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
});
|
||||
@@ -17,171 +17,178 @@ 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 {
|
||||
GroupHeader,
|
||||
GroupingKey,
|
||||
Item,
|
||||
VirtualizedGrouping,
|
||||
isGroupHeader
|
||||
} from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { FlashList } from "@shopify/flash-list";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import {
|
||||
NativeScrollEvent,
|
||||
NativeSyntheticEvent,
|
||||
RefreshControl
|
||||
} from "react-native";
|
||||
import { RefreshControl, View } from "react-native";
|
||||
import { FlashList } from "@shopify/flash-list";
|
||||
import Animated, { FadeInDown } from "react-native-reanimated";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import { useGroupOptions } from "../../hooks/use-group-options";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import Sync from "../../services/sync";
|
||||
import { RouteName } from "../../stores/use-navigation-store";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { eOpenJumpToDialog, eScrollEvent } from "../../utils/events";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { db } from "../../common/database";
|
||||
import { eScrollEvent } from "../../utils/events";
|
||||
import { tabBarRef } from "../../utils/global-refs";
|
||||
import JumpToSectionDialog from "../dialogs/jump-to-section";
|
||||
import { Footer } from "../list-items/footer";
|
||||
import { Header } from "../list-items/headers/header";
|
||||
import { SectionHeader } from "../list-items/headers/section-header";
|
||||
import { Empty, PlaceholderData } from "./empty";
|
||||
import { ListItemWrapper } from "./list-item.wrapper";
|
||||
import { NoteWrapper } from "../list-items/note/wrapper";
|
||||
import { NotebookWrapper } from "../list-items/notebook/wrapper";
|
||||
import TagItem from "../list-items/tag";
|
||||
import { Empty } from "./empty";
|
||||
import { getTotalNotes } from "@notesnook/common";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import ReminderItem from "../list-items/reminder";
|
||||
import { useGroupOptions } from "../../hooks/use-group-options";
|
||||
|
||||
type ListProps = {
|
||||
data: VirtualizedGrouping<Item> | undefined;
|
||||
dataType: Item["type"];
|
||||
onRefresh?: () => void;
|
||||
loading?: boolean;
|
||||
headerTitle?: string;
|
||||
customAccentColor?: string;
|
||||
renderedInRoute?: RouteName;
|
||||
CustomLisHeader?: React.JSX.Element;
|
||||
isRenderedInActionSheet?: boolean;
|
||||
CustomListComponent?: React.JSX.ElementType;
|
||||
placeholder?: PlaceholderData;
|
||||
const renderItems = {
|
||||
note: NoteWrapper,
|
||||
notebook: NotebookWrapper,
|
||||
topic: NotebookWrapper,
|
||||
tag: TagItem,
|
||||
section: SectionHeader,
|
||||
header: SectionHeader,
|
||||
reminder: ReminderItem
|
||||
};
|
||||
|
||||
export default function List(props: ListProps) {
|
||||
const RenderItem = ({ item, index, type, ...restArgs }) => {
|
||||
if (!item) return <View />;
|
||||
const Item = renderItems[item.itemType || item.type] || View;
|
||||
const totalNotes = getTotalNotes(item);
|
||||
return (
|
||||
<Item
|
||||
item={item}
|
||||
index={index}
|
||||
type={type}
|
||||
totalNotes={totalNotes}
|
||||
{...restArgs}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {any} param0
|
||||
* @returns
|
||||
*/
|
||||
const List = ({
|
||||
listData,
|
||||
type,
|
||||
refreshCallback,
|
||||
placeholderData,
|
||||
loading,
|
||||
headerProps = {
|
||||
heading: "Home",
|
||||
color: null
|
||||
},
|
||||
screen,
|
||||
ListHeader,
|
||||
warning,
|
||||
isSheet = false,
|
||||
onMomentumScrollEnd,
|
||||
handlers,
|
||||
ScrollComponent
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const scrollRef = useRef();
|
||||
const [notesListMode, notebooksListMode] = useSettingStore((state) => [
|
||||
state.settings.notesListMode,
|
||||
state.settings.notebooksListMode
|
||||
]);
|
||||
|
||||
const isCompactModeEnabled =
|
||||
(props.dataType === "note" && notesListMode === "compact") ||
|
||||
props.dataType === "notebook" ||
|
||||
(type === "notes" && notesListMode === "compact") ||
|
||||
type === "notebooks" ||
|
||||
notebooksListMode === "compact";
|
||||
|
||||
const groupType =
|
||||
props.renderedInRoute === "Notes"
|
||||
? "home"
|
||||
: props.renderedInRoute === "Favorites"
|
||||
? "favorites"
|
||||
: props.renderedInRoute === "Trash"
|
||||
? "trash"
|
||||
: `${props.dataType}s`;
|
||||
screen === "Home" ? "home" : screen === "Favorites" ? "favorites" : type;
|
||||
|
||||
const groupOptions = useGroupOptions(groupType);
|
||||
|
||||
const _onRefresh = async () => {
|
||||
Sync.run("global", false, true, () => {
|
||||
props.onRefresh?.();
|
||||
});
|
||||
};
|
||||
const dateBy =
|
||||
groupOptions.sortBy !== "title" ? groupOptions.sortBy : "dateEdited";
|
||||
|
||||
const renderItem = React.useCallback(
|
||||
({ item, index }: { item: string | GroupHeader; index: number }) => {
|
||||
if (isGroupHeader(item)) {
|
||||
return (
|
||||
<SectionHeader
|
||||
screen={props.renderedInRoute}
|
||||
item={item}
|
||||
index={index}
|
||||
dataType={props.dataType}
|
||||
color={props.customAccentColor}
|
||||
groupOptions={groupOptions}
|
||||
onOpenJumpToDialog={() => {
|
||||
eSendEvent(eOpenJumpToDialog, {
|
||||
ref: scrollRef,
|
||||
data: props.data
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<ListItemWrapper
|
||||
id={item}
|
||||
index={index}
|
||||
isSheet={props.isRenderedInActionSheet || false}
|
||||
items={props.data}
|
||||
group={groupType as GroupingKey}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
({ item, index }) => (
|
||||
<RenderItem
|
||||
item={item}
|
||||
index={index}
|
||||
color={headerProps?.color}
|
||||
title={headerProps?.heading}
|
||||
dateBy={dateBy}
|
||||
type={groupType}
|
||||
screen={screen}
|
||||
isSheet={isSheet}
|
||||
groupOptions={groupOptions}
|
||||
/>
|
||||
),
|
||||
[
|
||||
groupOptions,
|
||||
headerProps?.color,
|
||||
headerProps?.heading,
|
||||
screen,
|
||||
isSheet,
|
||||
dateBy,
|
||||
groupType,
|
||||
props.customAccentColor,
|
||||
props.data,
|
||||
props.dataType,
|
||||
props.isRenderedInActionSheet,
|
||||
props.renderedInRoute
|
||||
groupOptions
|
||||
]
|
||||
);
|
||||
|
||||
const onListScroll = React.useCallback(
|
||||
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
const _onRefresh = async () => {
|
||||
Sync.run("global", false, true, () => {
|
||||
if (refreshCallback) {
|
||||
refreshCallback();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const _onScroll = React.useCallback(
|
||||
(event) => {
|
||||
if (!event) return;
|
||||
let y = event.nativeEvent.contentOffset.y;
|
||||
eSendEvent(eScrollEvent, {
|
||||
y: event.nativeEvent.contentOffset.y,
|
||||
screen: props.renderedInRoute
|
||||
y,
|
||||
screen
|
||||
});
|
||||
},
|
||||
[props.renderedInRoute]
|
||||
[screen]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
eSendEvent(eScrollEvent, {
|
||||
y: 0,
|
||||
screen: props.renderedInRoute
|
||||
screen
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const styles = {
|
||||
let styles = {
|
||||
width: "100%",
|
||||
minHeight: 1,
|
||||
minWidth: 1
|
||||
};
|
||||
|
||||
const ListView = props.CustomListComponent
|
||||
? props.CustomListComponent
|
||||
: FlashList;
|
||||
|
||||
const ListView = ScrollComponent ? ScrollComponent : FlashList;
|
||||
return (
|
||||
<>
|
||||
<Animated.View
|
||||
style={{
|
||||
flex: 1
|
||||
}}
|
||||
entering={props.renderedInRoute === "Search" ? undefined : FadeInDown}
|
||||
entering={type === "search" ? undefined : FadeInDown}
|
||||
>
|
||||
<ListView
|
||||
{...handlers}
|
||||
style={styles}
|
||||
ref={scrollRef}
|
||||
testID={notesnook.list.id}
|
||||
data={props.data?.ids || []}
|
||||
data={listData}
|
||||
renderItem={renderItem}
|
||||
onScroll={onListScroll}
|
||||
onScroll={_onScroll}
|
||||
nestedScrollEnabled={true}
|
||||
onMomentumScrollEnd={() => {
|
||||
tabBarRef.current?.unlock();
|
||||
onMomentumScrollEnd?.();
|
||||
}}
|
||||
getItemType={(item: any) => (isGroupHeader(item) ? "header" : "item")}
|
||||
getItemType={(item) => item.itemType || item.type}
|
||||
estimatedItemSize={isCompactModeEnabled ? 60 : 100}
|
||||
directionalLockEnabled={true}
|
||||
keyboardShouldPersistTaps="always"
|
||||
@@ -196,32 +203,44 @@ export default function List(props: ListProps) {
|
||||
/>
|
||||
}
|
||||
ListEmptyComponent={
|
||||
props.placeholder ? (
|
||||
placeholderData ? (
|
||||
<Empty
|
||||
loading={props.loading}
|
||||
title={props.headerTitle}
|
||||
dataType={props.dataType}
|
||||
color={props.customAccentColor}
|
||||
placeholder={props.placeholder}
|
||||
screen={props.renderedInRoute}
|
||||
loading={loading}
|
||||
placeholderData={placeholderData}
|
||||
headerProps={headerProps}
|
||||
type={type}
|
||||
screen={screen}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
ListFooterComponent={<Footer />}
|
||||
ListHeaderComponent={
|
||||
<>
|
||||
{props.CustomLisHeader ? (
|
||||
props.CustomLisHeader
|
||||
) : !props.headerTitle ? null : (
|
||||
{ListHeader ? (
|
||||
ListHeader
|
||||
) : !headerProps ? null : (
|
||||
<Header
|
||||
color={props.customAccentColor}
|
||||
screen={props.renderedInRoute}
|
||||
title={headerProps.heading}
|
||||
color={headerProps.color}
|
||||
type={type}
|
||||
screen={screen}
|
||||
warning={warning}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Animated.View>
|
||||
{listData ? (
|
||||
<JumpToSectionDialog
|
||||
screen={screen}
|
||||
data={listData}
|
||||
type={screen === "Home" ? "home" : type}
|
||||
scrollRef={scrollRef}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default List;
|
||||
@@ -1,269 +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 from "react";
|
||||
import { getSortValue } from "@notesnook/core/dist/utils/grouping";
|
||||
import {
|
||||
GroupingKey,
|
||||
Item,
|
||||
VirtualizedGrouping,
|
||||
Color,
|
||||
Reminder,
|
||||
Notebook,
|
||||
Tag,
|
||||
TrashItem,
|
||||
ItemType,
|
||||
Note
|
||||
} from "@notesnook/core";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { NoteWrapper } from "../list-items/note/wrapper";
|
||||
import { db } from "../../common/database";
|
||||
import { NotebookWrapper } from "../list-items/notebook/wrapper";
|
||||
import ReminderItem from "../list-items/reminder";
|
||||
import TagItem from "../list-items/tag";
|
||||
|
||||
export type WithDateEdited<T> = { items: T[]; dateEdited: number };
|
||||
export type NotebooksWithDateEdited = WithDateEdited<Notebook>;
|
||||
export type TagsWithDateEdited = WithDateEdited<Tag>;
|
||||
|
||||
type ListItemWrapperProps<TItem = Item> = {
|
||||
group?: GroupingKey;
|
||||
items: VirtualizedGrouping<TItem> | undefined;
|
||||
id: string;
|
||||
isSheet: boolean;
|
||||
index: number;
|
||||
};
|
||||
|
||||
export function ListItemWrapper(props: ListItemWrapperProps) {
|
||||
const { id, items, group, isSheet, index } = props;
|
||||
const [item, setItem] = useState<Item>();
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
const { item, data } = (await items?.item(id, resolveItems)) || {};
|
||||
if (!item) return;
|
||||
if (item.type === "note" && isNoteResolvedData(data)) {
|
||||
tags.current = data.tags;
|
||||
notebooks.current = data.notebooks;
|
||||
reminder.current = data.reminder;
|
||||
color.current = data.color;
|
||||
attachmentsCount.current = data.attachmentsCount;
|
||||
} else if (item.type === "notebook" && typeof data === "number") {
|
||||
totalNotes.current = data;
|
||||
} else if (item.type === "tag" && typeof data === "number") {
|
||||
totalNotes.current = data;
|
||||
}
|
||||
setItem(item);
|
||||
})();
|
||||
}, [id, items]);
|
||||
|
||||
if (!item) return <View style={{ height: 100, width: "100%" }} />;
|
||||
|
||||
const type = ((item as TrashItem).itemType || item.type) as ItemType;
|
||||
switch (type) {
|
||||
case "note": {
|
||||
return (
|
||||
<NoteWrapper
|
||||
item={item as Note}
|
||||
tags={tags.current}
|
||||
color={color.current}
|
||||
notebooks={notebooks.current}
|
||||
reminder={reminder.current}
|
||||
attachmentsCount={attachmentsCount.current}
|
||||
date={getDate(item, group)}
|
||||
isRenderedInActionSheet={isSheet}
|
||||
index={index}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "notebook":
|
||||
return (
|
||||
<NotebookWrapper
|
||||
item={item as Notebook}
|
||||
totalNotes={totalNotes.current}
|
||||
date={getDate(item, group)}
|
||||
index={index}
|
||||
/>
|
||||
);
|
||||
|
||||
case "reminder":
|
||||
return (
|
||||
<ReminderItem item={item as Reminder} index={index} isSheet={isSheet} />
|
||||
);
|
||||
case "tag":
|
||||
return (
|
||||
<TagItem
|
||||
item={item as Tag}
|
||||
index={index}
|
||||
totalNotes={totalNotes.current}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function withDateEdited<
|
||||
T extends { dateEdited: number } | { dateModified: number }
|
||||
>(items: T[]): WithDateEdited<T> {
|
||||
let latestDateEdited = 0;
|
||||
items.forEach((item) => {
|
||||
const date = "dateEdited" in item ? item.dateEdited : item.dateModified;
|
||||
if (latestDateEdited < date) latestDateEdited = date;
|
||||
});
|
||||
return { dateEdited: latestDateEdited, items };
|
||||
}
|
||||
|
||||
function getDate(item: Item, groupType?: GroupingKey): number {
|
||||
return (
|
||||
getSortValue(
|
||||
groupType
|
||||
? db.settings.getGroupOptions(groupType)
|
||||
: {
|
||||
groupBy: "default",
|
||||
sortBy: "dateEdited",
|
||||
sortDirection: "desc"
|
||||
},
|
||||
item
|
||||
) || 0
|
||||
);
|
||||
}
|
||||
|
||||
async function resolveItems(ids: string[], items: Record<string, Item>) {
|
||||
const { type } = items[ids[0]];
|
||||
if (type === "note") return resolveNotes(ids);
|
||||
else if (type === "notebook") {
|
||||
const data: Record<string, number> = {};
|
||||
for (const id of ids) data[id] = await db.notebooks.totalNotes(id);
|
||||
return data;
|
||||
} else if (type === "tag") {
|
||||
const data: Record<string, number> = {};
|
||||
for (const id of ids)
|
||||
data[id] = await db.relations.from({ id, type: "tag" }, "note").count();
|
||||
return data;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
type NoteResolvedData = {
|
||||
notebooks?: NotebooksWithDateEdited;
|
||||
reminder?: Reminder;
|
||||
color?: Color;
|
||||
tags?: TagsWithDateEdited;
|
||||
attachmentsCount: number;
|
||||
};
|
||||
async function resolveNotes(ids: string[]) {
|
||||
console.time("relations");
|
||||
const relations = [
|
||||
...(await db.relations
|
||||
.to({ type: "note", ids }, ["notebook", "tag", "color"])
|
||||
.get()),
|
||||
...(await db.relations.from({ type: "note", ids }, "reminder").get())
|
||||
];
|
||||
console.timeEnd("relations");
|
||||
|
||||
const relationIds: {
|
||||
notebooks: Set<string>;
|
||||
colors: Set<string>;
|
||||
tags: Set<string>;
|
||||
reminders: Set<string>;
|
||||
} = {
|
||||
colors: new Set(),
|
||||
notebooks: new Set(),
|
||||
tags: new Set(),
|
||||
reminders: new Set()
|
||||
};
|
||||
|
||||
const grouped: Record<
|
||||
string,
|
||||
{
|
||||
notebooks: string[];
|
||||
color?: string;
|
||||
tags: string[];
|
||||
reminder?: string;
|
||||
}
|
||||
> = {};
|
||||
for (const relation of relations) {
|
||||
const noteId =
|
||||
relation.toType === "relation" ? relation.fromId : relation.toId;
|
||||
const data = grouped[noteId] || {
|
||||
notebooks: [],
|
||||
tags: []
|
||||
};
|
||||
|
||||
if (relation.toType === "relation" && !data.reminder) {
|
||||
data.reminder = relation.fromId;
|
||||
relationIds.reminders.add(relation.fromId);
|
||||
} else if (relation.fromType === "notebook" && data.notebooks.length < 2) {
|
||||
data.notebooks.push(relation.fromId);
|
||||
relationIds.notebooks.add(relation.fromId);
|
||||
} else if (relation.fromType === "tag" && data.tags.length < 3) {
|
||||
data.tags.push(relation.fromId);
|
||||
relationIds.tags.add(relation.fromId);
|
||||
} else if (relation.fromType === "color" && !data.color) {
|
||||
data.color = relation.fromId;
|
||||
relationIds.colors.add(relation.fromId);
|
||||
}
|
||||
grouped[relation.toId] = data;
|
||||
}
|
||||
|
||||
console.time("resolve");
|
||||
const resolved = {
|
||||
notebooks: await db.notebooks.all.records(
|
||||
Array.from(relationIds.notebooks)
|
||||
),
|
||||
tags: await db.tags.all.records(Array.from(relationIds.tags)),
|
||||
colors: await db.colors.all.records(Array.from(relationIds.colors)),
|
||||
reminders: await db.reminders.all.records(Array.from(relationIds.reminders))
|
||||
};
|
||||
console.timeEnd("resolve");
|
||||
|
||||
const data: Record<string, NoteResolvedData> = {};
|
||||
for (const noteId in grouped) {
|
||||
const group = grouped[noteId];
|
||||
data[noteId] = {
|
||||
color: group.color ? resolved.colors[group.color] : undefined,
|
||||
reminder: group.reminder ? resolved.reminders[group.reminder] : undefined,
|
||||
tags: withDateEdited(group.tags.map((id) => resolved.tags[id])),
|
||||
notebooks: withDateEdited(
|
||||
group.notebooks.map((id) => resolved.notebooks[id])
|
||||
),
|
||||
attachmentsCount:
|
||||
(await db.attachments?.ofNote(noteId, "all").ids())?.length || 0
|
||||
};
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function isNoteResolvedData(data: unknown): data is NoteResolvedData {
|
||||
return (
|
||||
typeof data === "object" &&
|
||||
!!data &&
|
||||
"notebooks" in data &&
|
||||
"reminder" in data &&
|
||||
"color" in data &&
|
||||
"tags" in data
|
||||
);
|
||||
}
|
||||
@@ -91,7 +91,7 @@ const MergeConflicts = () => {
|
||||
if (editorController.current?.note?.id === note.id) {
|
||||
// reload the note in editor
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: editorController.current?.note,
|
||||
...editorController.current?.note,
|
||||
forced: true
|
||||
});
|
||||
}
|
||||
@@ -324,12 +324,10 @@ const MergeConflicts = () => {
|
||||
if (!note) return;
|
||||
await sleep(300);
|
||||
eSendEvent(eOnLoadNote + ":conflictPrimary", {
|
||||
item: {
|
||||
...note,
|
||||
content: {
|
||||
...content.current,
|
||||
isPreview: true
|
||||
}
|
||||
...note,
|
||||
content: {
|
||||
...content.current,
|
||||
isPreview: true
|
||||
}
|
||||
});
|
||||
}}
|
||||
@@ -361,10 +359,8 @@ const MergeConflicts = () => {
|
||||
if (!note) return;
|
||||
await sleep(300);
|
||||
eSendEvent(eOnLoadNote + ":conflictSecondary", {
|
||||
item: {
|
||||
...note,
|
||||
content: { ...content.current.conflicted, isPreview: true }
|
||||
}
|
||||
...note,
|
||||
content: { ...content.current.conflicted, isPreview: true }
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -17,29 +17,26 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import Editor from "../../screens/editor";
|
||||
import EditorOverlay from "../../screens/editor/loading";
|
||||
import { editorController } from "../../screens/editor/tiptap/utils";
|
||||
import { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||
import { eSendEvent, ToastEvent } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { useEditorStore } from "../../stores/use-editor-store";
|
||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useTrashStore } from "../../stores/use-trash-store";
|
||||
import { eCloseSheet, eOnLoadNote } from "../../utils/events";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Dialog } from "../dialog";
|
||||
import DialogHeader from "../dialog/dialog-header";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import { Button } from "../ui/button";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {any} param0
|
||||
* @returns
|
||||
*/
|
||||
export default function NotePreview({ session, content, note }) {
|
||||
const { colors } = useThemeColors();
|
||||
const editorId = ":noteHistory";
|
||||
@@ -49,7 +46,7 @@ export default function NotePreview({ session, content, note }) {
|
||||
await db.trash.restore(note.id);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useSelectionStore.getState().setSelectionMode(false);
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Restore successful",
|
||||
type: "success"
|
||||
});
|
||||
@@ -60,7 +57,7 @@ export default function NotePreview({ session, content, note }) {
|
||||
if (useEditorStore.getState()?.currentEditingNote === session?.noteId) {
|
||||
if (editorController.current?.note) {
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: editorController.current?.note,
|
||||
...editorController.current?.note,
|
||||
forced: true
|
||||
});
|
||||
}
|
||||
@@ -69,7 +66,7 @@ export default function NotePreview({ session, content, note }) {
|
||||
eSendEvent(eCloseSheet);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Note restored successfully",
|
||||
type: "success"
|
||||
});
|
||||
@@ -86,7 +83,7 @@ export default function NotePreview({ session, content, note }) {
|
||||
await db.trash.delete(note.id);
|
||||
useTrashStore.getState().setTrash();
|
||||
useSelectionStore.getState().setSelectionMode(false);
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Permanently deleted items",
|
||||
type: "success",
|
||||
context: "local"
|
||||
@@ -120,12 +117,10 @@ export default function NotePreview({ session, content, note }) {
|
||||
onLoad={async () => {
|
||||
const _note = note || db.notes.note(session?.noteId)?.data;
|
||||
eSendEvent(eOnLoadNote + editorId, {
|
||||
item: {
|
||||
..._note,
|
||||
content: {
|
||||
...content,
|
||||
isPreview: true
|
||||
}
|
||||
..._note,
|
||||
content: {
|
||||
...content,
|
||||
isPreview: true
|
||||
}
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -25,7 +25,7 @@ import { usePricing } from "../../hooks/use-pricing";
|
||||
import {
|
||||
eSendEvent,
|
||||
presentSheet,
|
||||
ToastManager
|
||||
ToastEvent
|
||||
} from "../../services/event-manager";
|
||||
import PremiumService from "../../services/premium";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
@@ -517,7 +517,7 @@ export const PricingPlans = ({
|
||||
try {
|
||||
if (!(await getPromo(value as string)))
|
||||
throw new Error("Error applying promo code");
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Discount applied!",
|
||||
type: "success",
|
||||
context: "local"
|
||||
@@ -525,7 +525,7 @@ export const PricingPlans = ({
|
||||
setBuying(false);
|
||||
} catch (e) {
|
||||
setBuying(false);
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Promo code invalid or expired",
|
||||
message: (e as Error).message,
|
||||
type: "error",
|
||||
|
||||
@@ -23,11 +23,6 @@ import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {any} param0
|
||||
* @returns
|
||||
*/
|
||||
export const ProTag = ({ width, size, background }) => {
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
|
||||
@@ -17,10 +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 { DefaultColors } from "@notesnook/core/dist/collections/colors";
|
||||
import { Color, ItemReference, Note } from "@notesnook/core/dist/types";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
@@ -28,56 +25,45 @@ import { db } from "../../common/database";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { useMenuStore } from "../../stores/use-menu-store";
|
||||
import { useRelationStore } from "../../stores/use-relation-store";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { ColorValues } from "../../utils/colors";
|
||||
import { refreshNotesPage } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { PressableButton } from "../ui/pressable";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
|
||||
export const ColorTags = ({ item }: { item: Note }) => {
|
||||
export const ColorTags = ({ item }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [note, setNote] = useState(item);
|
||||
const setColorNotes = useMenuStore((state) => state.setColorNotes);
|
||||
const isTablet = useSettingStore((state) => state.deviceMode) !== "mobile";
|
||||
const updater = useRelationStore((state) => state.updater);
|
||||
|
||||
const getColorInfo = async (colorCode: string) => {
|
||||
const dbColor = await db.colors.all.find((v) =>
|
||||
v.and([v(`colorCode`, "==", colorCode)])
|
||||
);
|
||||
let isLinked = false;
|
||||
|
||||
if (dbColor) {
|
||||
const hasRelation = await db.relations.from(dbColor, "note").has(item.id);
|
||||
if (hasRelation) {
|
||||
isLinked = true;
|
||||
}
|
||||
const changeColor = async (color) => {
|
||||
if (note.color === color.name) {
|
||||
await db.notes.note(note.id).uncolor();
|
||||
} else {
|
||||
await db.notes.note(note.id).color(color.name);
|
||||
}
|
||||
|
||||
return {
|
||||
linked: isLinked,
|
||||
item: dbColor
|
||||
};
|
||||
let _note = db.notes.note(note.id).data;
|
||||
setNote({ ..._note });
|
||||
setColorNotes();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
eSendEvent(refreshNotesPage);
|
||||
};
|
||||
|
||||
const ColorItem = ({ name }: { name: keyof typeof DefaultColors }) => {
|
||||
const color = DefaultColors[name];
|
||||
const [colorInfo, setColorInfo] = useState<{
|
||||
linked: boolean;
|
||||
item: Color | undefined;
|
||||
}>();
|
||||
|
||||
useEffect(() => {
|
||||
getColorInfo(color).then((info) => setColorInfo(info));
|
||||
}, [color]);
|
||||
const _renderColor = (c) => {
|
||||
const color = {
|
||||
name: c,
|
||||
value: ColorValues[c?.toLowerCase()]
|
||||
};
|
||||
|
||||
return (
|
||||
<PressableButton
|
||||
type="accent"
|
||||
accentColor={color}
|
||||
accentColor={colors.static[color.name?.toLowerCase()]}
|
||||
accentText={colors.static.white}
|
||||
testID={notesnook.ids.dialogs.actionsheet.color(name)}
|
||||
key={color}
|
||||
onPress={() => changeColor(name)}
|
||||
testID={notesnook.ids.dialogs.actionsheet.color(c)}
|
||||
key={color.value}
|
||||
onPress={() => changeColor(color)}
|
||||
customStyle={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
@@ -87,40 +73,13 @@ export const ColorTags = ({ item }: { item: Note }) => {
|
||||
marginRight: isTablet ? 10 : undefined
|
||||
}}
|
||||
>
|
||||
{colorInfo?.linked ? (
|
||||
{note.color?.toLowerCase() === color.name ? (
|
||||
<Icon testID="icon-check" name="check" color="white" size={SIZE.lg} />
|
||||
) : null}
|
||||
</PressableButton>
|
||||
);
|
||||
};
|
||||
|
||||
const changeColor = async (color: string) => {
|
||||
const colorInfo = await getColorInfo(DefaultColors[color]);
|
||||
|
||||
if (colorInfo.item) {
|
||||
if (colorInfo.linked) {
|
||||
await db.relations.unlink(colorInfo.item, item);
|
||||
} else {
|
||||
await db.relations.add(colorInfo.item, item);
|
||||
}
|
||||
} else {
|
||||
const colorId = await db.colors.add({
|
||||
title: color,
|
||||
colorCode: DefaultColors[color]
|
||||
});
|
||||
|
||||
const dbColor = await db.colors.color(colorId);
|
||||
if (dbColor) {
|
||||
await db.relations.add(dbColor as unknown as ItemReference, item);
|
||||
}
|
||||
}
|
||||
|
||||
useRelationStore.getState().update();
|
||||
setColorNotes();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
eSendEvent(refreshNotesPage);
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
@@ -133,9 +92,7 @@ export const ColorTags = ({ item }: { item: Note }) => {
|
||||
justifyContent: isTablet ? "center" : "space-between"
|
||||
}}
|
||||
>
|
||||
{Object.keys(DefaultColors).map((name: keyof typeof DefaultColors) => {
|
||||
return <ColorItem key={name} name={name} />;
|
||||
})}
|
||||
{Object.keys(ColorValues).map(_renderColor)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -53,24 +53,23 @@ export const DateMeta = ({ item }) => {
|
||||
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: 3
|
||||
}}
|
||||
>
|
||||
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
|
||||
{getNameFromKey(key)}
|
||||
</Paragraph>
|
||||
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
|
||||
{getFormattedDate(item[key], "date-time")}
|
||||
</Paragraph>
|
||||
</View>
|
||||
);
|
||||
const renderItem = (key) => (
|
||||
<View
|
||||
key={key}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
paddingVertical: 3
|
||||
}}
|
||||
>
|
||||
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
|
||||
{getNameFromKey(key)}
|
||||
</Paragraph>
|
||||
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
|
||||
{getFormattedDate(item[key], "date-time")}
|
||||
</Paragraph>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
|
||||
@@ -16,13 +16,14 @@ 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 from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import React, { useRef } from "react";
|
||||
import { Dimensions, Platform, View, useWindowDimensions } from "react-native";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import { db } from "../../common/database";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { presentSheet } from "../../services/event-manager";
|
||||
import SearchService from "../../services/search";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { ColorValues } from "../../utils/colors";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
@@ -33,8 +34,7 @@ import { DateMeta } from "./date-meta";
|
||||
import { Items } from "./items";
|
||||
import Notebooks from "./notebooks";
|
||||
import { Synced } from "./synced";
|
||||
import { TagStrip, Tags } from "./tags";
|
||||
|
||||
import { Tags, TagStrip } from "./tags";
|
||||
const Line = ({ top = 6, bottom = 6 }) => {
|
||||
const { colors } = useThemeColors();
|
||||
return (
|
||||
@@ -52,6 +52,7 @@ const Line = ({ top = 6, bottom = 6 }) => {
|
||||
|
||||
export const Properties = ({ close = () => {}, item, buttons = [] }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const alias = item.alias || item.title;
|
||||
const isColor = !!ColorValues[item.title];
|
||||
if (!item || !item.id) {
|
||||
return (
|
||||
@@ -89,13 +90,9 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
|
||||
#
|
||||
</Heading>
|
||||
) : null}
|
||||
{item.title}
|
||||
{alias}
|
||||
</Heading>
|
||||
|
||||
{item.type === "notebook" && item.description ? (
|
||||
<Paragraph>{item.description}</Paragraph>
|
||||
) : null}
|
||||
|
||||
{item.type === "note" ? (
|
||||
<TagStrip close={close} item={item} />
|
||||
) : null}
|
||||
@@ -133,6 +130,9 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
|
||||
buttons={buttons}
|
||||
close={() => {
|
||||
close();
|
||||
setTimeout(() => {
|
||||
SearchService.updateAndSearch();
|
||||
}, 1000);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -152,7 +152,7 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
|
||||
);
|
||||
};
|
||||
|
||||
Properties.present = async (item, buttons = [], isSheet) => {
|
||||
Properties.present = (item, buttons = [], isSheet) => {
|
||||
if (!item) return;
|
||||
let type = item?.type;
|
||||
let props = [];
|
||||
@@ -164,7 +164,7 @@ Properties.present = async (item, buttons = [], isSheet) => {
|
||||
break;
|
||||
case "note":
|
||||
android = Platform.OS === "android" ? ["pin-to-notifications"] : [];
|
||||
props[0] = await db.notes.note(item.id);
|
||||
props[0] = db.notes.note(item.id).data;
|
||||
props.push([
|
||||
"notebooks",
|
||||
"add-reminder",
|
||||
@@ -189,34 +189,33 @@ Properties.present = async (item, buttons = [], isSheet) => {
|
||||
]);
|
||||
break;
|
||||
case "notebook":
|
||||
props[0] = await db.notebooks.notebook(item.id);
|
||||
props[0] = db.notebooks.notebook(item.id).data;
|
||||
props.push([
|
||||
"edit-notebook",
|
||||
"pin",
|
||||
"add-shortcut",
|
||||
"trash",
|
||||
"default-notebook",
|
||||
"add-notebook"
|
||||
"default-notebook"
|
||||
]);
|
||||
break;
|
||||
case "topic":
|
||||
props[0] = db.notebooks
|
||||
.notebook(item.notebookId)
|
||||
.topics.topic(item.id)._topic;
|
||||
props.push([
|
||||
"move-notes",
|
||||
"edit-topic",
|
||||
"add-shortcut",
|
||||
"trash",
|
||||
"default-topic"
|
||||
]);
|
||||
break;
|
||||
// case "topic":
|
||||
// props[0] = db.notebooks
|
||||
// .notebook(item.notebookId)
|
||||
// .topics.topic(item.id)._topic;
|
||||
// props.push([
|
||||
// "move-notes",
|
||||
// "edit-topic",
|
||||
// "add-shortcut",
|
||||
// "trash",
|
||||
// "default-topic"
|
||||
// ]);
|
||||
// break;
|
||||
case "tag":
|
||||
props[0] = await db.tags.tag(item.id);
|
||||
props[0] = db.tags.tag(item.id);
|
||||
props.push(["add-shortcut", "trash", "rename-tag"]);
|
||||
break;
|
||||
case "reminder": {
|
||||
props[0] = await db.reminders.reminder(item.id);
|
||||
props[0] = db.reminders.reminder(item.id);
|
||||
props.push(["edit-reminder", "trash", "disable-reminder"]);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import { PressableButton } from "../ui/pressable";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
|
||||
export const Items = ({ item, buttons, close }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const dimensions = useSettingStore((state) => state.dimensions);
|
||||
@@ -178,37 +177,17 @@ export const Items = ({ item, buttons, close }) => {
|
||||
"export",
|
||||
"lock-unlock"
|
||||
];
|
||||
|
||||
if (!shouldShrink) {
|
||||
topBarItemsList.push("publish");
|
||||
}
|
||||
|
||||
const topBarItems = data
|
||||
.filter((item) => topBarItemsList.indexOf(item.id) > -1)
|
||||
.sort((a, b) =>
|
||||
topBarItemsList.indexOf(a.id) > topBarItemsList.indexOf(b.id) ? 1 : -1
|
||||
);
|
||||
const topBarItems = data.filter(
|
||||
(item) => topBarItemsList.indexOf(item.id) > -1
|
||||
);
|
||||
|
||||
const bottomBarItemsList = [
|
||||
"notebooks",
|
||||
"add-reminder",
|
||||
"pin-to-notifications",
|
||||
"duplicate",
|
||||
"read-only",
|
||||
"local-only",
|
||||
"history",
|
||||
"reminders",
|
||||
"attachments",
|
||||
"trash"
|
||||
];
|
||||
|
||||
const bottomGridItems = data
|
||||
.filter((item) => bottomBarItemsList.indexOf(item.id) > -1)
|
||||
.sort((a, b) =>
|
||||
bottomBarItemsList.indexOf(a.id) > bottomBarItemsList.indexOf(b.id)
|
||||
? 1
|
||||
: -1
|
||||
);
|
||||
const bottomGridItems = data.filter(
|
||||
(item) => topBarItemsList.indexOf(item.id) === -1
|
||||
);
|
||||
|
||||
const topBarItemWidth =
|
||||
(width - (topBarItems.length * 10 + 14)) / topBarItems.length;
|
||||
|
||||
@@ -17,46 +17,76 @@ 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, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import React from "react";
|
||||
import { ScrollView, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../common/database";
|
||||
import NotebookScreen from "../../screens/notebook";
|
||||
import { eSendEvent, presentSheet } from "../../services/event-manager";
|
||||
import { eClearEditor } from "../../utils/events";
|
||||
import Notebook from "../../screens/notebook";
|
||||
import { TopicNotes } from "../../screens/notes/topic-notes";
|
||||
import {
|
||||
eSendEvent,
|
||||
presentSheet,
|
||||
ToastEvent
|
||||
} from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { useNotebookStore } from "../../stores/use-notebook-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import { PressableButton } from "../ui/pressable";
|
||||
import { eClearEditor } from "../../utils/events";
|
||||
|
||||
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(() => {
|
||||
getNotebooks().then((notebooks) => setNoteNotebooks(notebooks));
|
||||
});
|
||||
const notebooks = useNotebookStore((state) => state.notebooks);
|
||||
function getNotebooks(item) {
|
||||
let filteredNotebooks = [];
|
||||
const relations = db.relations.to(note, "notebook");
|
||||
filteredNotebooks.push(
|
||||
...relations.map((notebook) => ({
|
||||
...notebook,
|
||||
topics: []
|
||||
}))
|
||||
);
|
||||
if (!item.notebooks || item.notebooks.length < 1) return filteredNotebooks;
|
||||
|
||||
const navigateNotebook = async (id) => {
|
||||
let item = await db.notebooks.notebook(id);
|
||||
for (let notebookReference of item.notebooks) {
|
||||
let notebook = {
|
||||
...(notebooks.find((item) => item.id === notebookReference.id) || {})
|
||||
};
|
||||
if (notebook.id) {
|
||||
notebook.topics = notebook.topics.filter((topic) => {
|
||||
return notebookReference.topics.findIndex((t) => t === topic.id) > -1;
|
||||
});
|
||||
const index = filteredNotebooks.findIndex(
|
||||
(item) => item.id === notebook.id
|
||||
);
|
||||
if (index > -1) {
|
||||
filteredNotebooks[index].topics = notebook.topics;
|
||||
} else {
|
||||
filteredNotebooks.push(notebook);
|
||||
}
|
||||
}
|
||||
}
|
||||
return filteredNotebooks;
|
||||
}
|
||||
const noteNotebooks = getNotebooks(note);
|
||||
|
||||
const navigateNotebook = (id) => {
|
||||
let item = db.notebooks.notebook(id)?.data;
|
||||
if (!item) return;
|
||||
NotebookScreen.navigate(item, true);
|
||||
Notebook.navigate(item, true);
|
||||
};
|
||||
|
||||
const navigateTopic = (id, notebookId) => {
|
||||
let item = db.notebooks.notebook(notebookId)?.topics?.topic(id)?._topic;
|
||||
if (!item) return;
|
||||
TopicNotes.navigate(item, true);
|
||||
};
|
||||
const renderItem = (item) => (
|
||||
<PressableButton
|
||||
<View
|
||||
key={item.id}
|
||||
onPress={() => {
|
||||
navigateNotebook(item.id);
|
||||
eSendEvent(eClearEditor);
|
||||
close();
|
||||
}}
|
||||
type={full ? "transparent" : "grayBg"}
|
||||
customStyle={{
|
||||
style={{
|
||||
justifyContent: "flex-start",
|
||||
paddingHorizontal: 12,
|
||||
flexDirection: "row",
|
||||
@@ -64,7 +94,10 @@ export default function Notebooks({ note, close, full }) {
|
||||
flexShrink: 1,
|
||||
flexGrow: 1,
|
||||
padding: 6,
|
||||
borderWidth: full ? 0 : 1,
|
||||
borderColor: colors.primary.background,
|
||||
borderRadius: 10,
|
||||
backgroundColor: full ? "transparent" : colors.secondary.background,
|
||||
minHeight: 42
|
||||
}}
|
||||
>
|
||||
@@ -82,10 +115,65 @@ export default function Notebooks({ note, close, full }) {
|
||||
maxWidth: "50%"
|
||||
}}
|
||||
size={SIZE.sm}
|
||||
onPress={() => {
|
||||
navigateNotebook(item.id);
|
||||
eSendEvent(eClearEditor);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</Heading>
|
||||
</PressableButton>
|
||||
|
||||
<ScrollView
|
||||
horizontal={true}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
marginLeft: 8,
|
||||
borderLeftColor: colors.primary.hover,
|
||||
borderLeftWidth: 1,
|
||||
paddingLeft: 8
|
||||
}}
|
||||
>
|
||||
{item.topics.map((topic) => (
|
||||
<Button
|
||||
key={topic.id}
|
||||
onPress={() => {
|
||||
navigateTopic(topic.id, item.id);
|
||||
eSendEvent(eClearEditor);
|
||||
close();
|
||||
}}
|
||||
onLongPress={async () => {
|
||||
await db.notes.removeFromNotebook(
|
||||
{
|
||||
id: item.id,
|
||||
topic: topic.id
|
||||
},
|
||||
note
|
||||
);
|
||||
useNotebookStore.getState().setNotebooks();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
ToastEvent.show({
|
||||
heading: "Note removed from topic",
|
||||
context: "local",
|
||||
type: "success"
|
||||
});
|
||||
}}
|
||||
title={topic.title}
|
||||
type="gray"
|
||||
height={30}
|
||||
fontSize={SIZE.xs}
|
||||
icon="bookmark-outline"
|
||||
style={{
|
||||
marginRight: 5,
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 8
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<View style={{ width: 10 }} />
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
|
||||
return noteNotebooks.length === 0 ? null : (
|
||||
@@ -102,7 +190,7 @@ export default function Notebooks({ note, close, full }) {
|
||||
|
||||
{noteNotebooks.length > 1 && !full ? (
|
||||
<Button
|
||||
title={`View all linked notebooks`}
|
||||
title={`See all linked notebooks`}
|
||||
fontSize={SIZE.xs}
|
||||
style={{
|
||||
alignSelf: "flex-end",
|
||||
|
||||
@@ -27,7 +27,6 @@ import { sleep } from "../../utils/time";
|
||||
import ManageTagsSheet from "../sheets/manage-tags";
|
||||
import { Button } from "../ui/button";
|
||||
import { ColorTags } from "./color-tags";
|
||||
|
||||
export const Tags = ({ item, close }) => {
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
@@ -48,7 +47,7 @@ export const Tags = ({ item, close }) => {
|
||||
>
|
||||
<Button
|
||||
onPress={async () => {
|
||||
ManageTagsSheet.present([item.id]);
|
||||
ManageTagsSheet.present([item]);
|
||||
}}
|
||||
buttonType={{
|
||||
text: colors.primary.accent
|
||||
|
||||
70
apps/mobile/app/components/properties/topics.js
Normal file
70
apps/mobile/app/components/properties/topics.js
Normal file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
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 { TopicNotes } from "../../screens/notes/topic-notes";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
export const Topics = ({ item, close }) => {
|
||||
const open = (topic) => {
|
||||
close();
|
||||
TopicNotes.navigate(topic, true);
|
||||
};
|
||||
|
||||
const renderItem = (topic) => (
|
||||
<Button
|
||||
key={topic.id}
|
||||
title={topic.title}
|
||||
type="grayBg"
|
||||
// buttonType={{
|
||||
// text: colors.primary.accent
|
||||
// }}
|
||||
height={30}
|
||||
onPress={() => open(topic)}
|
||||
icon="bookmark-outline"
|
||||
fontSize={SIZE.xs}
|
||||
style={{
|
||||
marginRight: 5,
|
||||
paddingHorizontal: 8,
|
||||
borderRadius: 100,
|
||||
marginVertical: 5
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return item &&
|
||||
item.type === "notebook" &&
|
||||
item.topics &&
|
||||
item.topics.length > 0 ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
marginTop: 5,
|
||||
width: "100%",
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
>
|
||||
{item.topics
|
||||
.sort((a, b) => a.dateEdited - b.dateEdited)
|
||||
.slice(0, 6)
|
||||
.map(renderItem)}
|
||||
</View>
|
||||
) : null;
|
||||
};
|
||||
365
apps/mobile/app/components/selection-header/index.js
Normal file
365
apps/mobile/app/components/selection-header/index.js
Normal file
@@ -0,0 +1,365 @@
|
||||
/*
|
||||
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, { useCallback, useEffect } from "react";
|
||||
import { BackHandler, Platform, View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { ToastEvent } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import SearchService from "../../services/search";
|
||||
import useNavigationStore from "../../stores/use-navigation-store";
|
||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { deleteItems } from "../../utils/functions";
|
||||
import { tabBarRef } from "../../utils/global-refs";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import MoveNoteSheet from "../sheets/add-to";
|
||||
import ExportNotesSheet from "../sheets/export-notes";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import ManageTagsSheet from "../sheets/manage-tags";
|
||||
|
||||
export const SelectionHeader = React.memo(() => {
|
||||
const { colors } = useThemeColors();
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
const selectedItemsList = useSelectionStore(
|
||||
(state) => state.selectedItemsList
|
||||
);
|
||||
const setSelectionMode = useSelectionStore((state) => state.setSelectionMode);
|
||||
const clearSelection = useSelectionStore((state) => state.clearSelection);
|
||||
const currentScreen = useNavigationStore((state) => state.currentScreen);
|
||||
const screen = currentScreen.name;
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
SearchService.prepareSearch?.();
|
||||
const allItems = SearchService.getSearchInformation()?.get() || [];
|
||||
const allSelected = allItems.length === selectedItemsList.length;
|
||||
useEffect(() => {
|
||||
if (selectionMode) {
|
||||
tabBarRef.current?.lock();
|
||||
} else {
|
||||
tabBarRef.current?.unlock();
|
||||
}
|
||||
}, [selectionMode]);
|
||||
|
||||
const addToFavorite = async () => {
|
||||
if (selectedItemsList.length > 0) {
|
||||
selectedItemsList.forEach((item) => {
|
||||
db.notes.note(item.id).favorite();
|
||||
});
|
||||
Navigation.queueRoutesForUpdate();
|
||||
clearSelection();
|
||||
}
|
||||
};
|
||||
|
||||
const restoreItem = async () => {
|
||||
if (selectedItemsList.length > 0) {
|
||||
let noteIds = [];
|
||||
selectedItemsList.forEach((item) => {
|
||||
noteIds.push(item.id);
|
||||
});
|
||||
await db.trash.restore(...noteIds);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
|
||||
clearSelection();
|
||||
ToastEvent.show({
|
||||
heading: "Restore successful",
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const deleteItem = async () => {
|
||||
presentDialog({
|
||||
title: `Delete ${selectedItemsList.length > 1 ? "items" : "item"}`,
|
||||
paragraph: `Are you sure you want to delete ${
|
||||
selectedItemsList.length > 1
|
||||
? "these items permanently?"
|
||||
: "this item permanently?"
|
||||
}`,
|
||||
positiveText: "Delete",
|
||||
negativeText: "Cancel",
|
||||
positivePress: async () => {
|
||||
if (selectedItemsList.length > 0) {
|
||||
let noteIds = [];
|
||||
selectedItemsList.forEach((item) => {
|
||||
noteIds.push(item.id);
|
||||
});
|
||||
await db.trash.delete(...noteIds);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
clearSelection();
|
||||
}
|
||||
},
|
||||
positiveType: "errorShade"
|
||||
});
|
||||
};
|
||||
|
||||
const onBackPress = useCallback(() => {
|
||||
clearSelection();
|
||||
return true;
|
||||
}, [clearSelection]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectionMode) {
|
||||
BackHandler.addEventListener("hardwareBackPress", onBackPress);
|
||||
} else {
|
||||
BackHandler.removeEventListener("hardwareBackPress", onBackPress);
|
||||
}
|
||||
}, [onBackPress, selectionMode]);
|
||||
|
||||
return !selectionMode ? null : (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: Platform.OS === "android" ? 50 + insets.top : 50,
|
||||
paddingTop: Platform.OS === "android" ? insets.top : null,
|
||||
backgroundColor: colors.primary.background,
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
zIndex: 999,
|
||||
paddingHorizontal: 12,
|
||||
marginVertical: 10
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "center",
|
||||
borderRadius: 100
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
customStyle={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: 40,
|
||||
width: 40,
|
||||
borderRadius: 100,
|
||||
marginRight: 10
|
||||
}}
|
||||
type="grayBg"
|
||||
onPress={() => {
|
||||
setSelectionMode(!selectionMode);
|
||||
}}
|
||||
size={SIZE.xl}
|
||||
color={colors.primary.icon}
|
||||
name="close"
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.secondary.background,
|
||||
height: 40,
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 16,
|
||||
justifyContent: "center",
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.md} color={colors.primary.accent}>
|
||||
{selectedItemsList.length}
|
||||
</Heading>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
useSelectionStore
|
||||
.getState()
|
||||
.setAll(allSelected ? [] : [...allItems]);
|
||||
}}
|
||||
tooltipText="Select all"
|
||||
tooltipPosition={4}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
name="select-all"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
|
||||
{screen === "Trash" ||
|
||||
screen === "Notebooks" ||
|
||||
screen === "Reminders" ? null : (
|
||||
<>
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
await sleep(100);
|
||||
ManageTagsSheet.present(selectedItemsList);
|
||||
}}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
color={colors.primary.icon}
|
||||
tooltipText="Manage tags"
|
||||
tooltipPosition={4}
|
||||
name="pound"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
ExportNotesSheet.present(selectedItemsList);
|
||||
}}
|
||||
tooltipText="Export"
|
||||
tooltipPosition={4}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
name="export"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
//setSelectionMode(false);
|
||||
await sleep(100);
|
||||
MoveNoteSheet.present();
|
||||
}}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
tooltipText="Add to notebooks"
|
||||
tooltipPosition={4}
|
||||
color={colors.primary.paragraph}
|
||||
name="plus"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{screen === "TopicNotes" || screen === "Notebook" ? (
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
if (selectedItemsList.length > 0) {
|
||||
const currentScreen =
|
||||
useNavigationStore.getState().currentScreen;
|
||||
|
||||
if (screen === "Notebook") {
|
||||
for (const item of selectedItemsList) {
|
||||
await db.relations.unlink(
|
||||
{ type: "notebook", id: currentScreen.id },
|
||||
item
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await db.notes.removeFromNotebook(
|
||||
{
|
||||
id: currentScreen.notebookId,
|
||||
topic: currentScreen.id
|
||||
},
|
||||
...selectedItemsList.map((item) => item.id)
|
||||
);
|
||||
}
|
||||
|
||||
Navigation.queueRoutesForUpdate();
|
||||
clearSelection();
|
||||
}
|
||||
}}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
tooltipText={`Remove from ${
|
||||
screen === "Notebook" ? "notebook" : "topic"
|
||||
}`}
|
||||
tooltipPosition={4}
|
||||
testID="select-minus"
|
||||
color={colors.primary.paragraph}
|
||||
name="minus"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{screen === "Favorites" ? (
|
||||
<IconButton
|
||||
onPress={addToFavorite}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
tooltipText="Remove from favorites"
|
||||
tooltipPosition={4}
|
||||
color={colors.primary.paragraph}
|
||||
name="star-off"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{screen === "Trash" ? null : (
|
||||
<IconButton
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
onPress={() => {
|
||||
deleteItems();
|
||||
}}
|
||||
tooltipText="Move to trash"
|
||||
tooltipPosition={1}
|
||||
color={colors.primary.paragraph}
|
||||
name="delete"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
)}
|
||||
|
||||
{screen === "Trash" ? (
|
||||
<>
|
||||
<IconButton
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
onPress={restoreItem}
|
||||
name="delete-restore"
|
||||
tooltipText="Restore"
|
||||
tooltipPosition={4}
|
||||
size={SIZE.xl - 3}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
onPress={deleteItem}
|
||||
tooltipText="Delete"
|
||||
tooltipPosition={4}
|
||||
name="delete"
|
||||
size={SIZE.xl - 3}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
SelectionHeader.displayName = "SelectionHeader";
|
||||
|
||||
export default SelectionHeader;
|
||||
@@ -1,363 +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 { Item, ItemType, VirtualizedGrouping } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import {
|
||||
BackHandler,
|
||||
NativeEventSubscription,
|
||||
Platform,
|
||||
View
|
||||
} from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import useNavigationStore from "../../stores/use-navigation-store";
|
||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
||||
import { deleteItems } from "../../utils/functions";
|
||||
import { tabBarRef } from "../../utils/global-refs";
|
||||
import { updateNotebook } from "../../utils/notebooks";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import MoveNoteSheet from "../sheets/add-to";
|
||||
import ExportNotesSheet from "../sheets/export-notes";
|
||||
import ManageTagsSheet from "../sheets/manage-tags";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Animated, { FadeInUp } from "react-native-reanimated";
|
||||
|
||||
export const SelectionHeader = React.memo(
|
||||
({
|
||||
items,
|
||||
type,
|
||||
id,
|
||||
renderedInRoute
|
||||
}: {
|
||||
items?: VirtualizedGrouping<Item>;
|
||||
id?: string;
|
||||
type?: ItemType;
|
||||
renderedInRoute?: string;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
const selectedItemsList = useSelectionStore(
|
||||
(state) => state.selectedItemsList
|
||||
);
|
||||
const clearSelection = useSelectionStore((state) => state.clearSelection);
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const allSelected =
|
||||
items?.ids?.filter((id) => typeof id === "string").length ===
|
||||
selectedItemsList.length;
|
||||
const focusedRouteId = useNavigationStore((state) => state.focusedRouteId);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectionMode) {
|
||||
tabBarRef.current?.lock();
|
||||
} else {
|
||||
tabBarRef.current?.unlock();
|
||||
}
|
||||
}, [selectionMode]);
|
||||
|
||||
const addToFavorite = async () => {
|
||||
if (!selectedItemsList.length) return;
|
||||
db.notes.favorite(true, ...selectedItemsList);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
clearSelection();
|
||||
};
|
||||
|
||||
const restoreItem = async () => {
|
||||
if (!selectedItemsList.length) return;
|
||||
await db.trash.restore(...selectedItemsList);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
|
||||
clearSelection();
|
||||
ToastManager.show({
|
||||
heading: "Restore successful",
|
||||
type: "success"
|
||||
});
|
||||
};
|
||||
|
||||
const deleteItem = async () => {
|
||||
presentDialog({
|
||||
title: `Delete ${selectedItemsList.length > 1 ? "items" : "item"}`,
|
||||
paragraph: `Are you sure you want to delete ${
|
||||
selectedItemsList.length > 1
|
||||
? "these items permanently?"
|
||||
: "this item permanently?"
|
||||
}`,
|
||||
positiveText: "Delete",
|
||||
negativeText: "Cancel",
|
||||
positivePress: async () => {
|
||||
if (!selectedItemsList.length) return;
|
||||
await db.trash.delete(...selectedItemsList);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
clearSelection();
|
||||
},
|
||||
positiveType: "errorShade"
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const onBackPress = () => {
|
||||
clearSelection();
|
||||
return true;
|
||||
};
|
||||
let sub: NativeEventSubscription | undefined;
|
||||
if (selectionMode) {
|
||||
sub = BackHandler.addEventListener("hardwareBackPress", onBackPress);
|
||||
}
|
||||
|
||||
return () => {
|
||||
sub?.remove();
|
||||
};
|
||||
}, [clearSelection, selectionMode]);
|
||||
|
||||
return selectionMode !== type || focusedRouteId !== id ? null : (
|
||||
<Animated.View
|
||||
entering={FadeInUp}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: Platform.OS === "android" ? 50 + insets.top : 50,
|
||||
paddingTop: Platform.OS === "android" ? insets.top : null,
|
||||
backgroundColor: colors.primary.background,
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
zIndex: 999,
|
||||
paddingHorizontal: 12,
|
||||
marginVertical: 10
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "center",
|
||||
borderRadius: 100
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
customStyle={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: 40,
|
||||
width: 40,
|
||||
borderRadius: 100,
|
||||
marginRight: 10
|
||||
}}
|
||||
type="grayBg"
|
||||
onPress={() => {
|
||||
clearSelection();
|
||||
}}
|
||||
size={SIZE.xl}
|
||||
color={colors.primary.icon}
|
||||
name="close"
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.secondary.background,
|
||||
height: 40,
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 16,
|
||||
justifyContent: "center",
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.md} color={colors.primary.accent}>
|
||||
{selectedItemsList.length}
|
||||
</Heading>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
useSelectionStore
|
||||
.getState()
|
||||
.setAll(
|
||||
allSelected
|
||||
? []
|
||||
: [
|
||||
...((items?.ids.filter(
|
||||
(id) => typeof id !== "object"
|
||||
) as string[]) || [])
|
||||
]
|
||||
);
|
||||
}}
|
||||
tooltipText="Select all"
|
||||
tooltipPosition={4}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
color={
|
||||
allSelected ? colors.primary.accent : colors.primary.paragraph
|
||||
}
|
||||
name="select-all"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
|
||||
{type !== "note" ? null : (
|
||||
<>
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
await sleep(100);
|
||||
ManageTagsSheet.present(selectedItemsList);
|
||||
}}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
color={colors.primary.icon}
|
||||
tooltipText="Manage tags"
|
||||
tooltipPosition={4}
|
||||
name="pound"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
ExportNotesSheet.present(selectedItemsList);
|
||||
}}
|
||||
tooltipText="Export"
|
||||
tooltipPosition={4}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
name="export"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
MoveNoteSheet.present();
|
||||
}}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
tooltipText="Add to notebooks"
|
||||
tooltipPosition={4}
|
||||
color={colors.primary.paragraph}
|
||||
name="plus"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{renderedInRoute === "Notebook" ? (
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
if (selectedItemsList.length > 0) {
|
||||
if (!id) return;
|
||||
await db.notes.removeFromNotebook(id, ...selectedItemsList);
|
||||
updateNotebook(id);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
clearSelection();
|
||||
}
|
||||
}}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
tooltipText={`Remove from Notebook`}
|
||||
tooltipPosition={4}
|
||||
testID="select-minus"
|
||||
color={colors.primary.paragraph}
|
||||
name="minus"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{focusedRouteId === "Favorites" ? (
|
||||
<IconButton
|
||||
onPress={addToFavorite}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
tooltipText="Remove from favorites"
|
||||
tooltipPosition={4}
|
||||
color={colors.primary.paragraph}
|
||||
name="star-off"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{type === "trash" ? null : (
|
||||
<IconButton
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
onPress={() => {
|
||||
deleteItems(
|
||||
undefined,
|
||||
useSelectionStore.getState().selectionMode
|
||||
);
|
||||
}}
|
||||
tooltipText="Move to trash"
|
||||
tooltipPosition={1}
|
||||
color={colors.primary.paragraph}
|
||||
name="delete"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === "trash" ? (
|
||||
<>
|
||||
<IconButton
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
onPress={restoreItem}
|
||||
name="delete-restore"
|
||||
tooltipText="Restore"
|
||||
tooltipPosition={4}
|
||||
size={SIZE.xl - 3}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
onPress={deleteItem}
|
||||
tooltipText="Delete"
|
||||
tooltipPosition={4}
|
||||
name="delete"
|
||||
size={SIZE.xl - 3}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
SelectionHeader.displayName = "SelectionHeader";
|
||||
|
||||
export default SelectionHeader;
|
||||
@@ -101,6 +101,7 @@ const SheetProvider = ({ context = "global" }) => {
|
||||
setVisible(false);
|
||||
setData(null);
|
||||
}}
|
||||
keyboardHandlerDisabled={data?.keyboardHandlerDisabled}
|
||||
bottomPadding={!data.noBottomPadding}
|
||||
enableGesturesInScrollView={
|
||||
typeof data.enableGesturesInScrollView === "undefined"
|
||||
|
||||
553
apps/mobile/app/components/sheets/add-notebook/index.js
Normal file
553
apps/mobile/app/components/sheets/add-notebook/index.js
Normal file
@@ -0,0 +1,553 @@
|
||||
/*
|
||||
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, { createRef } from "react";
|
||||
import {
|
||||
Keyboard,
|
||||
StyleSheet,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from "react-native";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { db } from "../../../common/database";
|
||||
import { DDS } from "../../../services/device-detection";
|
||||
import { ToastEvent, presentSheet } from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { useMenuStore } from "../../../stores/use-menu-store";
|
||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
import { SIZE, ph, pv } from "../../../utils/size";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import Input from "../../ui/input";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import { MoveNotes } from "../move-notes/movenote";
|
||||
|
||||
let refs = [];
|
||||
export class AddNotebookSheet extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
refs = [];
|
||||
this.state = {
|
||||
notebook: props.notebook,
|
||||
topics:
|
||||
props.notebook?.topics?.map((item) => {
|
||||
return item.title;
|
||||
}) || [],
|
||||
description: null,
|
||||
titleFocused: false,
|
||||
descFocused: false,
|
||||
count: 0,
|
||||
topicInputFocused: false,
|
||||
editTopic: false,
|
||||
loading: false
|
||||
};
|
||||
|
||||
this.title = props.notebook?.title;
|
||||
this.description = props.notebook?.description;
|
||||
this.listRef;
|
||||
this.prevItem = null;
|
||||
this.prevIndex = null;
|
||||
this.currentSelectedInput = null;
|
||||
this.id = props.notebook?.id;
|
||||
this.backPressCount = 0;
|
||||
this.currentInputValue = null;
|
||||
this.titleRef;
|
||||
this.descriptionRef;
|
||||
this.topicsToDelete = [];
|
||||
this.hiddenInput = createRef();
|
||||
this.topicInputRef = createRef();
|
||||
this.addingTopic = false;
|
||||
this.actionSheetRef = props.actionSheetRef;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
refs = [];
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
sleep(300).then(() => {
|
||||
!this.state.notebook && this.titleRef?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
close = () => {
|
||||
refs = [];
|
||||
this.props.close(true);
|
||||
};
|
||||
|
||||
onDelete = (index) => {
|
||||
let { topics } = this.state;
|
||||
let prevTopics = topics;
|
||||
refs = [];
|
||||
prevTopics.splice(index, 1);
|
||||
let edit = this.state.notebook;
|
||||
if (edit && edit.id) {
|
||||
let topicToDelete = edit.topics[index];
|
||||
|
||||
if (topicToDelete) {
|
||||
this.topicsToDelete.push(topicToDelete.id);
|
||||
}
|
||||
}
|
||||
let nextTopics = [...prevTopics];
|
||||
if (this.prevIndex === index) {
|
||||
this.prevIndex = null;
|
||||
this.prevItem = null;
|
||||
this.currentInputValue = null;
|
||||
this.topicInputRef.current?.setNativeProps({
|
||||
text: null
|
||||
});
|
||||
}
|
||||
this.setState({
|
||||
topics: nextTopics
|
||||
});
|
||||
};
|
||||
|
||||
addNewNotebook = async () => {
|
||||
this.setState({
|
||||
loading: true
|
||||
});
|
||||
let { topics, notebook } = this.state;
|
||||
|
||||
if (!this.title || this.title?.trim().length === 0) {
|
||||
ToastEvent.show({
|
||||
heading: "Notebook title is required",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
let toEdit = null;
|
||||
if (notebook) {
|
||||
toEdit = db.notebooks.notebook(notebook.id).data;
|
||||
}
|
||||
|
||||
let prevTopics = [...topics];
|
||||
|
||||
if (this.currentInputValue && this.currentInputValue.trim().length !== 0) {
|
||||
if (this.prevItem != null) {
|
||||
prevTopics[this.prevIndex] = this.currentInputValue;
|
||||
} else {
|
||||
prevTopics.push(this.currentInputValue);
|
||||
this.currentInputValue = null;
|
||||
}
|
||||
}
|
||||
let newNotebookId = null;
|
||||
if (notebook) {
|
||||
if (this.topicsToDelete?.length > 0) {
|
||||
await db.notebooks
|
||||
.notebook(toEdit.id)
|
||||
.topics.delete(...this.topicsToDelete);
|
||||
toEdit = db.notebooks.notebook(toEdit.id).data;
|
||||
}
|
||||
|
||||
await db.notebooks.add({
|
||||
title: this.title,
|
||||
description: this.description,
|
||||
id: notebook.id
|
||||
});
|
||||
|
||||
let nextTopics = toEdit.topics.map((topic, index) => {
|
||||
let copy = { ...topic };
|
||||
copy.title = prevTopics[index];
|
||||
return copy;
|
||||
});
|
||||
|
||||
prevTopics.forEach((title, index) => {
|
||||
if (!nextTopics[index]) {
|
||||
nextTopics.push(title);
|
||||
}
|
||||
});
|
||||
|
||||
await db.notebooks.notebook(toEdit.id).topics.add(...nextTopics);
|
||||
this.close();
|
||||
} else {
|
||||
newNotebookId = await db.notebooks.add({
|
||||
title: this.title,
|
||||
description: this.description,
|
||||
topics: prevTopics,
|
||||
id: null
|
||||
});
|
||||
}
|
||||
useMenuStore.getState().setMenuPins();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useRelationStore.getState().update();
|
||||
MoveNotes.present(db.notebooks.notebook(newNotebookId).data);
|
||||
};
|
||||
|
||||
onSubmit = (forward = true) => {
|
||||
this.hiddenInput.current?.focus();
|
||||
let willFocus = true;
|
||||
let { topics } = this.state;
|
||||
if (!this.currentInputValue || this.currentInputValue?.trim().length === 0)
|
||||
return;
|
||||
|
||||
let prevTopics = [...topics];
|
||||
if (this.prevItem === null) {
|
||||
prevTopics.push(this.currentInputValue);
|
||||
this.setState({
|
||||
topics: prevTopics
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.listRef.current?.scrollToEnd?.({ animated: true });
|
||||
}, 30);
|
||||
this.currentInputValue = null;
|
||||
} else {
|
||||
prevTopics[this.prevIndex] = this.currentInputValue;
|
||||
this.setState({
|
||||
topics: prevTopics
|
||||
});
|
||||
this.currentInputValue = null;
|
||||
|
||||
if (this.state.editTopic) {
|
||||
this.topicInputRef.current?.blur();
|
||||
Keyboard.dismiss();
|
||||
this.setState({
|
||||
editTopic: false
|
||||
});
|
||||
willFocus = false;
|
||||
}
|
||||
this.prevItem = null;
|
||||
this.prevIndex = null;
|
||||
this.currentInputValue = null;
|
||||
|
||||
if (forward) {
|
||||
setTimeout(() => {
|
||||
this.listRef.current?.scrollToEnd?.({ animated: true });
|
||||
}, 30);
|
||||
}
|
||||
}
|
||||
this.topicInputRef.current?.setNativeProps({
|
||||
text: ""
|
||||
});
|
||||
willFocus && this.topicInputRef.current?.focus();
|
||||
};
|
||||
|
||||
renderTopicItem = ({ item, index }) => (
|
||||
<TopicItem
|
||||
item={item}
|
||||
onPress={(item, index) => {
|
||||
this.prevIndex = index;
|
||||
this.prevItem = item;
|
||||
this.topicInputRef.current?.setNativeProps({
|
||||
text: item
|
||||
});
|
||||
this.topicInputRef.current?.focus();
|
||||
this.currentInputValue = item;
|
||||
this.setState({
|
||||
editTopic: true
|
||||
});
|
||||
}}
|
||||
onDelete={this.onDelete}
|
||||
index={index}
|
||||
colors={this.props.colors}
|
||||
/>
|
||||
);
|
||||
|
||||
render() {
|
||||
const { colors } = this.props;
|
||||
const { topics, topicInputFocused, notebook } = this.state;
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
maxHeight: DDS.isTab ? "90%" : "97%",
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
ref={this.hiddenInput}
|
||||
style={{
|
||||
width: 1,
|
||||
height: 1,
|
||||
opacity: 0,
|
||||
position: "absolute"
|
||||
}}
|
||||
blurOnSubmit={false}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.lg}>
|
||||
{notebook && notebook.dateCreated
|
||||
? "Edit Notebook"
|
||||
: "New Notebook"}
|
||||
</Heading>
|
||||
<Button
|
||||
title="Save"
|
||||
type="accent"
|
||||
height={40}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 24
|
||||
}}
|
||||
onPress={this.addNewNotebook}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Seperator />
|
||||
|
||||
<Input
|
||||
fwdRef={(ref) => (this.titleRef = ref)}
|
||||
testID={notesnook.ids.dialogs.notebook.inputs.title}
|
||||
onChangeText={(value) => {
|
||||
this.title = value;
|
||||
}}
|
||||
placeholder="Enter a title"
|
||||
onSubmit={() => {
|
||||
this.descriptionRef.focus();
|
||||
}}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
defaultValue={notebook ? notebook.title : null}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={(ref) => (this.descriptionRef = ref)}
|
||||
testID={notesnook.ids.dialogs.notebook.inputs.description}
|
||||
onChangeText={(value) => {
|
||||
this.description = value;
|
||||
}}
|
||||
placeholder="Describe your notebook."
|
||||
onSubmit={() => {
|
||||
this.topicInputRef.current?.focus();
|
||||
}}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
defaultValue={notebook ? notebook.description : null}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={this.topicInputRef}
|
||||
testID={notesnook.ids.dialogs.notebook.inputs.topic}
|
||||
onChangeText={(value) => {
|
||||
this.currentInputValue = value;
|
||||
if (this.prevItem !== null) {
|
||||
refs[this.prevIndex].setNativeProps({
|
||||
text: value,
|
||||
style: {
|
||||
borderBottomColor: colors.primary.accent
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
returnKeyLabel="Done"
|
||||
returnKeyType="done"
|
||||
onSubmit={() => {
|
||||
this.onSubmit();
|
||||
}}
|
||||
blurOnSubmit={false}
|
||||
button={{
|
||||
testID: "topic-add-button",
|
||||
icon: this.state.editTopic ? "check" : "plus",
|
||||
onPress: this.onSubmit,
|
||||
color: topicInputFocused
|
||||
? colors.primary.accent
|
||||
: colors.secondary.icon
|
||||
}}
|
||||
placeholder="Add a topic"
|
||||
/>
|
||||
|
||||
<FlatList
|
||||
data={topics}
|
||||
ref={(ref) => (this.listRef = ref)}
|
||||
nestedScrollEnabled
|
||||
keyExtractor={(item, index) => item + index.toString()}
|
||||
keyboardShouldPersistTaps="always"
|
||||
keyboardDismissMode="interactive"
|
||||
ListFooterComponent={
|
||||
topics.length === 0 ? null : <View style={{ height: 50 }} />
|
||||
}
|
||||
renderItem={this.renderTopicItem}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddNotebookSheet.present = (notebook) => {
|
||||
presentSheet({
|
||||
component: (ref, close, _update, colors) => (
|
||||
<AddNotebookSheet
|
||||
actionSheetRef={ref}
|
||||
notebook={notebook}
|
||||
close={close}
|
||||
colors={colors}
|
||||
/>
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
const TopicItem = ({ item, index, colors, onPress, onDelete }) => {
|
||||
const topicRef = (ref) => (refs[index] = ref);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 5,
|
||||
marginVertical: 5
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
width: "80%",
|
||||
backgroundColor: "transparent",
|
||||
zIndex: 10,
|
||||
position: "absolute",
|
||||
height: 30
|
||||
}}
|
||||
onPress={() => {
|
||||
onPress(item, index);
|
||||
}}
|
||||
/>
|
||||
<TextInput
|
||||
ref={topicRef}
|
||||
editable={false}
|
||||
style={[
|
||||
styles.topicInput,
|
||||
{
|
||||
color: colors.primary.paragraph
|
||||
}
|
||||
]}
|
||||
defaultValue={item}
|
||||
placeholderTextColor={colors.primary.placeholder}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: 80,
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-end"
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
onPress(item, index);
|
||||
}}
|
||||
name="pencil"
|
||||
size={SIZE.lg - 5}
|
||||
color={colors.secondary.icon}
|
||||
/>
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
onDelete(index);
|
||||
}}
|
||||
name="minus"
|
||||
size={SIZE.lg}
|
||||
color={colors.secondary.icon}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
wrapper: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: "rgba(0,0,0,0.3)",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
},
|
||||
overlay: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute"
|
||||
},
|
||||
headingContainer: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
},
|
||||
headingText: {
|
||||
marginLeft: 5,
|
||||
fontSize: SIZE.xl
|
||||
},
|
||||
input: {
|
||||
paddingRight: 12,
|
||||
paddingHorizontal: 0,
|
||||
borderRadius: 0,
|
||||
minHeight: 45,
|
||||
fontSize: SIZE.md,
|
||||
padding: pv - 2,
|
||||
borderBottomWidth: 1,
|
||||
marginTop: 10,
|
||||
marginBottom: 5
|
||||
},
|
||||
addBtn: {
|
||||
width: "12%",
|
||||
minHeight: 45,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
position: "absolute",
|
||||
right: 0
|
||||
},
|
||||
buttonContainer: {
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
marginTop: 20
|
||||
},
|
||||
|
||||
topicContainer: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginTop: 10
|
||||
},
|
||||
topicInput: {
|
||||
padding: pv - 5,
|
||||
fontSize: SIZE.sm,
|
||||
//fontFamily: "sans-serif",
|
||||
paddingHorizontal: ph,
|
||||
paddingRight: 40,
|
||||
paddingVertical: 10,
|
||||
width: "100%",
|
||||
maxWidth: "100%"
|
||||
},
|
||||
topicBtn: {
|
||||
borderRadius: 5,
|
||||
width: 40,
|
||||
height: 40,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
position: "absolute",
|
||||
right: 0
|
||||
}
|
||||
});
|
||||
@@ -1,189 +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 { Notebook } from "@notesnook/core";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { TextInput, View } from "react-native";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { db } from "../../../common/database";
|
||||
import { DDS } from "../../../services/device-detection";
|
||||
import {
|
||||
ToastManager,
|
||||
eSendEvent,
|
||||
presentSheet
|
||||
} from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { useMenuStore } from "../../../stores/use-menu-store";
|
||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Button } from "../../ui/button";
|
||||
import Input from "../../ui/input";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import { MoveNotes } from "../move-notes/movenote";
|
||||
import { eOnNotebookUpdated } from "../../../utils/events";
|
||||
import { getParentNotebookId } from "../../../utils/notebooks";
|
||||
|
||||
export const AddNotebookSheet = ({
|
||||
notebook,
|
||||
parentNotebook,
|
||||
close
|
||||
}: {
|
||||
notebook?: Notebook;
|
||||
parentNotebook?: Notebook;
|
||||
close: ((context?: string | undefined) => void) | undefined;
|
||||
}) => {
|
||||
const title = useRef(notebook?.title);
|
||||
const description = useRef(notebook?.description);
|
||||
const titleInput = useRef<TextInput>(null);
|
||||
const descriptionInput = useRef<TextInput>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const onSaveChanges = async () => {
|
||||
if (loading) return;
|
||||
setLoading(true);
|
||||
|
||||
if (!title.current || title?.current.trim().length === 0) {
|
||||
ToastManager.show({
|
||||
heading: "Notebook title is required",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const id = await db.notebooks.add({
|
||||
title: title.current,
|
||||
description: description.current,
|
||||
id: notebook?.id
|
||||
});
|
||||
|
||||
if (parentNotebook) {
|
||||
await db.relations.add(parentNotebook, {
|
||||
type: "notebook",
|
||||
id: id
|
||||
});
|
||||
}
|
||||
|
||||
useMenuStore.getState().setMenuPins();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useRelationStore.getState().update();
|
||||
|
||||
const parent =
|
||||
parentNotebook?.id || (await getParentNotebookId(notebook?.id || id));
|
||||
|
||||
eSendEvent(eOnNotebookUpdated, parent);
|
||||
if (notebook) {
|
||||
setImmediate(() => {
|
||||
console.log(parent, notebook.id);
|
||||
eSendEvent(eOnNotebookUpdated, notebook.id);
|
||||
});
|
||||
}
|
||||
|
||||
if (!notebook) {
|
||||
MoveNotes.present(await db.notebooks.notebook(id));
|
||||
} else {
|
||||
close?.();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
maxHeight: DDS.isTab ? "90%" : "97%",
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.lg}>
|
||||
{notebook ? "Edit Notebook" : "New Notebook"}
|
||||
</Heading>
|
||||
<Button
|
||||
title={notebook ? "Save" : "Add"}
|
||||
type="accent"
|
||||
height={40}
|
||||
style={{
|
||||
paddingHorizontal: 24
|
||||
}}
|
||||
onPress={onSaveChanges}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Seperator />
|
||||
|
||||
<Input
|
||||
fwdRef={titleInput}
|
||||
testID={notesnook.ids.dialogs.notebook.inputs.title}
|
||||
onChangeText={(value) => {
|
||||
title.current = value;
|
||||
}}
|
||||
onLayout={() => {
|
||||
setImmediate(() => {
|
||||
titleInput?.current?.focus();
|
||||
});
|
||||
}}
|
||||
placeholder="Enter a title"
|
||||
onSubmit={() => {
|
||||
descriptionInput.current?.focus();
|
||||
}}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
defaultValue={notebook ? notebook.title : ""}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={descriptionInput}
|
||||
testID={notesnook.ids.dialogs.notebook.inputs.description}
|
||||
onChangeText={(value) => {
|
||||
description.current = value;
|
||||
}}
|
||||
placeholder="Describe your notebook."
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
defaultValue={notebook ? notebook.description : ""}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
AddNotebookSheet.present = (
|
||||
notebook?: Notebook,
|
||||
parentNotebook?: Notebook,
|
||||
context?: string
|
||||
) => {
|
||||
presentSheet({
|
||||
context: context,
|
||||
component: (ref, close) => (
|
||||
<AddNotebookSheet
|
||||
notebook={notebook}
|
||||
parentNotebook={parentNotebook}
|
||||
close={close}
|
||||
/>
|
||||
)
|
||||
});
|
||||
};
|
||||
29
apps/mobile/app/components/sheets/add-to/context.js
Normal file
29
apps/mobile/app/components/sheets/add-to/context.js
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
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 { createContext, useContext } from "react";
|
||||
|
||||
export const SelectionContext = createContext({
|
||||
toggleSelection: (item) => null,
|
||||
deselect: (item) => null,
|
||||
select: (item) => null,
|
||||
deselectAll: () => null
|
||||
});
|
||||
export const SelectionProvider = SelectionContext.Provider;
|
||||
export const useSelectionContext = () => useContext(SelectionContext);
|
||||
76
apps/mobile/app/components/sheets/add-to/filtered-list.js
Normal file
76
apps/mobile/app/components/sheets/add-to/filtered-list.js
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
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, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { FlashList } from "react-native-actions-sheet";
|
||||
import { db } from "../../../common/database";
|
||||
import { ListHeaderInputItem } from "./list-header-item.js";
|
||||
|
||||
export const FilteredList = ({
|
||||
data,
|
||||
itemType,
|
||||
onAddItem,
|
||||
hasHeaderSearch,
|
||||
listRef,
|
||||
...restProps
|
||||
}) => {
|
||||
const [filtered, setFiltered] = useState(data);
|
||||
const query = useRef();
|
||||
const onChangeText = useCallback(
|
||||
(value) => {
|
||||
query.current = value;
|
||||
try {
|
||||
if (!value) return setFiltered(data);
|
||||
const results = db.lookup[itemType + "s"]([...data], value);
|
||||
setFiltered(results);
|
||||
} catch (e) {
|
||||
console.warn(e.message);
|
||||
}
|
||||
},
|
||||
[data, itemType]
|
||||
);
|
||||
const onSubmit = async (value) => {
|
||||
return await onAddItem(value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
onChangeText(query.current);
|
||||
}, [data, onChangeText]);
|
||||
|
||||
return (
|
||||
<FlashList
|
||||
{...restProps}
|
||||
data={filtered}
|
||||
ref={listRef}
|
||||
ListHeaderComponent={
|
||||
hasHeaderSearch ? (
|
||||
<ListHeaderInputItem
|
||||
onSubmit={onSubmit}
|
||||
onChangeText={onChangeText}
|
||||
itemType={itemType}
|
||||
testID={"list-input" + itemType}
|
||||
placeholder={`Search or add a new ${itemType}`}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
keyboardShouldPersistTaps="always"
|
||||
keyboardDismissMode="none"
|
||||
/>
|
||||
);
|
||||
};
|
||||
486
apps/mobile/app/components/sheets/add-to/index.js
Normal file
486
apps/mobile/app/components/sheets/add-to/index.js
Normal file
@@ -0,0 +1,486 @@
|
||||
/*
|
||||
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, { useCallback, useEffect, useMemo } from "react";
|
||||
import { Keyboard, TouchableOpacity, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../../common/database";
|
||||
import {
|
||||
eSendEvent,
|
||||
presentSheet,
|
||||
ToastEvent
|
||||
} from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import SearchService from "../../../services/search";
|
||||
import { useNotebookStore } from "../../../stores/use-notebook-store";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import { eOnTopicSheetUpdate } from "../../../utils/events";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { Dialog } from "../../dialog";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
import { Button } from "../../ui/button";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { SelectionProvider } from "./context";
|
||||
import { FilteredList } from "./filtered-list";
|
||||
import { ListItem } from "./list-item";
|
||||
import { useItemSelectionStore } from "./store";
|
||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
|
||||
const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const notebooks = useNotebookStore((state) =>
|
||||
state.notebooks.filter((n) => n?.type === "notebook")
|
||||
);
|
||||
const dimensions = useSettingStore((state) => state.dimensions);
|
||||
const selectedItemsList = useSelectionStore(
|
||||
(state) => state.selectedItemsList
|
||||
);
|
||||
const setNotebooks = useNotebookStore((state) => state.setNotebooks);
|
||||
|
||||
const multiSelect = useItemSelectionStore((state) => state.multiSelect);
|
||||
|
||||
const onAddNotebook = async (title) => {
|
||||
if (!title || title.trim().length === 0) {
|
||||
ToastEvent.show({
|
||||
heading: "Notebook title is required",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
await db.notebooks.add({
|
||||
title: title,
|
||||
description: null,
|
||||
topics: [],
|
||||
id: null
|
||||
});
|
||||
setNotebooks();
|
||||
return true;
|
||||
};
|
||||
|
||||
const openAddTopicDialog = (item) => {
|
||||
presentDialog({
|
||||
context: "move_note",
|
||||
input: true,
|
||||
inputPlaceholder: "Enter title",
|
||||
title: "New topic",
|
||||
paragraph: "Add a new topic in " + item.title,
|
||||
positiveText: "Add",
|
||||
positivePress: (value) => {
|
||||
return onAddTopic(value, item);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onAddTopic = useCallback(
|
||||
async (value, item) => {
|
||||
if (!value || value.trim().length === 0) {
|
||||
ToastEvent.show({
|
||||
heading: "Topic title is required",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
await db.notebooks.notebook(item.id).topics.add(value);
|
||||
setNotebooks();
|
||||
return true;
|
||||
},
|
||||
[setNotebooks]
|
||||
);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const getSelectedNotesCountInItem = React.useCallback(
|
||||
(item) => {
|
||||
switch (item.type) {
|
||||
case "notebook": {
|
||||
const notes = db.relations.from(item, "note");
|
||||
if (notes.length === 0) return 0;
|
||||
let count = 0;
|
||||
selectedItemsList.forEach((item) =>
|
||||
notes.findIndex((note) => note.id === item.id) > -1
|
||||
? count++
|
||||
: undefined
|
||||
);
|
||||
return count;
|
||||
}
|
||||
case "topic": {
|
||||
const noteIds = db.notes?.topicReferences.get(item.id);
|
||||
let count = 0;
|
||||
selectedItemsList.forEach((item) =>
|
||||
noteIds.indexOf(item.id) > -1 ? count++ : undefined
|
||||
);
|
||||
return count;
|
||||
}
|
||||
}
|
||||
},
|
||||
[selectedItemsList]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
resetItemState();
|
||||
return () => {
|
||||
useItemSelectionStore.getState().setMultiSelect(false);
|
||||
useItemSelectionStore.getState().setItemState({});
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const resetItemState = useCallback(
|
||||
(state) => {
|
||||
const itemState = {};
|
||||
const notebooks = db.notebooks.all;
|
||||
let count = 0;
|
||||
for (let notebook of notebooks) {
|
||||
itemState[notebook.id] = state
|
||||
? state
|
||||
: areAllSelectedItemsInNotebook(notebook, selectedItemsList)
|
||||
? "selected"
|
||||
: getSelectedNotesCountInItem(notebook, selectedItemsList) > 0
|
||||
? "intermediate"
|
||||
: "deselected";
|
||||
if (itemState[notebook.id] === "selected") {
|
||||
count++;
|
||||
}
|
||||
for (let topic of notebook.topics) {
|
||||
itemState[topic.id] = state
|
||||
? state
|
||||
: areAllSelectedItemsInTopic(topic, selectedItemsList) &&
|
||||
getSelectedNotesCountInItem(topic, selectedItemsList)
|
||||
? "selected"
|
||||
: getSelectedNotesCountInItem(topic, selectedItemsList) > 0
|
||||
? "intermediate"
|
||||
: "deselected";
|
||||
if (itemState[topic.id] === "selected") {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (count > 1) {
|
||||
useItemSelectionStore.getState().setMultiSelect(true);
|
||||
} else {
|
||||
useItemSelectionStore.getState().setMultiSelect(false);
|
||||
}
|
||||
useItemSelectionStore.getState().setItemState(itemState);
|
||||
},
|
||||
[getSelectedNotesCountInItem, selectedItemsList]
|
||||
);
|
||||
|
||||
const getItemsForItem = (item) => {
|
||||
switch (item.type) {
|
||||
case "notebook":
|
||||
return item.topics?.filter((t) => t.type === "topic");
|
||||
}
|
||||
};
|
||||
|
||||
function areAllSelectedItemsInNotebook(notebook, items) {
|
||||
const notes = db.relations.from(notebook, "note");
|
||||
if (notes.length === 0) return false;
|
||||
return items.every((item) => {
|
||||
return notes.find((note) => note.id === item.id);
|
||||
});
|
||||
}
|
||||
|
||||
function areAllSelectedItemsInTopic(topic, items) {
|
||||
return items.every((item) => {
|
||||
return db.notes.topicReferences.get(topic.id).indexOf(item.id) > -1;
|
||||
});
|
||||
}
|
||||
|
||||
const updateItemState = useCallback(function (item, state) {
|
||||
const itemState = { ...useItemSelectionStore.getState().itemState };
|
||||
const mergeState = {
|
||||
[item.id]: state
|
||||
};
|
||||
useItemSelectionStore.getState().setItemState({
|
||||
...itemState,
|
||||
...mergeState
|
||||
});
|
||||
}, []);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
toggleSelection: (item) => {
|
||||
const itemState = useItemSelectionStore.getState().itemState;
|
||||
if (itemState[item.id] === "selected") {
|
||||
updateItemState(item, "deselected");
|
||||
} else {
|
||||
updateItemState(item, "selected");
|
||||
}
|
||||
},
|
||||
deselect: (item) => {
|
||||
updateItemState(item, "deselected");
|
||||
},
|
||||
select: (item) => {
|
||||
updateItemState(item, "selected");
|
||||
},
|
||||
deselectAll: (state) => {
|
||||
resetItemState(state);
|
||||
}
|
||||
}),
|
||||
[resetItemState, updateItemState]
|
||||
);
|
||||
|
||||
const getItemFromId = (id) => {
|
||||
for (const nb of notebooks) {
|
||||
if (nb.id === id) return nb;
|
||||
for (const tp of nb.topics) {
|
||||
if (tp.id === id) return tp;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onSave = async () => {
|
||||
const noteIds = note ? [note.id] : selectedItemsList.map((n) => n.id);
|
||||
const itemState = useItemSelectionStore.getState().itemState;
|
||||
for (const id in itemState) {
|
||||
const item = getItemFromId(id);
|
||||
if (itemState[id] === "selected") {
|
||||
if (item.type === "notebook") {
|
||||
for (let noteId of noteIds) {
|
||||
await db.relations.add(item, { id: noteId, type: "note" });
|
||||
}
|
||||
} else {
|
||||
await db.notes.addToNotebook(
|
||||
{
|
||||
topic: item.id,
|
||||
id: item.notebookId,
|
||||
rebuildCache: true
|
||||
},
|
||||
...noteIds
|
||||
);
|
||||
}
|
||||
} else if (itemState[id] === "deselected") {
|
||||
if (item.type === "notebook") {
|
||||
for (let noteId of noteIds) {
|
||||
await db.relations.unlink(item, { id: noteId, type: "note" });
|
||||
}
|
||||
} else {
|
||||
await db.notes.removeFromNotebook(
|
||||
{
|
||||
id: item.notebookId,
|
||||
topic: item.id,
|
||||
rebuildCache: true
|
||||
},
|
||||
...noteIds
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Navigation.queueRoutesForUpdate();
|
||||
setNotebooks();
|
||||
eSendEvent(eOnTopicSheetUpdate);
|
||||
SearchService.updateAndSearch();
|
||||
useRelationStore.getState().update();
|
||||
actionSheetRef.current?.hide();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog context="move_note" />
|
||||
<View>
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute"
|
||||
}}
|
||||
onPress={() => {
|
||||
Keyboard.dismiss();
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start"
|
||||
}}
|
||||
>
|
||||
<DialogHeader
|
||||
style={{
|
||||
minHeight: 10,
|
||||
flexShrink: 1
|
||||
}}
|
||||
title="Select notebooks"
|
||||
paragraph={
|
||||
!multiSelect
|
||||
? "Long press to enable multi-select."
|
||||
: "Select topics you want to add note(s) to."
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
height={35}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 24,
|
||||
alignSelf: "flex-start"
|
||||
}}
|
||||
title="Save"
|
||||
type={"accent"}
|
||||
onPress={onSave}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
title="Reset selection"
|
||||
height={30}
|
||||
style={{
|
||||
alignSelf: "flex-start",
|
||||
paddingHorizontal: 0,
|
||||
width: "100%",
|
||||
marginTop: 6
|
||||
}}
|
||||
type="grayAccent"
|
||||
onPress={() => {
|
||||
resetItemState();
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<SelectionProvider value={contextValue}>
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
maxHeight: dimensions.height * 0.85,
|
||||
height: 50 * (notebooks.length + 2)
|
||||
}}
|
||||
>
|
||||
<FilteredList
|
||||
ListEmptyComponent={
|
||||
notebooks.length > 0 ? null : (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="book-outline"
|
||||
color={colors.primary.icon}
|
||||
size={100}
|
||||
/>
|
||||
<Paragraph style={{ marginBottom: 10 }}>
|
||||
You do not have any notebooks.
|
||||
</Paragraph>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
estimatedItemSize={50}
|
||||
data={notebooks}
|
||||
hasHeaderSearch={true}
|
||||
renderItem={({ item, index }) => (
|
||||
<ListItem
|
||||
item={item}
|
||||
key={item.id}
|
||||
index={index}
|
||||
hasNotes={getSelectedNotesCountInItem(item) > 0}
|
||||
sheetRef={actionSheetRef}
|
||||
infoText={
|
||||
<>
|
||||
{item.topics.length === 1
|
||||
? item.topics.length + " topic"
|
||||
: item.topics.length + " topics"}
|
||||
</>
|
||||
}
|
||||
getListItems={getItemsForItem}
|
||||
getSublistItemProps={(topic) => ({
|
||||
hasNotes: getSelectedNotesCountInItem(topic) > 0,
|
||||
style: {
|
||||
marginBottom: 0,
|
||||
height: 40
|
||||
},
|
||||
onPress: (item) => {
|
||||
const itemState =
|
||||
useItemSelectionStore.getState().itemState;
|
||||
const currentState = itemState[item.id];
|
||||
if (currentState !== "selected") {
|
||||
resetItemState("deselected");
|
||||
contextValue.select(item);
|
||||
} else {
|
||||
contextValue.deselect(item);
|
||||
}
|
||||
},
|
||||
key: item.id,
|
||||
type: "transparent"
|
||||
})}
|
||||
icon={(expanded) => ({
|
||||
name: expanded ? "chevron-up" : "chevron-down",
|
||||
color: expanded
|
||||
? colors.primary.accent
|
||||
: colors.primary.paragraph
|
||||
})}
|
||||
onScrollEnd={() => {
|
||||
actionSheetRef.current?.handleChildScrollEnd();
|
||||
}}
|
||||
hasSubList={true}
|
||||
hasHeaderSearch={false}
|
||||
type="grayBg"
|
||||
sublistItemType="topic"
|
||||
onAddItem={(title) => {
|
||||
return onAddTopic(title, item);
|
||||
}}
|
||||
onAddSublistItem={(item) => {
|
||||
openAddTopicDialog(item);
|
||||
}}
|
||||
onPress={(item) => {
|
||||
const itemState =
|
||||
useItemSelectionStore.getState().itemState;
|
||||
const currentState = itemState[item.id];
|
||||
if (currentState !== "selected") {
|
||||
resetItemState("deselected");
|
||||
contextValue.select(item);
|
||||
} else {
|
||||
contextValue.deselect(item);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
itemType="notebook"
|
||||
onAddItem={async (title) => {
|
||||
return await onAddNotebook(title);
|
||||
}}
|
||||
ListFooterComponent={<View style={{ height: 20 }} />}
|
||||
/>
|
||||
</View>
|
||||
</SelectionProvider>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
MoveNoteSheet.present = (note) => {
|
||||
presentSheet({
|
||||
component: (ref) => <MoveNoteSheet actionSheetRef={ref} note={note} />,
|
||||
enableGesturesInScrollView: false,
|
||||
noBottomPadding: true
|
||||
});
|
||||
};
|
||||
export default MoveNoteSheet;
|
||||
@@ -1,257 +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 { GroupHeader, Note } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { RefObject, useCallback, useEffect } from "react";
|
||||
import { Keyboard, TouchableOpacity, View } from "react-native";
|
||||
import { ActionSheetRef, FlashList } from "react-native-actions-sheet";
|
||||
import { db } from "../../../common/database";
|
||||
import { presentSheet } from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { ItemSelection } from "../../../stores/item-selection-store";
|
||||
import { useNotebookStore } from "../../../stores/use-notebook-store";
|
||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import { updateNotebook } from "../../../utils/notebooks";
|
||||
import { Dialog } from "../../dialog";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import SheetProvider from "../../sheet-provider";
|
||||
import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { NotebookItem } from "./notebook-item";
|
||||
import { useNotebookItemSelectionStore } from "./store";
|
||||
|
||||
async function updateInitialSelectionState(items: string[]) {
|
||||
const relations = await db.relations
|
||||
.to(
|
||||
{
|
||||
type: "note",
|
||||
ids: items
|
||||
},
|
||||
"notebook"
|
||||
)
|
||||
.get();
|
||||
|
||||
const initialSelectionState: ItemSelection = {};
|
||||
const notebookIds = [
|
||||
...new Set(relations.map((relation) => relation.fromId))
|
||||
];
|
||||
|
||||
for (const id of notebookIds) {
|
||||
const all = items.every((noteId) => {
|
||||
return (
|
||||
relations.findIndex(
|
||||
(relation) => relation.fromId === id && relation.toId === noteId
|
||||
) > -1
|
||||
);
|
||||
});
|
||||
if (all) {
|
||||
initialSelectionState[id] = "selected";
|
||||
} else {
|
||||
initialSelectionState[id] = "intermediate";
|
||||
}
|
||||
}
|
||||
useNotebookItemSelectionStore.setState({
|
||||
initialState: initialSelectionState,
|
||||
selection: { ...initialSelectionState },
|
||||
multiSelect: relations.length > 1
|
||||
});
|
||||
}
|
||||
|
||||
const MoveNoteSheet = ({
|
||||
note,
|
||||
actionSheetRef
|
||||
}: {
|
||||
note: Note | undefined;
|
||||
actionSheetRef: RefObject<ActionSheetRef>;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const notebooks = useNotebookStore((state) => state.notebooks);
|
||||
const dimensions = useSettingStore((state) => state.dimensions);
|
||||
const selectedItemsList = useSelectionStore(
|
||||
(state) => state.selectedItemsList
|
||||
);
|
||||
|
||||
const multiSelect = useNotebookItemSelectionStore(
|
||||
(state) => state.multiSelect
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const items = note ? [note.id] : selectedItemsList;
|
||||
updateInitialSelectionState(items);
|
||||
return () => {
|
||||
useNotebookItemSelectionStore.setState({
|
||||
initialState: {},
|
||||
selection: {},
|
||||
multiSelect: false,
|
||||
canEnableMultiSelectMode: true
|
||||
});
|
||||
};
|
||||
}, [note, selectedItemsList]);
|
||||
|
||||
const onSave = async () => {
|
||||
const noteIds = note ? [note.id] : selectedItemsList;
|
||||
|
||||
const changedNotebooks = useNotebookItemSelectionStore.getState().selection;
|
||||
|
||||
for (const id in changedNotebooks) {
|
||||
const item = await db.notebooks.notebook(id);
|
||||
if (!item) continue;
|
||||
if (changedNotebooks[id] === "selected") {
|
||||
for (const id of noteIds) {
|
||||
await db.relations.add(item, { id: id, type: "note" });
|
||||
updateNotebook(item.id);
|
||||
}
|
||||
} else if (changedNotebooks[id] === "deselected") {
|
||||
for (const id of noteIds) {
|
||||
await db.relations.unlink(item, { id: id, type: "note" });
|
||||
updateNotebook(item.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useRelationStore.getState().update();
|
||||
actionSheetRef.current?.hide();
|
||||
};
|
||||
|
||||
const renderNotebook = useCallback(
|
||||
({ item, index }: { item: string | GroupHeader; index: number }) =>
|
||||
(item as GroupHeader).type === "header" ? null : (
|
||||
<NotebookItem items={notebooks} id={item as string} index={index} />
|
||||
),
|
||||
[notebooks]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog context="move_note" />
|
||||
<SheetProvider context="link-notebooks" />
|
||||
<View>
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute"
|
||||
}}
|
||||
onPress={() => {
|
||||
Keyboard.dismiss();
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start"
|
||||
}}
|
||||
>
|
||||
<DialogHeader
|
||||
style={{
|
||||
minHeight: 10,
|
||||
flexShrink: 1
|
||||
}}
|
||||
title="Select notebooks"
|
||||
paragraph={
|
||||
!multiSelect
|
||||
? "Long press to enable multi-select."
|
||||
: "Select topics you want to add note(s) to."
|
||||
}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
columnGap: 10
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
name="restore"
|
||||
type="grayAccent"
|
||||
onPress={() => {
|
||||
const items = note ? [note.id] : selectedItemsList;
|
||||
updateInitialSelectionState(items);
|
||||
}}
|
||||
customStyle={{
|
||||
width: 40,
|
||||
height: 40
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
height={40}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 24,
|
||||
alignSelf: "flex-start"
|
||||
}}
|
||||
title="Save"
|
||||
type={"accent"}
|
||||
onPress={onSave}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 0,
|
||||
maxHeight: dimensions.height * 0.85,
|
||||
height: dimensions.height * 0.85,
|
||||
paddingTop: 6
|
||||
}}
|
||||
>
|
||||
<FlashList
|
||||
data={notebooks?.ids?.filter((id) => typeof id === "string")}
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
estimatedItemSize={50}
|
||||
keyExtractor={(item) => item as string}
|
||||
renderItem={renderNotebook}
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: 200
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.primary.icon}>No notebooks</Paragraph>
|
||||
</View>
|
||||
}
|
||||
ListFooterComponent={<View style={{ height: 50 }} />}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
MoveNoteSheet.present = (note?: Note) => {
|
||||
presentSheet({
|
||||
component: (ref) => <MoveNoteSheet actionSheetRef={ref} note={note} />,
|
||||
enableGesturesInScrollView: false,
|
||||
noBottomPadding: true
|
||||
});
|
||||
};
|
||||
export default MoveNoteSheet;
|
||||
88
apps/mobile/app/components/sheets/add-to/list-header-item.js
Normal file
88
apps/mobile/app/components/sheets/add-to/list-header-item.js
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import Input from "../../ui/input";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
|
||||
export const ListHeaderInputItem = ({
|
||||
onSubmit,
|
||||
onChangeText,
|
||||
placeholder,
|
||||
testID
|
||||
}) => {
|
||||
const [focused, setFocused] = useState(false);
|
||||
const { colors } = useThemeColors("sheet");
|
||||
|
||||
const [inputValue, setInputValue] = useState();
|
||||
const inputRef = useRef();
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
marginTop: 10,
|
||||
marginBottom: 5
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
fwdRef={inputRef}
|
||||
onChangeText={(value) => {
|
||||
setInputValue(value);
|
||||
onChangeText?.(value);
|
||||
}}
|
||||
testID={testID}
|
||||
blurOnSubmit={false}
|
||||
onFocusInput={() => {
|
||||
setFocused(true);
|
||||
}}
|
||||
onBlurInput={() => {
|
||||
setFocused(false);
|
||||
}}
|
||||
button={{
|
||||
icon: inputValue ? "plus" : "magnify",
|
||||
color: focused ? colors.selected.icon : colors.secondary.icon,
|
||||
onPress: async () => {
|
||||
const result = await onSubmit(inputValue);
|
||||
if (result) {
|
||||
inputRef.current?.blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
marginBottom={5}
|
||||
/>
|
||||
{inputValue ? (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.primary.shade,
|
||||
padding: 5,
|
||||
borderRadius: 5,
|
||||
marginBottom: 10
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.primary.accent}>
|
||||
Tap on + to add {`"${inputValue}"`}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
293
apps/mobile/app/components/sheets/add-to/list-item.js
Normal file
293
apps/mobile/app/components/sheets/add-to/list-item.js
Normal file
@@ -0,0 +1,293 @@
|
||||
/*
|
||||
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, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { db } from "../../../common/database";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import { PressableButton } from "../../ui/pressable";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { useSelectionContext } from "./context";
|
||||
import { FilteredList } from "./filtered-list";
|
||||
import { useItemSelectionStore } from "./store";
|
||||
|
||||
const SelectionIndicator = ({
|
||||
item,
|
||||
hasNotes,
|
||||
selectItem,
|
||||
onPress,
|
||||
onChange
|
||||
}) => {
|
||||
const itemState = useItemSelectionStore((state) => state.itemState[item.id]);
|
||||
const multiSelect = useItemSelectionStore((state) => state.multiSelect);
|
||||
|
||||
const isSelected = itemState === "selected";
|
||||
const isIntermediate = itemState === "intermediate";
|
||||
const isRemoved = !isSelected && hasNotes;
|
||||
const { colors } = useThemeColors("sheet");
|
||||
|
||||
useEffect(() => {
|
||||
onChange?.();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [itemState]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
size={22}
|
||||
customStyle={{
|
||||
marginRight: 5,
|
||||
width: 23,
|
||||
height: 23
|
||||
}}
|
||||
color={
|
||||
isRemoved
|
||||
? colors.static.red
|
||||
: isIntermediate || isSelected
|
||||
? colors.selected.icon
|
||||
: colors.primary.icon
|
||||
}
|
||||
onPress={() => {
|
||||
if (multiSelect) return selectItem();
|
||||
onPress?.(item);
|
||||
}}
|
||||
onLongPress={() => {
|
||||
useItemSelectionStore.getState().setMultiSelect(true);
|
||||
selectItem();
|
||||
}}
|
||||
testID={
|
||||
isRemoved
|
||||
? "close-circle-outline"
|
||||
: isSelected
|
||||
? "check-circle-outline"
|
||||
: isIntermediate
|
||||
? "minus-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
name={
|
||||
isRemoved
|
||||
? "close-circle-outline"
|
||||
: isSelected
|
||||
? "check-circle-outline"
|
||||
: isIntermediate
|
||||
? "minus-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ListItem = ({
|
||||
item,
|
||||
index,
|
||||
icon,
|
||||
infoText,
|
||||
hasSubList,
|
||||
onPress,
|
||||
onScrollEnd,
|
||||
getListItems,
|
||||
style,
|
||||
type,
|
||||
sublistItemType,
|
||||
onAddItem,
|
||||
getSublistItemProps,
|
||||
hasHeaderSearch,
|
||||
onAddSublistItem,
|
||||
hasNotes,
|
||||
onChange,
|
||||
sheetRef
|
||||
}) => {
|
||||
const { toggleSelection } = useSelectionContext();
|
||||
const multiSelect = useItemSelectionStore((state) => state.multiSelect);
|
||||
const [showSelectedIndicator, setShowSelectedIndicator] = useState(false);
|
||||
const { colors } = useThemeColors("sheet");
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
function selectItem() {
|
||||
toggleSelection(item);
|
||||
}
|
||||
|
||||
const getSelectedNotesCountInNotebookTopics = (item) => {
|
||||
if (item.type === "topic") return;
|
||||
|
||||
let count = 0;
|
||||
const noteIds = [];
|
||||
for (let topic of item.topics) {
|
||||
noteIds.push(...(db.notes?.topicReferences.get(topic.id) || []));
|
||||
if (useItemSelectionStore.getState().itemState[topic.id] === "selected") {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
useSelectionStore.getState().selectedItemsList.forEach((item) => {
|
||||
if (noteIds.indexOf(item.id) > -1) {
|
||||
count++;
|
||||
}
|
||||
});
|
||||
return count;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setShowSelectedIndicator(getSelectedNotesCountInNotebookTopics(item) > 0);
|
||||
}, [item]);
|
||||
|
||||
const onChangeSubItem = () => {
|
||||
setShowSelectedIndicator(getSelectedNotesCountInNotebookTopics(item) > 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
marginBottom: 10,
|
||||
...style
|
||||
}}
|
||||
>
|
||||
<PressableButton
|
||||
onPress={() => {
|
||||
if (hasSubList) return setExpanded(!expanded);
|
||||
if (multiSelect) return selectItem();
|
||||
onPress?.(item);
|
||||
}}
|
||||
type={type}
|
||||
onLongPress={() => {
|
||||
useItemSelectionStore.getState().setMultiSelect(true);
|
||||
selectItem();
|
||||
}}
|
||||
customStyle={{
|
||||
height: style?.height || 50,
|
||||
width: "100%",
|
||||
alignItems: "flex-start"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 50,
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<SelectionIndicator
|
||||
hasNotes={hasNotes}
|
||||
onPress={onPress}
|
||||
item={item}
|
||||
onChange={onChange}
|
||||
selectItem={selectItem}
|
||||
/>
|
||||
<View>
|
||||
{hasSubList && expanded ? (
|
||||
<Heading size={SIZE.md}>{item.title}</Heading>
|
||||
) : (
|
||||
<Paragraph size={SIZE.sm}>{item.title}</Paragraph>
|
||||
)}
|
||||
|
||||
{infoText ? (
|
||||
<Paragraph size={SIZE.xs} color={colors.primary.icon}>
|
||||
{infoText}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{showSelectedIndicator ? (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.primary.accent,
|
||||
width: 7,
|
||||
height: 7,
|
||||
borderRadius: 100,
|
||||
marginRight: 12
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{onAddSublistItem ? (
|
||||
<IconButton
|
||||
name={"plus"}
|
||||
testID="add-item-icon"
|
||||
color={colors.primary.paragraph}
|
||||
size={SIZE.xl}
|
||||
onPress={() => {
|
||||
onAddSublistItem(item);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{icon ? (
|
||||
<IconButton
|
||||
name={icon(expanded).name}
|
||||
color={icon(expanded).color}
|
||||
size={icon(expanded).size || SIZE.xl}
|
||||
onPress={
|
||||
hasSubList
|
||||
? () => setExpanded(!expanded)
|
||||
: icon(expanded).onPress
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</PressableButton>
|
||||
|
||||
{expanded && hasSubList ? (
|
||||
<FilteredList
|
||||
nestedScrollEnabled
|
||||
data={getListItems(item)}
|
||||
keyboardShouldPersistTaps="always"
|
||||
keyboardDismissMode="none"
|
||||
onMomentumScrollEnd={onScrollEnd}
|
||||
style={{
|
||||
width: "95%",
|
||||
alignSelf: "flex-end",
|
||||
maxHeight: 250
|
||||
}}
|
||||
estimatedItemSize={40}
|
||||
itemType={sublistItemType}
|
||||
hasHeaderSearch={hasHeaderSearch}
|
||||
renderItem={({ item, index }) => (
|
||||
<ListItem
|
||||
item={item}
|
||||
{...getSublistItemProps(item)}
|
||||
index={index}
|
||||
onChange={onChangeSubItem}
|
||||
onScrollEnd={onScrollEnd}
|
||||
/>
|
||||
)}
|
||||
onAddItem={onAddItem}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -1,247 +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 { Notebook, VirtualizedGrouping } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useMemo } from "react";
|
||||
import { View, useWindowDimensions } from "react-native";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { useTotalNotes } from "../../../hooks/use-db-item";
|
||||
import { useNotebook } from "../../../hooks/use-notebook";
|
||||
import useNavigationStore from "../../../stores/use-navigation-store";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import { PressableButton } from "../../ui/pressable";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { AddNotebookSheet } from "../add-notebook";
|
||||
import {
|
||||
useNotebookExpandedStore,
|
||||
useNotebookItemSelectionStore
|
||||
} from "./store";
|
||||
|
||||
type NotebookParentProp = {
|
||||
parent?: NotebookParentProp;
|
||||
item?: Notebook;
|
||||
};
|
||||
|
||||
export const NotebookItem = ({
|
||||
id,
|
||||
currentLevel = 0,
|
||||
index,
|
||||
parent,
|
||||
items
|
||||
}: {
|
||||
id: string;
|
||||
currentLevel?: number;
|
||||
index: number;
|
||||
parent?: NotebookParentProp;
|
||||
items?: VirtualizedGrouping<Notebook>;
|
||||
}) => {
|
||||
const { nestedNotebooks, notebook: item } = useNotebook(id, items);
|
||||
const ids = useMemo(() => (id ? [id] : []), [id]);
|
||||
const { totalNotes: totalNotes } = useTotalNotes(ids, "notebook");
|
||||
const focusedRouteId = useNavigationStore((state) => state.focusedRouteId);
|
||||
const { colors } = useThemeColors("sheet");
|
||||
const selection = useNotebookItemSelectionStore((state) =>
|
||||
id ? state.selection[id] : undefined
|
||||
);
|
||||
const isSelected = selection === "selected";
|
||||
const isFocused = focusedRouteId === id;
|
||||
const { fontScale } = useWindowDimensions();
|
||||
const expanded = useNotebookExpandedStore((state) => state.expanded[id]);
|
||||
|
||||
const onPress = () => {
|
||||
if (!item) return;
|
||||
const state = useNotebookItemSelectionStore.getState();
|
||||
|
||||
if (isSelected) {
|
||||
state.markAs(item, !state.initialState[id] ? undefined : "deselected");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.multiSelect) {
|
||||
const keys = Object.keys(state.selection);
|
||||
const nextState: any = {};
|
||||
for (const key in keys) {
|
||||
nextState[key] = !state.initialState[key] ? undefined : "deselected";
|
||||
}
|
||||
console.log("Single item selection");
|
||||
state.setSelection({
|
||||
[item.id]: "selected",
|
||||
...nextState
|
||||
});
|
||||
} else {
|
||||
console.log("Multi item selection");
|
||||
state.markAs(item, "selected");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingLeft: currentLevel > 0 && currentLevel < 6 ? 15 : undefined,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<PressableButton
|
||||
type={"transparent"}
|
||||
onLongPress={() => {
|
||||
if (!item) return;
|
||||
useNotebookItemSelectionStore.setState({
|
||||
multiSelect: true
|
||||
});
|
||||
const state = useNotebookItemSelectionStore.getState();
|
||||
useNotebookItemSelectionStore
|
||||
.getState()
|
||||
.markAs(
|
||||
item,
|
||||
!isSelected
|
||||
? "selected"
|
||||
: !state.initialState[id]
|
||||
? undefined
|
||||
: "deselected"
|
||||
);
|
||||
}}
|
||||
testID={`add-to-notebook-item-${currentLevel}-${index}`}
|
||||
onPress={onPress}
|
||||
customStyle={{
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12,
|
||||
borderRadius: 0,
|
||||
height: 45
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{nestedNotebooks?.ids.length ? (
|
||||
<IconButton
|
||||
size={SIZE.xl}
|
||||
color={isSelected ? colors.selected.icon : colors.primary.icon}
|
||||
onPress={() => {
|
||||
useNotebookExpandedStore.getState().setExpanded(id);
|
||||
}}
|
||||
top={0}
|
||||
left={0}
|
||||
bottom={0}
|
||||
right={0}
|
||||
customStyle={{
|
||||
width: 35,
|
||||
height: 35
|
||||
}}
|
||||
name={expanded ? "chevron-down" : "chevron-right"}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<IconButton
|
||||
size={SIZE.xl}
|
||||
color={
|
||||
isSelected
|
||||
? colors.selected.icon
|
||||
: selection === "deselected"
|
||||
? colors.error.accent
|
||||
: colors.primary.icon
|
||||
}
|
||||
onPress={onPress}
|
||||
top={0}
|
||||
left={0}
|
||||
bottom={0}
|
||||
right={0}
|
||||
customStyle={{
|
||||
width: 40,
|
||||
height: 40
|
||||
}}
|
||||
name={
|
||||
selection === "deselected"
|
||||
? "close-circle-outline"
|
||||
: isSelected
|
||||
? "check-circle-outline"
|
||||
: selection === "intermediate"
|
||||
? "minus-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
color={
|
||||
isFocused ? colors.selected.paragraph : colors.secondary.paragraph
|
||||
}
|
||||
size={SIZE.sm}
|
||||
>
|
||||
{item?.title}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
columnGap: 10,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{totalNotes?.(id) ? (
|
||||
<Paragraph size={SIZE.sm} color={colors.secondary.paragraph}>
|
||||
{totalNotes(id)}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
<IconButton
|
||||
name="plus"
|
||||
customStyle={{
|
||||
width: 40 * fontScale,
|
||||
height: 40 * fontScale
|
||||
}}
|
||||
testID={notesnook.ids.notebook.menu}
|
||||
onPress={() => {
|
||||
if (!item) return;
|
||||
AddNotebookSheet.present(undefined, item, "link-notebooks");
|
||||
}}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
top={0}
|
||||
color={colors.primary.icon}
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
</View>
|
||||
</PressableButton>
|
||||
|
||||
{!expanded
|
||||
? null
|
||||
: nestedNotebooks?.ids.map((id, index) => (
|
||||
<NotebookItem
|
||||
key={id as string}
|
||||
id={id as string}
|
||||
index={index}
|
||||
currentLevel={currentLevel + 1}
|
||||
items={nestedNotebooks}
|
||||
parent={{
|
||||
parent: parent,
|
||||
item: item
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -16,24 +16,27 @@ 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 create from "zustand";
|
||||
import { createItemSelectionStore } from "../../../stores/item-selection-store";
|
||||
import create, { State } from "zustand";
|
||||
|
||||
export const useNotebookExpandedStore = create<{
|
||||
expanded: {
|
||||
[id: string]: boolean;
|
||||
};
|
||||
setExpanded: (id: string) => void;
|
||||
}>((set, get) => ({
|
||||
expanded: {},
|
||||
setExpanded(id: string) {
|
||||
type SelectionItemState = Record<
|
||||
string,
|
||||
"intermediate" | "selected" | "deselected"
|
||||
>;
|
||||
|
||||
export interface SelectionStore extends State {
|
||||
itemState: SelectionItemState;
|
||||
setItemState: (state: SelectionItemState) => void;
|
||||
multiSelect: boolean;
|
||||
setMultiSelect: (multiSelect: boolean) => void;
|
||||
}
|
||||
|
||||
export const useItemSelectionStore = create<SelectionStore>((set) => ({
|
||||
itemState: {},
|
||||
setItemState: (itemState) => {
|
||||
set({
|
||||
expanded: {
|
||||
...get().expanded,
|
||||
[id]: !get().expanded[id]
|
||||
}
|
||||
itemState
|
||||
});
|
||||
}
|
||||
},
|
||||
multiSelect: false,
|
||||
setMultiSelect: (multiSelect) => set({ multiSelect })
|
||||
}));
|
||||
|
||||
export const useNotebookItemSelectionStore = createItemSelectionStore(true);
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
eSendEvent,
|
||||
presentSheet,
|
||||
PresentSheetOptions,
|
||||
ToastManager
|
||||
ToastEvent
|
||||
} from "../../../services/event-manager";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import { Button } from "../../ui/button";
|
||||
@@ -87,7 +87,7 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
|
||||
);
|
||||
eSendEvent(eUserLoggedIn);
|
||||
close?.();
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: `Email changed`,
|
||||
message: `Your account email has been updated to ${emailChangeData.current.email}`,
|
||||
type: "success",
|
||||
@@ -96,7 +96,7 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
|
||||
}
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
ToastManager.error(e as Error);
|
||||
ToastEvent.error(e as Error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -17,27 +17,20 @@ 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, { Fragment, useState } from "react";
|
||||
import { ActivityIndicator, Platform, StyleSheet, View } from "react-native";
|
||||
import { Platform, StyleSheet, View, ActivityIndicator } from "react-native";
|
||||
import FileViewer from "react-native-file-viewer";
|
||||
import Share from "react-native-share";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { db } from "../../../common/database";
|
||||
import { requestInAppReview } from "../../../services/app-review";
|
||||
import {
|
||||
PresentSheetOptions,
|
||||
ToastManager,
|
||||
eSendEvent,
|
||||
presentSheet
|
||||
} from "../../../services/event-manager";
|
||||
import { presentSheet, ToastEvent } from "../../../services/event-manager";
|
||||
import Exporter from "../../../services/exporter";
|
||||
import PremiumService from "../../../services/premium";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
import { getElevationStyle } from "../../../utils/elevation";
|
||||
import { eCloseSheet } from "../../../utils/events";
|
||||
import { SIZE, ph, pv } from "../../../utils/size";
|
||||
import { ph, pv, SIZE } from "../../../utils/size";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import { ProTag } from "../../premium/pro-tag";
|
||||
@@ -47,48 +40,38 @@ import { PressableButton } from "../../ui/pressable";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { eSendEvent } from "../../../services/event-manager";
|
||||
import { eCloseSheet } from "../../../utils/events";
|
||||
import { requestInAppReview } from "../../../services/app-review";
|
||||
import { Dialog } from "../../dialog";
|
||||
|
||||
const ExportNotesSheet = ({
|
||||
ids,
|
||||
update
|
||||
}: {
|
||||
ids: string[];
|
||||
update: ((props: PresentSheetOptions) => void) | undefined;
|
||||
}) => {
|
||||
const ExportNotesSheet = ({ notes, update }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const [complete, setComplete] = useState(false);
|
||||
const [result, setResult] = useState<
|
||||
| {
|
||||
fileName: string;
|
||||
filePath: string;
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
| undefined
|
||||
>();
|
||||
const [result, setResult] = useState({});
|
||||
const [status, setStatus] = useState(null);
|
||||
const premium = useUserStore((state) => state.premium);
|
||||
|
||||
const save = async (type: "pdf" | "txt" | "md" | "html") => {
|
||||
const save = async (type) => {
|
||||
if (exporting) return;
|
||||
if (!PremiumService.get() && type !== "txt") return;
|
||||
setExporting(true);
|
||||
update?.({ disableClosing: true } as PresentSheetOptions);
|
||||
update({ disableClosing: true });
|
||||
setComplete(false);
|
||||
let result;
|
||||
if (ids.length > 1) {
|
||||
result = await Exporter.bulkExport(ids, type, setStatus);
|
||||
if (notes.length > 1) {
|
||||
result = await Exporter.bulkExport(notes, type, setStatus);
|
||||
} else {
|
||||
result = await Exporter.exportNote(ids[0], type);
|
||||
result = await Exporter.exportNote(notes[0], type);
|
||||
await sleep(1000);
|
||||
}
|
||||
if (!result) {
|
||||
update?.({ disableClosing: false } as PresentSheetOptions);
|
||||
update({ disableClosing: false });
|
||||
return setExporting(false);
|
||||
}
|
||||
setResult(result as any);
|
||||
update?.({ disableClosing: false } as PresentSheetOptions);
|
||||
setResult(result);
|
||||
update({ disableClosing: false });
|
||||
setComplete(true);
|
||||
setExporting(false);
|
||||
requestInAppReview();
|
||||
@@ -159,7 +142,9 @@ const ExportNotesSheet = ({
|
||||
<DialogHeader
|
||||
icon="export"
|
||||
title={
|
||||
ids.length > 1 ? `Export ${ids.length} Notes` : "Export Note"
|
||||
notes.length > 1
|
||||
? `Export ${notes.length} Notes`
|
||||
: "Export Note"
|
||||
}
|
||||
paragraph={`All exports are saved in ${
|
||||
Platform.OS === "android"
|
||||
@@ -173,6 +158,8 @@ const ExportNotesSheet = ({
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<Dialog context="export-notes" />
|
||||
|
||||
<View style={styles.buttonContainer}>
|
||||
{!exporting && !complete ? (
|
||||
actions.map((item) => (
|
||||
@@ -245,7 +232,7 @@ const ExportNotesSheet = ({
|
||||
<>
|
||||
<ActivityIndicator />
|
||||
<Paragraph>
|
||||
{ids.length === 1
|
||||
{notes.length === 1
|
||||
? "Exporting note... Please wait"
|
||||
: `Exporting notes${
|
||||
status ? ` (${status})` : ``
|
||||
@@ -270,8 +257,8 @@ const ExportNotesSheet = ({
|
||||
}}
|
||||
color={colors.secondary.heading}
|
||||
>
|
||||
{ids.length > 1
|
||||
? `${ids.length} Notes exported`
|
||||
{notes.length > 1
|
||||
? `${notes.length} Notes exported`
|
||||
: "Note exported"}
|
||||
</Heading>
|
||||
<Paragraph
|
||||
@@ -279,8 +266,8 @@ const ExportNotesSheet = ({
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
Your {ids.length > 1 ? "notes are" : "note is"} exported
|
||||
successfully as {result?.fileName}
|
||||
Your {notes.length > 1 ? "notes are" : "note is"} exported
|
||||
successfully as {result.fileName}
|
||||
</Paragraph>
|
||||
<Button
|
||||
title="Open"
|
||||
@@ -292,15 +279,14 @@ const ExportNotesSheet = ({
|
||||
borderRadius: 100
|
||||
}}
|
||||
onPress={async () => {
|
||||
if (!result?.filePath) return;
|
||||
eSendEvent(eCloseSheet);
|
||||
await sleep(500);
|
||||
FileViewer.open(result?.filePath, {
|
||||
FileViewer.open(result.filePath, {
|
||||
showOpenWithDialog: true,
|
||||
showAppsSuggestions: true
|
||||
}).catch((e) => {
|
||||
console.log(e);
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Cannot open",
|
||||
message: `No application found to open ${result.name} file.`,
|
||||
type: "success",
|
||||
@@ -319,7 +305,6 @@ const ExportNotesSheet = ({
|
||||
borderRadius: 100
|
||||
}}
|
||||
onPress={async () => {
|
||||
if (!result?.filePath) return;
|
||||
if (Platform.OS === "ios") {
|
||||
Share.open({
|
||||
url: result.filePath
|
||||
@@ -329,7 +314,7 @@ const ExportNotesSheet = ({
|
||||
showOpenWithDialog: true,
|
||||
showAppsSuggestions: true,
|
||||
shareFile: true
|
||||
} as any).catch(console.log);
|
||||
}).catch(console.log);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -344,7 +329,7 @@ const ExportNotesSheet = ({
|
||||
}}
|
||||
onPress={async () => {
|
||||
setComplete(false);
|
||||
setResult(undefined);
|
||||
setResult(null);
|
||||
setExporting(false);
|
||||
}}
|
||||
/>
|
||||
@@ -357,12 +342,15 @@ const ExportNotesSheet = ({
|
||||
);
|
||||
};
|
||||
|
||||
ExportNotesSheet.present = async (ids?: string[], allNotes?: boolean) => {
|
||||
const exportNoteIds = allNotes ? await db.notes.all?.ids() : ids || [];
|
||||
ExportNotesSheet.present = (notes, allNotes) => {
|
||||
presentSheet({
|
||||
component: (ref, close, update) => (
|
||||
<ExportNotesSheet ids={exportNoteIds} update={update} />
|
||||
)
|
||||
<ExportNotesSheet
|
||||
notes={allNotes ? db.notes.all : notes}
|
||||
update={update}
|
||||
/>
|
||||
),
|
||||
keyboardHandlerDisabled: true
|
||||
});
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import FileViewer from "react-native-file-viewer";
|
||||
import { ToastManager } from "../../../services/event-manager";
|
||||
import { ToastEvent } from "../../../services/event-manager";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Button } from "../../ui/button";
|
||||
export const ShareComponent = ({ uri, name, padding }) => {
|
||||
@@ -40,7 +40,7 @@ export const ShareComponent = ({ uri, name, padding }) => {
|
||||
showOpenWithDialog: true,
|
||||
showAppsSuggestions: true
|
||||
}).catch(() => {
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Cannot open",
|
||||
message: `No application found to open ${name} file.`,
|
||||
type: "success",
|
||||
|
||||
@@ -22,7 +22,7 @@ import React, { useRef, useState } from "react";
|
||||
import { Linking, Platform, Text, TextInput, View } from "react-native";
|
||||
import { getVersion } from "react-native-device-info";
|
||||
import { db } from "../../../common/database";
|
||||
import { eSendEvent, ToastManager } from "../../../services/event-manager";
|
||||
import { eSendEvent, ToastEvent } from "../../../services/event-manager";
|
||||
import PremiumService from "../../../services/premium";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
@@ -100,7 +100,7 @@ Logged in: ${user ? "yes" : "no"}`,
|
||||
positiveText: "Copy link",
|
||||
positivePress: () => {
|
||||
Clipboard.setString(issue_url);
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Issue url copied!",
|
||||
type: "success",
|
||||
context: "global"
|
||||
@@ -110,8 +110,8 @@ Logged in: ${user ? "yes" : "no"}`,
|
||||
});
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
heading: "An error occured",
|
||||
ToastEvent.show({
|
||||
heading: "An error occurred",
|
||||
message: e.message,
|
||||
type: "error"
|
||||
});
|
||||
|
||||
287
apps/mobile/app/components/sheets/manage-tags/index.js
Normal file
287
apps/mobile/app/components/sheets/manage-tags/index.js
Normal file
@@ -0,0 +1,287 @@
|
||||
/*
|
||||
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, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { ScrollView } from "react-native-actions-sheet";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../../common/database";
|
||||
import { ToastEvent, presentSheet } from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { useTagStore } from "../../../stores/use-tag-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import Input from "../../ui/input";
|
||||
import { PressableButton } from "../../ui/pressable";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
const ManageTagsSheet = (props) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [notes, setNotes] = useState(props.notes || []);
|
||||
const allTags = useTagStore((state) => state.tags);
|
||||
const [tags, setTags] = useState([]);
|
||||
const [query, setQuery] = useState(null);
|
||||
const inputRef = useRef();
|
||||
const [focus, setFocus] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
sortTags();
|
||||
}, [allTags, notes, query, sortTags]);
|
||||
|
||||
const sortTags = useCallback(() => {
|
||||
let _tags = [...allTags];
|
||||
_tags = _tags.filter((t) => t.type === "tag");
|
||||
_tags = _tags.sort((a, b) => a.title.localeCompare(b.title));
|
||||
if (query) {
|
||||
_tags = db.lookup.tags(_tags, query);
|
||||
}
|
||||
const tagsMerged = [...notes.map((note) => note.tags || []).flat()];
|
||||
|
||||
if (!tagsMerged || !tagsMerged.length) {
|
||||
setTags(_tags);
|
||||
return;
|
||||
}
|
||||
let noteTags = [];
|
||||
for (let tag of tagsMerged) {
|
||||
let index = _tags.findIndex((t) => t.title === tag);
|
||||
if (index !== -1) {
|
||||
noteTags.push(_tags[index]);
|
||||
_tags.splice(index, 1);
|
||||
}
|
||||
}
|
||||
noteTags = noteTags.sort((a, b) => a.title.localeCompare(b.title));
|
||||
let combinedTags = [...noteTags, ..._tags];
|
||||
setTags(combinedTags);
|
||||
}, [allTags, notes, query]);
|
||||
|
||||
useEffect(() => {
|
||||
useTagStore.getState().setTags();
|
||||
}, []);
|
||||
|
||||
const onSubmit = async () => {
|
||||
let _query = query;
|
||||
if (!_query || _query === "" || _query.trimStart().length == 0) {
|
||||
ToastEvent.show({
|
||||
heading: "Tag field is empty",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let tag = _query;
|
||||
setNotes(
|
||||
notes.map((note) => ({
|
||||
...note,
|
||||
tags: note.tags ? [...note.tags, tag] : [tag]
|
||||
}))
|
||||
);
|
||||
|
||||
setQuery(null);
|
||||
inputRef.current?.setNativeProps({
|
||||
text: ""
|
||||
});
|
||||
try {
|
||||
for (let note of notes) {
|
||||
await db.notes.note(note.id).tag(tag);
|
||||
}
|
||||
useTagStore.getState().setTags();
|
||||
setNotes(notes.map((note) => db.notes.note(note.id).data));
|
||||
} catch (e) {
|
||||
ToastEvent.show({
|
||||
heading: "Cannot add tag",
|
||||
type: "error",
|
||||
message: e.message,
|
||||
context: "local"
|
||||
});
|
||||
}
|
||||
|
||||
Navigation.queueRoutesForUpdate();
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 12,
|
||||
minHeight: focus ? "100%" : "60%"
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
button={{
|
||||
icon: "magnify",
|
||||
color: colors.primary.accent,
|
||||
size: SIZE.lg
|
||||
}}
|
||||
testID="tag-input"
|
||||
fwdRef={inputRef}
|
||||
autoCapitalize="none"
|
||||
onChangeText={(v) => {
|
||||
setQuery(db.tags.sanitize(v));
|
||||
}}
|
||||
onFocusInput={() => {
|
||||
setFocus(true);
|
||||
}}
|
||||
onBlurInput={() => {
|
||||
setFocus(false);
|
||||
}}
|
||||
onSubmit={onSubmit}
|
||||
placeholder="Search or add a tag"
|
||||
/>
|
||||
|
||||
<ScrollView
|
||||
overScrollMode="never"
|
||||
scrollToOverflowEnabled={false}
|
||||
keyboardDismissMode="none"
|
||||
keyboardShouldPersistTaps="always"
|
||||
>
|
||||
{query && query !== tags[0]?.title ? (
|
||||
<PressableButton
|
||||
key={"query_item"}
|
||||
customStyle={{
|
||||
flexDirection: "row",
|
||||
marginVertical: 5,
|
||||
justifyContent: "space-between",
|
||||
padding: 12
|
||||
}}
|
||||
onPress={onSubmit}
|
||||
type="selected"
|
||||
>
|
||||
<Heading size={SIZE.sm} color={colors.selected.heading}>
|
||||
Add {'"' + "#" + query + '"'}
|
||||
</Heading>
|
||||
<Icon name="plus" color={colors.selected.icon} size={SIZE.lg} />
|
||||
</PressableButton>
|
||||
) : null}
|
||||
{!allTags || allTags.length === 0 ? (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 200,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Heading size={50} color={colors.secondary.heading}>
|
||||
#
|
||||
</Heading>
|
||||
<Paragraph
|
||||
textBreakStrategy="balanced"
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
You do not have any tags.
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{tags.map((item) => (
|
||||
<TagItem
|
||||
key={item.title}
|
||||
tag={item}
|
||||
notes={notes}
|
||||
setNotes={setNotes}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
ManageTagsSheet.present = (notes) => {
|
||||
presentSheet({
|
||||
component: (ref) => {
|
||||
return <ManageTagsSheet actionSheetRef={ref} notes={notes} />;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default ManageTagsSheet;
|
||||
|
||||
const TagItem = ({ tag, notes, setNotes }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const someNotesTagged = notes.some(
|
||||
(note) => note.tags?.indexOf(tag.title) !== -1
|
||||
);
|
||||
const allNotesTagged = notes.every(
|
||||
(note) => note.tags?.indexOf(tag.title) !== -1
|
||||
);
|
||||
const onPress = async () => {
|
||||
for (let note of notes) {
|
||||
try {
|
||||
if (someNotesTagged) {
|
||||
await db.notes
|
||||
.note(note.id)
|
||||
.untag(note.tags[note.tags.indexOf(tag.title)]);
|
||||
} else {
|
||||
await db.notes.note(note.id).tag(tag.title);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
useTagStore.getState().setTags();
|
||||
setNotes(notes.map((note) => db.notes.note(note.id).data));
|
||||
setTimeout(() => {
|
||||
Navigation.queueRoutesForUpdate();
|
||||
}, 1);
|
||||
};
|
||||
return (
|
||||
<PressableButton
|
||||
customStyle={{
|
||||
flexDirection: "row",
|
||||
marginVertical: 5,
|
||||
justifyContent: "flex-start",
|
||||
height: 40
|
||||
}}
|
||||
onPress={onPress}
|
||||
type="gray"
|
||||
>
|
||||
<IconButton
|
||||
size={22}
|
||||
customStyle={{
|
||||
marginRight: 5,
|
||||
width: 23,
|
||||
height: 23
|
||||
}}
|
||||
color={
|
||||
someNotesTagged || allNotesTagged
|
||||
? colors.selected.icon
|
||||
: colors.primary.icon
|
||||
}
|
||||
testID={
|
||||
allNotesTagged
|
||||
? "check-circle-outline"
|
||||
: someNotesTagged
|
||||
? "minus-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
name={
|
||||
allNotesTagged
|
||||
? "check-circle-outline"
|
||||
: someNotesTagged
|
||||
? "minus-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
/>
|
||||
<Paragraph size={SIZE.sm}>{"#" + tag.alias}</Paragraph>
|
||||
</PressableButton>
|
||||
);
|
||||
};
|
||||
@@ -1,416 +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 { VirtualizedGrouping } from "@notesnook/core";
|
||||
import { Tags } from "@notesnook/core/dist/collections/tags";
|
||||
import { Tag } from "@notesnook/core/dist/types";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, {
|
||||
RefObject,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState
|
||||
} from "react";
|
||||
import { TextInput, View, useWindowDimensions } from "react-native";
|
||||
import { ActionSheetRef, FlatList } from "react-native-actions-sheet";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../../common/database";
|
||||
import { useDBItem } from "../../../hooks/use-db-item";
|
||||
import { ToastManager, presentSheet } from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import {
|
||||
ItemSelection,
|
||||
createItemSelectionStore
|
||||
} from "../../../stores/item-selection-store";
|
||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
import { useTagStore } from "../../../stores/use-tag-store";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import Input from "../../ui/input";
|
||||
import { PressableButton } from "../../ui/pressable";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
|
||||
async function updateInitialSelectionState(items: string[]) {
|
||||
const relations = await db.relations
|
||||
.to(
|
||||
{
|
||||
type: "note",
|
||||
ids: items
|
||||
},
|
||||
"tag"
|
||||
)
|
||||
.get();
|
||||
|
||||
const initialSelectionState: ItemSelection = {};
|
||||
const tagId = [...new Set(relations.map((relation) => relation.fromId))];
|
||||
|
||||
for (const id of tagId) {
|
||||
const all = items.every((noteId) => {
|
||||
return (
|
||||
relations.findIndex(
|
||||
(relation) => relation.fromId === id && relation.toId === noteId
|
||||
) > -1
|
||||
);
|
||||
});
|
||||
if (all) {
|
||||
initialSelectionState[id] = "selected";
|
||||
} else {
|
||||
initialSelectionState[id] = "intermediate";
|
||||
}
|
||||
}
|
||||
|
||||
return initialSelectionState;
|
||||
}
|
||||
|
||||
const useTagItemSelection = createItemSelectionStore(true);
|
||||
|
||||
const ManageTagsSheet = (props: {
|
||||
ids?: string[];
|
||||
actionSheetRef: RefObject<ActionSheetRef>;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const ids = useMemo(() => props.ids || [], [props.ids]);
|
||||
const [tags, setTags] = useState<VirtualizedGrouping<Tag>>();
|
||||
const [query, setQuery] = useState<string>();
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
const [focus, setFocus] = useState(false);
|
||||
const [queryExists, setQueryExists] = useState(false);
|
||||
const dimensions = useWindowDimensions();
|
||||
const refreshSelection = useCallback(() => {
|
||||
updateInitialSelectionState(ids).then((selection) => {
|
||||
useTagItemSelection.setState({
|
||||
initialState: selection,
|
||||
selection: { ...selection }
|
||||
});
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ids, tags]);
|
||||
|
||||
const refreshTags = useCallback(() => {
|
||||
if (query && query.trim() !== "") {
|
||||
db.lookup
|
||||
.tags(query)
|
||||
.sorted()
|
||||
.then((items) => {
|
||||
setTags(items);
|
||||
console.log("searched tags");
|
||||
});
|
||||
} else {
|
||||
db.tags.all.sorted(db.settings.getGroupOptions("tags")).then((items) => {
|
||||
console.log("items loaded tags");
|
||||
setTags(items);
|
||||
});
|
||||
}
|
||||
}, [query]);
|
||||
|
||||
useEffect(() => {
|
||||
refreshTags();
|
||||
}, [refreshTags, query]);
|
||||
|
||||
useEffect(() => {
|
||||
refreshSelection();
|
||||
}, [refreshSelection]);
|
||||
|
||||
const checkQueryExists = (query: string) => {
|
||||
db.tags.all
|
||||
.find((v) => v.and([v(`title`, "==", query)]))
|
||||
.then((exists) => setQueryExists(!!exists));
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!query || query === "" || query.trimStart().length == 0) {
|
||||
ToastManager.show({
|
||||
heading: "Tag field is empty",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const tag = query;
|
||||
inputRef.current?.setNativeProps({
|
||||
text: ""
|
||||
});
|
||||
|
||||
try {
|
||||
const exists = await db.tags.all.find((v) =>
|
||||
v.and([v(`title`, "==", tag)])
|
||||
);
|
||||
|
||||
const id = exists
|
||||
? exists?.id
|
||||
: await db.tags.add({
|
||||
title: tag
|
||||
});
|
||||
|
||||
if (id) {
|
||||
for (const noteId of ids) {
|
||||
await db.relations.add(
|
||||
{
|
||||
id: id,
|
||||
type: "tag"
|
||||
},
|
||||
{
|
||||
id: noteId,
|
||||
type: "note"
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
useRelationStore.getState().update();
|
||||
useTagStore.getState().setTags();
|
||||
setQuery(undefined);
|
||||
} catch (e) {
|
||||
ToastManager.show({
|
||||
heading: "Cannot add tag",
|
||||
type: "error",
|
||||
message: (e as Error).message,
|
||||
context: "local"
|
||||
});
|
||||
}
|
||||
|
||||
Navigation.queueRoutesForUpdate();
|
||||
};
|
||||
|
||||
const onPress = useCallback(
|
||||
async (id: string) => {
|
||||
for (const noteId of ids) {
|
||||
try {
|
||||
if (!id) return;
|
||||
const isSelected =
|
||||
useTagItemSelection.getState().initialState[id] === "selected";
|
||||
if (isSelected) {
|
||||
await db.relations.unlink(
|
||||
{
|
||||
id: id,
|
||||
type: "tag"
|
||||
},
|
||||
{
|
||||
id: noteId,
|
||||
type: "note"
|
||||
}
|
||||
);
|
||||
} else {
|
||||
await db.relations.add(
|
||||
{
|
||||
id: id,
|
||||
type: "tag"
|
||||
},
|
||||
{
|
||||
id: noteId,
|
||||
type: "note"
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
useTagStore.getState().setTags();
|
||||
useRelationStore.getState().update();
|
||||
refreshTags();
|
||||
setTimeout(() => {
|
||||
Navigation.queueRoutesForUpdate();
|
||||
}, 1);
|
||||
refreshSelection();
|
||||
},
|
||||
[ids, refreshSelection, refreshTags]
|
||||
);
|
||||
|
||||
const renderTag = useCallback(
|
||||
({ item }: { item: string; index: number }) => (
|
||||
<TagItem
|
||||
key={item as string}
|
||||
tags={tags as VirtualizedGrouping<Tag>}
|
||||
id={item as string}
|
||||
onPress={onPress}
|
||||
/>
|
||||
),
|
||||
[onPress, tags]
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 12,
|
||||
maxHeight: dimensions.height * 0.85
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
button={{
|
||||
icon: "magnify",
|
||||
color: colors.primary.accent,
|
||||
size: SIZE.lg,
|
||||
onPress: () => {}
|
||||
}}
|
||||
testID="tag-input"
|
||||
fwdRef={inputRef}
|
||||
autoCapitalize="none"
|
||||
onChangeText={(v) => {
|
||||
setQuery(Tags.sanitize(v));
|
||||
checkQueryExists(Tags.sanitize(v));
|
||||
}}
|
||||
onFocusInput={() => {
|
||||
setFocus(true);
|
||||
}}
|
||||
onBlurInput={() => {
|
||||
setFocus(false);
|
||||
}}
|
||||
onSubmit={() => {
|
||||
onSubmit();
|
||||
}}
|
||||
placeholder="Search or add a tag"
|
||||
/>
|
||||
|
||||
{query && !queryExists ? (
|
||||
<PressableButton
|
||||
key={"query_item"}
|
||||
customStyle={{
|
||||
flexDirection: "row",
|
||||
marginVertical: 5,
|
||||
justifyContent: "space-between",
|
||||
padding: 12
|
||||
}}
|
||||
onPress={onSubmit}
|
||||
type="selected"
|
||||
>
|
||||
<Heading size={SIZE.sm} color={colors.selected.heading}>
|
||||
Add {'"' + "#" + query + '"'}
|
||||
</Heading>
|
||||
<Icon name="plus" color={colors.selected.icon} size={SIZE.lg} />
|
||||
</PressableButton>
|
||||
) : null}
|
||||
|
||||
<FlatList
|
||||
data={tags?.ids?.filter((id) => typeof id === "string") as string[]}
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
keyboardShouldPersistTaps
|
||||
keyboardDismissMode="interactive"
|
||||
keyExtractor={(item) => item as string}
|
||||
renderItem={renderTag}
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 200,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Heading size={50} color={colors.secondary.heading}>
|
||||
#
|
||||
</Heading>
|
||||
<Paragraph
|
||||
textBreakStrategy="balanced"
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
You do not have any tags.
|
||||
</Paragraph>
|
||||
</View>
|
||||
}
|
||||
ListFooterComponent={<View style={{ height: 50 }} />}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
ManageTagsSheet.present = (ids?: string[]) => {
|
||||
presentSheet({
|
||||
component: (ref) => {
|
||||
return <ManageTagsSheet actionSheetRef={ref} ids={ids} />;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default ManageTagsSheet;
|
||||
|
||||
const TagItem = ({
|
||||
id,
|
||||
tags,
|
||||
onPress
|
||||
}: {
|
||||
id: string;
|
||||
tags: VirtualizedGrouping<Tag>;
|
||||
onPress: (id: string) => void;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [tag] = useDBItem(id, "tag", tags);
|
||||
const selection = useTagItemSelection((state) => state.selection[id]);
|
||||
|
||||
return (
|
||||
<PressableButton
|
||||
customStyle={{
|
||||
flexDirection: "row",
|
||||
marginVertical: 5,
|
||||
justifyContent: "flex-start",
|
||||
height: 40
|
||||
}}
|
||||
onPress={() => onPress(id)}
|
||||
type="gray"
|
||||
>
|
||||
{!tag ? null : (
|
||||
<Icon
|
||||
size={22}
|
||||
onPress={() => onPress(id)}
|
||||
color={
|
||||
selection === "selected" || selection === "intermediate"
|
||||
? colors.selected.icon
|
||||
: colors.primary.icon
|
||||
}
|
||||
style={{
|
||||
marginRight: 6
|
||||
}}
|
||||
testID={
|
||||
selection === "selected"
|
||||
? "check-circle-outline"
|
||||
: selection === "intermediate"
|
||||
? "minus-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
name={
|
||||
selection === "selected"
|
||||
? "check-circle-outline"
|
||||
: selection === "intermediate"
|
||||
? "minus-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{tag ? (
|
||||
<Paragraph size={SIZE.sm}>{"#" + tag?.title}</Paragraph>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
width: 200,
|
||||
height: 30,
|
||||
// backgroundColor: colors.secondary.background,
|
||||
borderRadius: 5
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</PressableButton>
|
||||
);
|
||||
};
|
||||
@@ -26,7 +26,7 @@ import BackupService from "../../../services/backup";
|
||||
import {
|
||||
eSendEvent,
|
||||
presentSheet,
|
||||
ToastManager
|
||||
ToastEvent
|
||||
} from "../../../services/event-manager";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
@@ -94,7 +94,7 @@ export default function Migrate() {
|
||||
await sleep(1000);
|
||||
const backupSaved = await BackupService.run(false, "local");
|
||||
if (!backupSaved) {
|
||||
ToastManager.show({
|
||||
ToastEvent.show({
|
||||
heading: "Migration failed",
|
||||
message: "You must download a backup of your data before migrating.",
|
||||
context: "local"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user