mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-30 10:39:07 +02:00
Compare commits
116 Commits
3.0.8-beta
...
theme-engi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53f4e14ab7 | ||
|
|
936274bd6d | ||
|
|
c67b243049 | ||
|
|
fcaefe9b55 | ||
|
|
85fa49f53e | ||
|
|
703853bd8a | ||
|
|
9b046c6b6f | ||
|
|
d10e08a158 | ||
|
|
ad67947ad7 | ||
|
|
1574ae7a28 | ||
|
|
56201cb408 | ||
|
|
b3fc476352 | ||
|
|
fdd24a90d5 | ||
|
|
5140fd7108 | ||
|
|
becd852165 | ||
|
|
700f5e4205 | ||
|
|
4245ac1451 | ||
|
|
26e44798ec | ||
|
|
9648913a02 | ||
|
|
7091f64de8 | ||
|
|
e5ec15fd9c | ||
|
|
c067c73b33 | ||
|
|
d304f708c7 | ||
|
|
71cf9955cd | ||
|
|
c5fcfd6654 | ||
|
|
67a1c55420 | ||
|
|
6c54f28c0f | ||
|
|
c576671302 | ||
|
|
6e663bf913 | ||
|
|
0f999947ee | ||
|
|
ab458486a2 | ||
|
|
5e627b567f | ||
|
|
2ef7baab0a | ||
|
|
56ede7654a | ||
|
|
18ccbc0ba3 | ||
|
|
73f50a34f9 | ||
|
|
c0805b1915 | ||
|
|
3c9a62b8ed | ||
|
|
b61c094a5d | ||
|
|
7f7390810f | ||
|
|
09ea2f577b | ||
|
|
cbcd7a9e5a | ||
|
|
bca774dfd0 | ||
|
|
0b56bdb0c4 | ||
|
|
cdf481d581 | ||
|
|
c644f606f3 | ||
|
|
c45e029780 | ||
|
|
59422d3e9c | ||
|
|
f0df45bd82 | ||
|
|
c5ce153c00 | ||
|
|
3e7d68f524 | ||
|
|
e4eea50319 | ||
|
|
47e0c54ab0 | ||
|
|
1ff1319e8a | ||
|
|
16c46d3fc7 | ||
|
|
8362c39277 | ||
|
|
eedf347708 | ||
|
|
403b2809b7 | ||
|
|
6c551eef0a | ||
|
|
d34b354420 | ||
|
|
38a4652c29 | ||
|
|
228c1a4d3a | ||
|
|
7bca693c44 | ||
|
|
583fa3da49 | ||
|
|
2ff73e241d | ||
|
|
fe2c753068 | ||
|
|
19201186c5 | ||
|
|
b2dd6b750d | ||
|
|
25380ec052 | ||
|
|
81cc687e1e | ||
|
|
1dab6be32d | ||
|
|
b9bfeee126 | ||
|
|
a06fa9f00e | ||
|
|
b147afbefe | ||
|
|
7646f35e58 | ||
|
|
beb782b9ad | ||
|
|
6e3f706b3a | ||
|
|
f01d415415 | ||
|
|
b6417d0016 | ||
|
|
f1ab8e92bd | ||
|
|
80ffbc511d | ||
|
|
745bf65641 | ||
|
|
a0015af169 | ||
|
|
e6e775dbeb | ||
|
|
e082007a49 | ||
|
|
deced99339 | ||
|
|
b0acce7b4f | ||
|
|
e081e7ea09 | ||
|
|
35db89aa11 | ||
|
|
679a912e2b | ||
|
|
627259e129 | ||
|
|
70c8201355 | ||
|
|
064059278a | ||
|
|
959e7466a6 | ||
|
|
69bd277d87 | ||
|
|
5833832123 | ||
|
|
37187403d4 | ||
|
|
b24d52bbbd | ||
|
|
82142d11aa | ||
|
|
19b4d84c40 | ||
|
|
fd98b89900 | ||
|
|
1870406b5e | ||
|
|
64e5f63f70 | ||
|
|
02f75ac1cd | ||
|
|
52849a5bf3 | ||
|
|
f64c91faec | ||
|
|
c7ae14f239 | ||
|
|
9ab084d793 | ||
|
|
6514b98ce8 | ||
|
|
5a5cacee4f | ||
|
|
04451eee5d | ||
|
|
6e7558b364 | ||
|
|
9c02fed3af | ||
|
|
5d7195fb41 | ||
|
|
6cd1178347 | ||
|
|
b7b4a18624 |
@@ -33,8 +33,7 @@ const SCOPES = [
|
||||
"misc",
|
||||
"common",
|
||||
"global",
|
||||
"docs",
|
||||
"themebuilder"
|
||||
"docs"
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -19,6 +19,7 @@ runs:
|
||||
packages/crypto/package-lock.json
|
||||
packages/sodium/package-lock.json
|
||||
packages/clipper/package-lock.json
|
||||
packages/crypto-worker/package-lock.json
|
||||
packages/editor-mobile/package-lock.json
|
||||
packages/editor/package-lock.json
|
||||
packages/logger/package-lock.json
|
||||
|
||||
8
.github/workflows/android.publish.yml
vendored
8
.github/workflows/android.publish.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Publish @notesnook/android
|
||||
name: Publish @notesnook/mobile
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
@@ -33,15 +33,13 @@ jobs:
|
||||
|
||||
- name: Sign app bundle for Playstore release
|
||||
id: sign_app
|
||||
uses: r0adkll/sign-android-release@master
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
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
|
||||
@@ -66,8 +64,6 @@ 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: |
|
||||
|
||||
31
.github/workflows/desktop.publish.yml
vendored
31
.github/workflows/desktop.publish.yml
vendored
@@ -18,21 +18,6 @@ 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:
|
||||
@@ -86,7 +71,6 @@ jobs:
|
||||
build-macos:
|
||||
name: Build for macOS
|
||||
needs: build
|
||||
if: inputs.build-mac
|
||||
runs-on: macos-12
|
||||
|
||||
steps:
|
||||
@@ -113,7 +97,6 @@ jobs:
|
||||
- name: Get App Store Version
|
||||
id: appstore
|
||||
uses: streetwriters/appstore-connect-app-version@develop
|
||||
if: inputs.publish-apple
|
||||
with:
|
||||
app-id: ${{ steps.app_metadata.outputs.apple_app_id }}
|
||||
key-id: ${{ secrets.api_key_id }}
|
||||
@@ -179,7 +162,6 @@ jobs:
|
||||
build-linux:
|
||||
name: Build for Linux
|
||||
needs: build
|
||||
if: inputs.build-linux
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
@@ -238,22 +220,12 @@ 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
|
||||
|
||||
@@ -274,9 +246,6 @@ 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
67
.github/workflows/ios.publish.yml
vendored
@@ -1,67 +0,0 @@
|
||||
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
|
||||
31
.github/workflows/theme-builder.publish.yml
vendored
31
.github/workflows/theme-builder.publish.yml
vendored
@@ -1,31 +0,0 @@
|
||||
name: Publish @notesnook/theme-builder
|
||||
|
||||
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=themebuilder
|
||||
|
||||
- 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 build
|
||||
run: npm run build:theme-builder
|
||||
|
||||
- name: Publish to Cloudflare Pages
|
||||
run: npx --yes wrangler pages deploy --project-name=notesnook-theme-builder ./apps/theme-builder/build/
|
||||
4
.github/workflows/vericrypt.publish.yml
vendored
4
.github/workflows/vericrypt.publish.yml
vendored
@@ -24,8 +24,8 @@ jobs:
|
||||
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
|
||||
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate build
|
||||
- name: Generate desktop build
|
||||
run: npm run build:vericrypt
|
||||
|
||||
- name: Publish on Cloudflare Pages
|
||||
run: npx --yes wrangler pages deploy --project-name=vericrypt ./apps/vericrypt/build/
|
||||
run: npx --yes wrangler pages publish --project-name=vericrypt ./apps/vericrypt/build/
|
||||
|
||||
69
.github/workflows/web.beta.publish.yml
vendored
69
.github/workflows/web.beta.publish.yml
vendored
@@ -1,69 +0,0 @@
|
||||
name: Publish @notesnook/web Beta
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- '!v3-beta'
|
||||
paths-ignore:
|
||||
- "apps/mobile/**"
|
||||
- "packages/editor-mobile/**"
|
||||
- "apps/desktop/**"
|
||||
- "apps/vericrypt/**"
|
||||
- "servers/**"
|
||||
- "packages/clipper/**"
|
||||
- "extensions/web-clipper/**"
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
- ready_for_review
|
||||
- reopened
|
||||
- opened
|
||||
- synchronize
|
||||
branches:
|
||||
- master
|
||||
- '!v3-beta'
|
||||
paths-ignore:
|
||||
- "apps/mobile/**"
|
||||
- "packages/editor-mobile/**"
|
||||
- "apps/desktop/**"
|
||||
- "apps/vericrypt/**"
|
||||
- "servers/**"
|
||||
- "packages/clipper/**"
|
||||
- "extensions/web-clipper/**"
|
||||
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-beta-app
|
||||
directory: ./apps/web/build
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
github-environment: ${{ (github.ref == 'refs/heads/master' && 'beta') || 'preview' }}
|
||||
6
.github/workflows/web.publish.yml
vendored
6
.github/workflows/web.publish.yml
vendored
@@ -24,8 +24,8 @@ jobs:
|
||||
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
|
||||
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate build
|
||||
- name: Generate desktop build
|
||||
run: npm run build:web
|
||||
|
||||
- name: Publish to Cloudflare Pages
|
||||
run: npx --yes wrangler pages deploy --project-name=notesnook-app ./apps/web/build/
|
||||
- name: Publish on Cloudflare Pages
|
||||
run: npx --yes wrangler pages publish --project-name=notesnook-app ./apps/web/build/
|
||||
|
||||
40
.github/workflows/web.v3-beta.publish.yml
vendored
40
.github/workflows/web.v3-beta.publish.yml
vendored
@@ -1,40 +0,0 @@
|
||||
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,5 +9,4 @@ nx-cloud.env
|
||||
.env.local
|
||||
.nyc_output
|
||||
site
|
||||
node_modules.backup
|
||||
.nx
|
||||
node_modules.backup
|
||||
@@ -103,7 +103,7 @@ All commits must include valid scopes in the commit message. **Valid commit scop
|
||||
|
||||
**Packages:**
|
||||
|
||||
1. `crypto`: changes related to the cryptographic core
|
||||
1. `crypto`: changes related to the cryptographic core (including `@notesnook/crypto-worker`)
|
||||
2. `editor`: changes related to the editor (including `@notesnook/editor-mobile`)
|
||||
3. `logger`: changes related to the logger
|
||||
4. `theme`: changes related to the theme
|
||||
|
||||
@@ -46,6 +46,7 @@ Notesnook is built using the following technologies:
|
||||
| `@notesnook/editor-mobile` | [/packages/editor-mobile](/packages/editor-mobile) | A very thin wrapper around `@notesnook/editor` for mobile clients |
|
||||
| `@notesnook/logger` | [/packages/logger](/packages/logger) | Simple & pluggable logger |
|
||||
| `@notesnook/sodium` | [/packages/sodium](/packages/sodium) | Wrapper around libsodium to support Node.js & Browser |
|
||||
| `@notesnook/crypto-worker` | [/packages/crypto-worker](/packages/crypto-worker) | Helpers to use `@notesnook/crypto` from a Worker |
|
||||
| `@notesnook/streamable-fs` | [/packages/streamable-fs](/packages/streamable-fs) | Streaming interface around an IndexedDB based file system |
|
||||
| `@notesnook/theme` | [/packages/theme](/packages/theme) | The core theme used in web & desktop clients |
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
owner: streetwriters
|
||||
repo: notesnook
|
||||
provider: github
|
||||
31
apps/desktop/global.d.ts
vendored
31
apps/desktop/global.d.ts
vendored
@@ -19,40 +19,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
/* eslint-disable no-var */
|
||||
|
||||
import { BrowserWindow } from "electron";
|
||||
import {
|
||||
type FormData as FormDataType,
|
||||
type Headers as HeadersType,
|
||||
type Request as RequestType,
|
||||
type Response as ResponseType
|
||||
} from "undici";
|
||||
|
||||
declare global {
|
||||
var window: BrowserWindow | null;
|
||||
var RELEASE: boolean;
|
||||
var MAC_APP_STORE: boolean;
|
||||
|
||||
// Re-export undici fetch function and various classes to global scope.
|
||||
// These are classes and functions expected to be at global scope according to Node.js v18 API
|
||||
// documentation.
|
||||
// See: https://nodejs.org/dist/latest-v18.x/docs/api/globals.html
|
||||
// eslint-disable-next-line no-var
|
||||
export var {
|
||||
FormData,
|
||||
Headers,
|
||||
Request,
|
||||
Response,
|
||||
fetch
|
||||
}: typeof import("undici");
|
||||
|
||||
type FormData = FormDataType;
|
||||
type Headers = HeadersType;
|
||||
type Request = RequestType;
|
||||
type Response = ResponseType;
|
||||
}
|
||||
|
||||
// NOTE: the import in the global block above needs to be a var for this to work properly.
|
||||
globalThis.fetch = fetch;
|
||||
globalThis.FormData = FormData;
|
||||
globalThis.Headers = Headers;
|
||||
globalThis.Request = Request;
|
||||
globalThis.Response = Response;
|
||||
|
||||
1621
apps/desktop/package-lock.json
generated
1621
apps/desktop/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "2.6.16",
|
||||
"version": "2.5.8",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
@@ -12,25 +12,25 @@
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@notesnook/crypto": "file:../../packages/crypto",
|
||||
"@trpc/client": "10.38.3",
|
||||
"@trpc/server": "10.38.3",
|
||||
"@trpc/client": "10.31.0",
|
||||
"@trpc/server": "10.31.0",
|
||||
"electron-trpc": "0.5.2",
|
||||
"electron-updater": "6.1.4",
|
||||
"electron-updater": "6.1.1",
|
||||
"icojs": "^0.17.1",
|
||||
"typed-emitter": "^2.1.0",
|
||||
"yargs": "^17.6.2",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.16.1",
|
||||
"@types/node": "^18.15.0",
|
||||
"@types/yargs": "^17.0.24",
|
||||
"chokidar": "^3.5.3",
|
||||
"electron": "25.9.8",
|
||||
"electron-builder": "^24.9.1",
|
||||
"electron": "24.5.1",
|
||||
"electron-builder": "24.4.0",
|
||||
"electron-builder-notarize": "^1.5.1",
|
||||
"esbuild": "^0.17.19",
|
||||
"tree-kill": "^1.2.2",
|
||||
"undici": "^5.23.0"
|
||||
"node-fetch": "^3.3.1",
|
||||
"tree-kill": "^1.2.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"dmg-license": "^1.0.11"
|
||||
@@ -135,10 +135,6 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"signingHashAlgorithms": [
|
||||
"sha256"
|
||||
],
|
||||
"sign": "./sign.js",
|
||||
"icon": "assets/icons/app.ico"
|
||||
},
|
||||
"portable": {
|
||||
@@ -195,7 +191,6 @@
|
||||
"allowNativeWayland": true
|
||||
},
|
||||
"extraResources": [
|
||||
"app-update.yml",
|
||||
"./assets/**"
|
||||
],
|
||||
"extraMetadata": {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js b/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
||||
index fcb7f54..cb1c7f7 100644
|
||||
index 3ba5d6a..cf8e147 100644
|
||||
--- a/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
||||
+++ b/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
||||
@@ -116,6 +116,7 @@ class LinuxTargetHelper {
|
||||
@@ -105,6 +105,7 @@ class LinuxTargetHelper {
|
||||
StartupWMClass: appInfo.productName,
|
||||
...extra,
|
||||
...targetSpecificOptions.desktop,
|
||||
@@ -10,7 +10,7 @@ index fcb7f54..cb1c7f7 100644
|
||||
};
|
||||
const description = this.getDescription(targetSpecificOptions);
|
||||
if (!(0, builder_util_1.isEmptyOrSpaces)(description)) {
|
||||
@@ -159,6 +160,23 @@ class LinuxTargetHelper {
|
||||
@@ -148,6 +149,23 @@ class LinuxTargetHelper {
|
||||
data += `\n${name}=${desktopMeta[name]}`;
|
||||
}
|
||||
data += "\n";
|
||||
@@ -1,60 +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/>.
|
||||
*/
|
||||
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"
|
||||
});
|
||||
}
|
||||
@@ -29,8 +29,6 @@ import { dirname } from "path";
|
||||
import { resolvePath } from "../utils/resolve-path";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { AssetManager } from "../utils/asset-manager";
|
||||
import { isFlatpak } from "../utils";
|
||||
import { setupDesktopIntegration } from "../utils/desktop-integration";
|
||||
|
||||
const t = initTRPC.create();
|
||||
|
||||
@@ -46,39 +44,29 @@ const NotificationOptions = z.object({
|
||||
});
|
||||
|
||||
export const osIntegrationRouter = t.router({
|
||||
isFlatpak: t.procedure.query(() => isFlatpak()),
|
||||
|
||||
zoomFactor: t.procedure.query(() => config.zoomFactor),
|
||||
setZoomFactor: t.procedure.input(z.number()).mutation(({ input: factor }) => {
|
||||
globalThis.window?.webContents.setZoomFactor(factor);
|
||||
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() }))
|
||||
.mutation(({ input: { enabled } }) => {
|
||||
.input(z.boolean().optional())
|
||||
.mutation(({ input: privacyMode }) => {
|
||||
if (!globalThis.window || !["win32", "darwin"].includes(process.platform))
|
||||
return;
|
||||
|
||||
globalThis.window.setContentProtection(enabled);
|
||||
globalThis.window.setContentProtection(!!privacyMode);
|
||||
|
||||
if (process.platform === "win32") {
|
||||
globalThis.window.setThumbnailClip(
|
||||
enabled
|
||||
privacyMode
|
||||
? { x: 0, y: 0, width: 1, height: 1 }
|
||||
: { x: 0, y: 0, width: 0, height: 0 }
|
||||
);
|
||||
}
|
||||
config.privacyMode = enabled;
|
||||
config.privacyMode = !!privacyMode;
|
||||
}),
|
||||
|
||||
desktopIntegration: t.procedure.query(() => config.desktopSettings),
|
||||
@@ -91,7 +79,6 @@ export const osIntegrationRouter = t.router({
|
||||
AutoLaunch.disable();
|
||||
}
|
||||
config.desktopSettings = settings;
|
||||
setupDesktopIntegration(settings);
|
||||
}),
|
||||
|
||||
selectDirectory: t.procedure
|
||||
@@ -130,14 +117,6 @@ export const osIntegrationRouter = t.router({
|
||||
writeFileSync(resolvedPath, data);
|
||||
}),
|
||||
|
||||
resolvePath: t.procedure
|
||||
.input(z.object({ filePath: z.string() }))
|
||||
.query(({ input }) => {
|
||||
const { filePath } = input;
|
||||
if (!filePath) return;
|
||||
return resolvePath(filePath);
|
||||
}),
|
||||
|
||||
showNotification: t.procedure
|
||||
.input(NotificationOptions)
|
||||
.query(({ input }) => {
|
||||
|
||||
@@ -114,17 +114,9 @@ export const spellCheckerRouter = t.router({
|
||||
globalThis.window?.webContents.session.setSpellCheckerLanguages(languages)
|
||||
),
|
||||
toggle: t.procedure
|
||||
.input(z.object({ enabled: z.boolean() }))
|
||||
.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
|
||||
);
|
||||
})
|
||||
.input(z.boolean().optional())
|
||||
.mutation(({ input: enabled }) => {
|
||||
globalThis.window?.webContents.session.setSpellCheckerEnabled(!!enabled);
|
||||
config.isSpellCheckerEnabled = !!enabled;
|
||||
})
|
||||
});
|
||||
|
||||
@@ -41,9 +41,10 @@ export const updaterRouter = t.router({
|
||||
}),
|
||||
|
||||
toggleAutoUpdates: t.procedure
|
||||
.input(z.object({ enabled: z.boolean() }))
|
||||
.mutation(({ input: { enabled } }) => {
|
||||
config.automaticUpdates = enabled;
|
||||
.input(z.boolean().optional())
|
||||
.mutation(({ input }) => {
|
||||
config.automaticUpdates =
|
||||
input === undefined ? !config.automaticUpdates : input;
|
||||
}),
|
||||
|
||||
onChecking: createSubscription("checking-for-update"),
|
||||
|
||||
@@ -27,23 +27,19 @@ export type CLIOptions = {
|
||||
hidden: boolean;
|
||||
};
|
||||
|
||||
export async function parseArguments(argv: string[]): Promise<CLIOptions> {
|
||||
export async function parseArguments(): Promise<CLIOptions> {
|
||||
const result: CLIOptions = {
|
||||
note: false,
|
||||
notebook: false,
|
||||
reminder: false,
|
||||
hidden: false
|
||||
};
|
||||
const { hidden } = await yargs(hideBin(argv))
|
||||
const { hidden } = await yargs(hideBin(process.argv))
|
||||
.boolean("hidden")
|
||||
// have to account for this flag added on Windows when launching
|
||||
// via Jumplist
|
||||
.boolean("allow-file-access-from-files")
|
||||
.command("new", "Create a new item", (yargs) => {
|
||||
return yargs
|
||||
.command("note", "Create a new note", {}, () => {
|
||||
result.note = true;
|
||||
console.log("HERE!");
|
||||
})
|
||||
.command("notebook", "Create a new notebook", {}, () => {
|
||||
result.notebook = true;
|
||||
|
||||
@@ -17,13 +17,14 @@ 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 { app, BrowserWindow, nativeTheme, shell } from "electron";
|
||||
import { app, BrowserWindow, nativeTheme, session, shell } from "electron";
|
||||
import { isDevelopment } from "./utils";
|
||||
import { registerProtocol, PROTOCOL_URL } from "./utils/protocol";
|
||||
import { configureAutoUpdater } from "./utils/autoupdater";
|
||||
import { getBackgroundColor, getTheme, setTheme } from "./utils/theme";
|
||||
import { setupMenu } from "./utils/menu";
|
||||
import { WindowState } from "./utils/window-state";
|
||||
import { AutoLaunch } from "./utils/autolaunch";
|
||||
import { setupJumplist } from "./utils/jumplist";
|
||||
import { setupTray } from "./utils/tray";
|
||||
import { CLIOptions, parseArguments } from "./cli";
|
||||
@@ -32,15 +33,6 @@ import { createIPCHandler } from "electron-trpc/main";
|
||||
import { router, api } from "./api";
|
||||
import { config } from "./utils/config";
|
||||
import path from "path";
|
||||
import { bringToFront } from "./utils/bring-to-front";
|
||||
import { bridge } from "./api/bridge";
|
||||
import { setupDesktopIntegration } from "./utils/desktop-integration";
|
||||
|
||||
// only run a single instance
|
||||
if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
|
||||
console.log("Another instance is already running!");
|
||||
app.exit();
|
||||
}
|
||||
|
||||
if (process.platform == "win32" && process.env.PORTABLE_EXECUTABLE_DIR) {
|
||||
console.log("Portable app: true");
|
||||
@@ -50,21 +42,19 @@ if (process.platform == "win32" && process.env.PORTABLE_EXECUTABLE_DIR) {
|
||||
app.setPath("userData", path.join(root, "UserData"));
|
||||
}
|
||||
|
||||
// only run a single instance
|
||||
if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
|
||||
app.exit();
|
||||
}
|
||||
|
||||
if (process.platform === "win32") {
|
||||
app.setAppUserModelId(app.name);
|
||||
}
|
||||
|
||||
process.on("uncaughtException", (error) => {
|
||||
console.error("uncaughtException:", error);
|
||||
});
|
||||
process.on("unhandledRejection", (reason) => {
|
||||
console.error("unhandledRejection:", reason);
|
||||
});
|
||||
|
||||
app.commandLine.appendSwitch("lang", "en-US");
|
||||
|
||||
async function createWindow() {
|
||||
const cliOptions = await parseArguments(process.argv);
|
||||
const cliOptions = await parseArguments();
|
||||
setTheme(getTheme());
|
||||
|
||||
const mainWindowState = new WindowState({});
|
||||
@@ -77,7 +67,7 @@ async function createWindow() {
|
||||
darkTheme: getTheme() === "dark",
|
||||
backgroundColor: getBackgroundColor(),
|
||||
opacity: 0,
|
||||
autoHideMenuBar: false,
|
||||
autoHideMenuBar: true,
|
||||
icon: AssetManager.appIcon({
|
||||
size: 512,
|
||||
format: process.platform === "win32" ? "ico" : "png"
|
||||
@@ -91,9 +81,7 @@ async function createWindow() {
|
||||
}
|
||||
});
|
||||
|
||||
createIPCHandler({ router, windows: [mainWindow] });
|
||||
globalThis.window = mainWindow;
|
||||
mainWindow.setMenuBarVisibility(false);
|
||||
mainWindowState.manage(mainWindow);
|
||||
|
||||
if (cliOptions.hidden && !config.desktopSettings.minimizeToSystemTray)
|
||||
@@ -103,16 +91,16 @@ async function createWindow() {
|
||||
mainWindow.setOpacity(1);
|
||||
|
||||
if (config.privacyMode) {
|
||||
await api.integration.setPrivacyMode({ enabled: config.privacyMode });
|
||||
await api.integration.setPrivacyMode(config.privacyMode);
|
||||
}
|
||||
|
||||
await AssetManager.loadIcons();
|
||||
setupDesktopIntegration(config.desktopSettings);
|
||||
setupDesktopIntegration();
|
||||
createIPCHandler({ router, windows: [mainWindow] });
|
||||
|
||||
mainWindow.webContents.session.setSpellCheckerDictionaryDownloadURL(
|
||||
"http://dictionaries.notesnook.com/"
|
||||
);
|
||||
mainWindow.webContents.session.setProxy({ proxyRules: config.proxyRules });
|
||||
|
||||
mainWindow.once("closed", () => {
|
||||
globalThis.window = null;
|
||||
@@ -149,15 +137,6 @@ app.once("window-all-closed", () => {
|
||||
}
|
||||
});
|
||||
|
||||
app.on("second-instance", async (_ev, argv) => {
|
||||
if (!globalThis.window) return;
|
||||
const cliOptions = await parseArguments(argv);
|
||||
if (cliOptions.note) bridge.onCreateItem("note");
|
||||
if (cliOptions.notebook) bridge.onCreateItem("notebook");
|
||||
if (cliOptions.reminder) bridge.onCreateItem("reminder");
|
||||
bringToFront();
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
if (globalThis.window === null) {
|
||||
createWindow();
|
||||
@@ -178,3 +157,49 @@ function createURL(options: CLIOptions, path = "/") {
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
function setupDesktopIntegration() {
|
||||
const desktopIntegration = config.desktopSettings;
|
||||
|
||||
if (
|
||||
desktopIntegration.closeToSystemTray ||
|
||||
desktopIntegration.minimizeToSystemTray
|
||||
) {
|
||||
setupTray();
|
||||
}
|
||||
|
||||
// when close to system tray is enabled, it becomes nigh impossible
|
||||
// to "quit" the app. This is necessary in order to fix that.
|
||||
if (desktopIntegration.closeToSystemTray) {
|
||||
app.on("before-quit", () => app.exit(0));
|
||||
}
|
||||
|
||||
globalThis.window?.once("close", (e) => {
|
||||
if (config.desktopSettings.closeToSystemTray) {
|
||||
e.preventDefault();
|
||||
if (process.platform == "darwin") {
|
||||
// on macOS window cannot be minimized/hidden if it is already fullscreen
|
||||
// so we just close it.
|
||||
if (globalThis.window?.isFullScreen()) app.exit(0);
|
||||
else app.hide();
|
||||
} else {
|
||||
globalThis.window?.minimize();
|
||||
globalThis.window?.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
globalThis.window?.on("minimize", () => {
|
||||
if (config.desktopSettings.minimizeToSystemTray) {
|
||||
if (process.platform == "darwin") {
|
||||
app.hide();
|
||||
} else {
|
||||
globalThis.window?.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (desktopIntegration.autoStart) {
|
||||
AutoLaunch.enable(!!desktopIntegration.startMinimized);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ declare global {
|
||||
}
|
||||
|
||||
process.once("loaded", async () => {
|
||||
console.log("HELLO!");
|
||||
const electronTRPC: RendererGlobalElectronTRPC = {
|
||||
sendMessage: (operation) =>
|
||||
ipcRenderer.send(ELECTRON_TRPC_CHANNEL, operation),
|
||||
|
||||
@@ -25,11 +25,7 @@ Type=Application
|
||||
Version=${app.getVersion()}
|
||||
Name=${app.getName()}
|
||||
Comment=${app.getName()} startup script
|
||||
Exec=${
|
||||
process.env.APPIMAGE
|
||||
? `${process.env.APPIMAGE}${hidden ? " --hidden" : ""}`
|
||||
: `${process.execPath}${hidden ? " --hidden" : ""}`
|
||||
}
|
||||
Exec=${process.execPath}${hidden ? " --hidden" : ""}
|
||||
StartupNotify=false
|
||||
Terminal=false`;
|
||||
|
||||
@@ -39,8 +35,6 @@ const LINUX_AUTOSTART_DIRECTORY_PATH = path.join(
|
||||
"autostart"
|
||||
);
|
||||
|
||||
const STARTUP_ARGS = ["--hidden"];
|
||||
|
||||
export class AutoLaunch {
|
||||
static enable(hidden: boolean) {
|
||||
if (process.platform === "linux") {
|
||||
@@ -53,14 +47,10 @@ export class AutoLaunch {
|
||||
LINUX_DESKTOP_ENTRY(hidden)
|
||||
);
|
||||
} else {
|
||||
const loginItemSettings = app.getLoginItemSettings({
|
||||
args: STARTUP_ARGS
|
||||
});
|
||||
if (loginItemSettings.openAtLogin) return;
|
||||
app.setLoginItemSettings({
|
||||
openAtLogin: true,
|
||||
openAsHidden: hidden,
|
||||
args: STARTUP_ARGS
|
||||
args: ["--hidden"]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,11 +38,10 @@ export const config = {
|
||||
closeToSystemTray: false
|
||||
},
|
||||
privacyMode: false,
|
||||
isSpellCheckerEnabled: true,
|
||||
zoomFactor: 1,
|
||||
isSpellCheckerEnabled: false,
|
||||
zoomFactor: 0,
|
||||
theme: nativeTheme.themeSource,
|
||||
automaticUpdates: true,
|
||||
proxyRules: ""
|
||||
automaticUpdates: true
|
||||
};
|
||||
|
||||
type ConfigKey = keyof typeof config;
|
||||
|
||||
@@ -1,74 +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 { app } from "electron";
|
||||
import { DesktopIntegration, config } from "./config";
|
||||
import { setupTray, destroyTray } from "./tray";
|
||||
import { AutoLaunch } from "./autolaunch";
|
||||
|
||||
export function setupDesktopIntegration(
|
||||
desktopIntegration: DesktopIntegration
|
||||
) {
|
||||
if (
|
||||
desktopIntegration.closeToSystemTray ||
|
||||
desktopIntegration.minimizeToSystemTray
|
||||
) {
|
||||
setupTray();
|
||||
} else {
|
||||
destroyTray();
|
||||
}
|
||||
|
||||
// when close to system tray is enabled, it becomes nigh impossible
|
||||
// to "quit" the app. This is necessary in order to fix that.
|
||||
app.on("before-quit", () =>
|
||||
desktopIntegration.closeToSystemTray ? app.exit(0) : null
|
||||
);
|
||||
|
||||
globalThis.window?.on("close", (e) => {
|
||||
if (config.desktopSettings.closeToSystemTray) {
|
||||
e.preventDefault();
|
||||
if (process.platform == "darwin") {
|
||||
// on macOS window cannot be minimized/hidden if it is already fullscreen
|
||||
// so we just close it.
|
||||
if (globalThis.window?.isFullScreen()) app.exit(0);
|
||||
else app.hide();
|
||||
} else {
|
||||
try {
|
||||
globalThis.window?.minimize();
|
||||
globalThis.window?.hide();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
globalThis.window?.on("minimize", () => {
|
||||
if (config.desktopSettings.minimizeToSystemTray) {
|
||||
if (process.platform == "darwin") {
|
||||
app.hide();
|
||||
} else {
|
||||
globalThis.window?.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (desktopIntegration.autoStart) {
|
||||
AutoLaunch.enable(!!desktopIntegration.startMinimized);
|
||||
}
|
||||
}
|
||||
@@ -131,17 +131,6 @@ 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,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 { protocol } from "electron";
|
||||
import { protocol, ProtocolRequest } from "electron";
|
||||
import { isDevelopment } from "./index";
|
||||
import { createReadStream } from "fs";
|
||||
import { extname, normalize } from "path";
|
||||
import { URL } from "url";
|
||||
import fetch, { Response } from "node-fetch";
|
||||
|
||||
const BASE_PATH = isDevelopment() ? "../public" : "";
|
||||
const HOSTNAME = `app.notesnook.com`;
|
||||
const FILE_NOT_FOUND = -6;
|
||||
const SCHEME = "https";
|
||||
const extensionToMimeType: Record<string, string> = {
|
||||
html: "text/html",
|
||||
@@ -32,59 +34,101 @@ const extensionToMimeType: Record<string, string> = {
|
||||
js: "application/javascript",
|
||||
css: "text/css",
|
||||
svg: "image/svg+xml",
|
||||
png: "image/png",
|
||||
jpg: "image/jpg",
|
||||
ttf: "font/ttf",
|
||||
woff: "font/woff",
|
||||
woff2: "font/woff2"
|
||||
png: "image/png"
|
||||
};
|
||||
|
||||
function registerProtocol() {
|
||||
protocol.handle(SCHEME, async (request) => {
|
||||
const url = new URL(request.url);
|
||||
if (shouldInterceptRequest(url)) {
|
||||
console.info("Intercepting request:", request.url);
|
||||
const loadIndex = !extname(url.pathname);
|
||||
const filePath = normalize(
|
||||
`${__dirname}${
|
||||
loadIndex ? `${BASE_PATH}/index.html` : `${BASE_PATH}/${url.pathname}`
|
||||
}`
|
||||
);
|
||||
if (!filePath) {
|
||||
console.error("Local asset file not found at", filePath);
|
||||
return new Response(undefined, {
|
||||
status: 404,
|
||||
statusText: "FILE_NOT_FOUND"
|
||||
const protocolInterceptionResult = protocol.interceptStreamProtocol(
|
||||
SCHEME,
|
||||
async (request, callback) => {
|
||||
const url = new URL(request.url);
|
||||
if (shouldInterceptRequest(url)) {
|
||||
console.info("Intercepting request:", request.url);
|
||||
|
||||
const loadIndex = !extname(url.pathname);
|
||||
const filePath = normalize(
|
||||
`${__dirname}${
|
||||
loadIndex
|
||||
? `${BASE_PATH}/index.html`
|
||||
: `${BASE_PATH}/${url.pathname}`
|
||||
}`
|
||||
);
|
||||
if (!filePath) {
|
||||
console.error("Local asset file not found at", filePath);
|
||||
callback({ error: FILE_NOT_FOUND });
|
||||
return;
|
||||
}
|
||||
const fileExtension = extname(filePath).replace(".", "");
|
||||
|
||||
const data = createReadStream(filePath);
|
||||
callback({
|
||||
data,
|
||||
mimeType: extensionToMimeType[fileExtension]
|
||||
});
|
||||
} else {
|
||||
let response: Response;
|
||||
try {
|
||||
const body = await getBody(request);
|
||||
response = await fetch(request.url, {
|
||||
...request,
|
||||
body,
|
||||
headers: {
|
||||
...request.headers
|
||||
// origin: `${PROTOCOL}://${HOSTNAME}/`
|
||||
},
|
||||
referrer: request.referrer,
|
||||
redirect: "manual"
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.error(`Error sending request to `, request.url, "Error: ", e);
|
||||
callback({ statusCode: 400 });
|
||||
return;
|
||||
}
|
||||
callback({
|
||||
statusCode: response.status,
|
||||
data: response.body || undefined,
|
||||
headers: Object.fromEntries(response.headers.entries()),
|
||||
mimeType: response.headers.get("Content-Type") || undefined
|
||||
});
|
||||
}
|
||||
const fileExtension = extname(filePath).replace(".", "");
|
||||
return new Response(createReadStream(filePath), {
|
||||
headers: { "Content-Type": extensionToMimeType[fileExtension] }
|
||||
});
|
||||
} else {
|
||||
if (request.headers.has("X-Content-Length")) {
|
||||
request.headers.set(
|
||||
"Content-Length",
|
||||
request.headers.get("X-Content-Length") || "0"
|
||||
);
|
||||
request.headers.delete("X-Content-Length");
|
||||
}
|
||||
const headers = Object.fromEntries(request.headers.entries());
|
||||
|
||||
return await fetch(request.url, {
|
||||
signal: request.signal,
|
||||
mode: request.mode,
|
||||
headers,
|
||||
method: request.method,
|
||||
body: request.body,
|
||||
credentials: request.credentials,
|
||||
referrer: (request as any).referrer,
|
||||
duplex: "half",
|
||||
redirect: "manual"
|
||||
});
|
||||
}
|
||||
});
|
||||
console.info(`${SCHEME} protocol inteception "successful"`);
|
||||
);
|
||||
|
||||
console.info(
|
||||
`${SCHEME} protocol inteception ${
|
||||
protocolInterceptionResult ? "successful" : "failed"
|
||||
}.`
|
||||
);
|
||||
|
||||
// protocol.handle(SCHEME, (request) => {
|
||||
// const url = new URL(request.url);
|
||||
// if (shouldInterceptRequest(url)) {
|
||||
// console.info("Intercepting request:", request.url);
|
||||
// const loadIndex = !extname(url.pathname);
|
||||
// const absoluteFilePath = normalize(
|
||||
// `${__dirname}${
|
||||
// loadIndex ? `${BASE_PATH}/index.html` : `${BASE_PATH}/${url.pathname}`
|
||||
// }`
|
||||
// );
|
||||
// const filePath = getPath(absoluteFilePath);
|
||||
// if (!filePath) {
|
||||
// console.error("Local asset file not found at", filePath);
|
||||
// return new Response(undefined, {
|
||||
// status: 404,
|
||||
// statusText: "FILE_NOT_FOUND"
|
||||
// });
|
||||
// }
|
||||
// const fileExtension = extname(filePath).replace(".", "");
|
||||
// const data = createReadStream(filePath);
|
||||
// return new Response(data, {
|
||||
// headers: { "Content-Type": extensionToMimeType[fileExtension] }
|
||||
// });
|
||||
// } else {
|
||||
// return net.fetch(request);
|
||||
// }
|
||||
// });
|
||||
// console.info(`${SCHEME} protocol inteception "successful"`);
|
||||
}
|
||||
|
||||
const bypassedRoutes: string[] = [];
|
||||
@@ -95,3 +139,22 @@ function shouldInterceptRequest(url: URL) {
|
||||
|
||||
const PROTOCOL_URL = `${SCHEME}://${HOSTNAME}/`;
|
||||
export { registerProtocol, PROTOCOL_URL };
|
||||
|
||||
async function getBody(request: ProtocolRequest) {
|
||||
const session = globalThis?.window?.webContents?.session;
|
||||
|
||||
const blobParts = [];
|
||||
if (!request.uploadData || !request.uploadData.length) return null;
|
||||
for (const data of request.uploadData) {
|
||||
if (data.bytes) {
|
||||
blobParts.push(new Uint8Array(data.bytes));
|
||||
} else if (session && data.blobUUID) {
|
||||
const buffer = await session.getBlobData(data.blobUUID);
|
||||
if (!buffer) continue;
|
||||
blobParts.push(new Uint8Array(buffer));
|
||||
}
|
||||
}
|
||||
const blob = new Blob(blobParts);
|
||||
return blob;
|
||||
}
|
||||
|
||||
|
||||
1
apps/mobile/.gitignore
vendored
1
apps/mobile/.gitignore
vendored
@@ -5,6 +5,7 @@ artifacts/
|
||||
.DS_Store
|
||||
|
||||
native/android/app/src/main/assets/
|
||||
|
||||
*Issues.md
|
||||
build_cache/
|
||||
#
|
||||
|
||||
@@ -16,13 +16,12 @@ 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 "@azure/core-asynciterator-polyfill";
|
||||
import {
|
||||
THEME_COMPATIBILITY_VERSION,
|
||||
useThemeEngineStore
|
||||
} from "@notesnook/theme";
|
||||
import React, { useEffect } from "react";
|
||||
import { I18nManager, View } from "react-native";
|
||||
import { View } from "react-native";
|
||||
import "react-native-gesture-handler";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
@@ -31,17 +30,14 @@ 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.init();
|
||||
SettingsService.checkOrientation();
|
||||
const App = () => {
|
||||
const init = useAppEvents();
|
||||
@@ -50,7 +46,6 @@ const App = () => {
|
||||
if (appLockMode && appLockMode !== "none") {
|
||||
useUserStore.getState().lockApp(true);
|
||||
}
|
||||
globalThis["IS_MAIN_APP_RUNNING"] = true;
|
||||
init();
|
||||
setTimeout(async () => {
|
||||
SettingsService.onFirstLaunch();
|
||||
@@ -98,12 +93,6 @@ const App = () => {
|
||||
);
|
||||
};
|
||||
|
||||
let currTheme =
|
||||
useThemeStore.getState().colorScheme === "dark"
|
||||
? SettingsService.getProperty("darkTheme")
|
||||
: SettingsService.getProperty("lightTheme");
|
||||
useThemeEngineStore.getState().setTheme(currTheme);
|
||||
|
||||
export const withTheme = (Element) => {
|
||||
return function AppWithThemeProvider() {
|
||||
const [colorScheme, darkTheme, lightTheme] = useThemeStore((state) => [
|
||||
@@ -113,35 +102,31 @@ export const withTheme = (Element) => {
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
const currentTheme = colorScheme === "dark" ? darkTheme : lightTheme;
|
||||
if (!currentTheme) return;
|
||||
themeTrpcClient.updateTheme
|
||||
.query({
|
||||
version: currentTheme.version,
|
||||
compatibilityVersion: THEME_COMPATIBILITY_VERSION,
|
||||
id: currentTheme.id
|
||||
})
|
||||
.then((theme) => {
|
||||
if (theme) {
|
||||
console.log(theme.version, "theme updated");
|
||||
theme.colorScheme === "dark"
|
||||
? useThemeStore.getState().setDarkTheme(theme)
|
||||
: useThemeStore.getState().setLightTheme(theme);
|
||||
}
|
||||
})
|
||||
.catch(console.log);
|
||||
}, 1000);
|
||||
const currentTheme = colorScheme === "dark" ? darkTheme : lightTheme;
|
||||
if (!currentTheme) return;
|
||||
themeTrpcClient.updateTheme
|
||||
.query({
|
||||
version: currentTheme.version,
|
||||
compatibilityVersion: THEME_COMPATIBILITY_VERSION,
|
||||
id: currentTheme.id
|
||||
})
|
||||
.then((theme) => {
|
||||
if (theme) {
|
||||
console.log(theme.version, "theme updated");
|
||||
theme.colorScheme === "dark"
|
||||
? useThemeStore.getState().setDarkTheme(theme)
|
||||
: useThemeStore.getState().setLightTheme(theme);
|
||||
}
|
||||
})
|
||||
.catch(console.log);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const nextTheme = colorScheme === "dark" ? darkTheme : lightTheme;
|
||||
if (JSON.stringify(nextTheme) !== JSON.stringify(currTheme)) {
|
||||
useEffect(() => {
|
||||
useThemeEngineStore
|
||||
.getState()
|
||||
.setTheme(colorScheme === "dark" ? darkTheme : lightTheme);
|
||||
currTheme = nextTheme;
|
||||
}
|
||||
}, [colorScheme, darkTheme, lightTheme]);
|
||||
|
||||
return <Element />;
|
||||
};
|
||||
|
||||
@@ -126,18 +126,6 @@ export async function decrypt(password, data) {
|
||||
return await Sodium.decrypt(password, _data);
|
||||
}
|
||||
|
||||
export async function decryptMulti(password, data) {
|
||||
if (!password.password && !password.key) return undefined;
|
||||
if (password.password && password.password === "" && !password.key)
|
||||
return undefined;
|
||||
|
||||
data = data.map((d) => {
|
||||
d.output = "plain";
|
||||
return d;
|
||||
});
|
||||
return await Sodium.decryptMulti(password, data);
|
||||
}
|
||||
|
||||
export function parseAlgorithm(alg) {
|
||||
if (!alg) return {};
|
||||
const [enc, kdf, compressed, compressionAlg, base64variant] = alg.split("-");
|
||||
@@ -166,24 +154,3 @@ export async function encrypt(password, data) {
|
||||
alg: getAlgorithm(7)
|
||||
};
|
||||
}
|
||||
|
||||
export async function encryptMulti(password, data) {
|
||||
if (!password.password && !password.key) return undefined;
|
||||
if (password.password && password.password === "" && !password.key)
|
||||
return undefined;
|
||||
|
||||
let results = await Sodium.encryptMulti(
|
||||
password,
|
||||
data.map((item) => ({
|
||||
type: "plain",
|
||||
data: item
|
||||
}))
|
||||
);
|
||||
|
||||
return !results
|
||||
? []
|
||||
: results.map((result) => ({
|
||||
...result,
|
||||
alg: getAlgorithm(7)
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -17,47 +17,56 @@ 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 { database } from "@notesnook/common";
|
||||
import { logger as dbLogger } from "@notesnook/core/dist/logger";
|
||||
import { initalize, logger as dbLogger } from "@notesnook/core/logger";
|
||||
import { Platform } from "react-native";
|
||||
import * as Gzip from "react-native-gzip";
|
||||
import { MMKVLoader } from "react-native-mmkv-storage";
|
||||
import filesystem from "../filesystem";
|
||||
import EventSource from "../../utils/sse/even-source-ios";
|
||||
import AndroidEventSource from "../../utils/sse/event-source";
|
||||
import filesystem from "../filesystem";
|
||||
import "./logger";
|
||||
import Storage from "./storage";
|
||||
import Storage, { KV } from "./storage";
|
||||
import * as Gzip from "react-native-gzip";
|
||||
|
||||
database.host(
|
||||
__DEV__
|
||||
? {
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
AUTH_HOST: "https://auth.streetwriters.co",
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co"
|
||||
// API_HOST: "http://192.168.43.108:5264",
|
||||
// AUTH_HOST: "http://192.168.43.108:8264",
|
||||
// SSE_HOST: "http://192.168.43.108:7264",
|
||||
// SUBSCRIPTIONS_HOST: "http://192.168.43.108:9264",
|
||||
// ISSUES_HOST: "http://192.168.43.108:2624"
|
||||
}
|
||||
: {
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
AUTH_HOST: "https://auth.streetwriters.co",
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co"
|
||||
}
|
||||
);
|
||||
const LoggerStorage = new MMKVLoader()
|
||||
.withInstanceID("notesnook_logs")
|
||||
.initialize();
|
||||
|
||||
database.setup(
|
||||
Storage,
|
||||
Platform.OS === "ios" ? EventSource : AndroidEventSource,
|
||||
filesystem,
|
||||
{
|
||||
compress: Gzip.deflate,
|
||||
decompress: Gzip.inflate
|
||||
}
|
||||
);
|
||||
|
||||
database.host(
|
||||
__DEV__
|
||||
? {
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
AUTH_HOST: "https://auth.streetwriters.co",
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co"
|
||||
// API_HOST: "http://192.168.8.101:5264",
|
||||
// AUTH_HOST: "http://192.168.8.101:8264",
|
||||
// SSE_HOST: "http://192.168.8.101:7264",
|
||||
// SUBSCRIPTIONS_HOST: "http://192.168.8.101:9264",
|
||||
// ISSUES_HOST: "http://192.168.8.101:2624"
|
||||
}
|
||||
: {
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
AUTH_HOST: "https://auth.streetwriters.co",
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co"
|
||||
}
|
||||
);
|
||||
|
||||
database.setup(
|
||||
Storage,
|
||||
Platform.OS === "ios" ? EventSource : AndroidEventSource,
|
||||
filesystem,
|
||||
{
|
||||
compress: Gzip.deflate,
|
||||
decompress: Gzip.inflate
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
|
||||
initalize(new KV(LoggerStorage), true);
|
||||
|
||||
export const db = database;
|
||||
export const DatabaseLogger = dbLogger;
|
||||
|
||||
@@ -1,29 +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 { initalize } from "@notesnook/core/dist/logger";
|
||||
import { MMKVLoader } from "react-native-mmkv-storage";
|
||||
import { KV } from "./storage";
|
||||
|
||||
const LoggerStorage = new MMKVLoader()
|
||||
.withInstanceID("notesnook_logs")
|
||||
.initialize();
|
||||
|
||||
initalize(new KV(LoggerStorage));
|
||||
|
||||
export { LoggerStorage };
|
||||
@@ -18,13 +18,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Platform } from "react-native";
|
||||
import { ProcessingModes, MMKVLoader } from "react-native-mmkv-storage";
|
||||
import MMKVStorage, { ProcessingModes } from "react-native-mmkv-storage";
|
||||
|
||||
export const MMKV = new MMKVLoader()
|
||||
export const MMKV = new MMKVStorage.Loader()
|
||||
.setProcessingMode(
|
||||
Platform.OS === "ios"
|
||||
? ProcessingModes.MULTI_PROCESS
|
||||
: ProcessingModes.SINGLE_PROCESS
|
||||
)
|
||||
.disableIndexing()
|
||||
.initialize();
|
||||
|
||||
@@ -27,16 +27,11 @@ import {
|
||||
getCryptoKey,
|
||||
getRandomBytes,
|
||||
hash,
|
||||
removeCryptoKey,
|
||||
decryptMulti,
|
||||
encryptMulti
|
||||
removeCryptoKey
|
||||
} from "./encryption";
|
||||
import { MMKV } from "./mmkv";
|
||||
|
||||
export class KV {
|
||||
/**
|
||||
* @type {typeof MMKV}
|
||||
*/
|
||||
storage = null;
|
||||
constructor(storage) {
|
||||
this.storage = storage;
|
||||
@@ -58,7 +53,6 @@ export class KV {
|
||||
key,
|
||||
typeof data === "string" ? data : JSON.stringify(data)
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -66,36 +60,27 @@ export class KV {
|
||||
if (keys.length <= 0) {
|
||||
return [];
|
||||
} else {
|
||||
try {
|
||||
let data = await this.storage.getMultipleItemsAsync(
|
||||
keys.slice(),
|
||||
"string"
|
||||
);
|
||||
return data.map(([key, value]) => {
|
||||
let obj;
|
||||
try {
|
||||
obj = JSON.parse(value);
|
||||
} catch (e) {
|
||||
obj = value;
|
||||
}
|
||||
return [key, obj];
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
let data = await this.storage.getMultipleItemsAsync(keys.slice());
|
||||
|
||||
return data.map(([key, value]) => {
|
||||
let obj;
|
||||
try {
|
||||
obj = JSON.parse(value);
|
||||
} catch (e) {
|
||||
obj = value;
|
||||
}
|
||||
|
||||
return [key, obj];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async remove(key) {
|
||||
return this.storage.removeItem(key);
|
||||
}
|
||||
|
||||
async removeMulti(keys) {
|
||||
return this.storage.removeItems(...keys);
|
||||
return await this.storage.removeItem(key);
|
||||
}
|
||||
|
||||
async clear() {
|
||||
return this.storage.clearStore();
|
||||
return await this.storage.clearStore();
|
||||
}
|
||||
|
||||
async getAllKeys() {
|
||||
@@ -111,10 +96,6 @@ export class KV {
|
||||
);
|
||||
return keys;
|
||||
}
|
||||
|
||||
async writeMulti(items) {
|
||||
return this.storage.setMultipleItemsAsync(items, "object");
|
||||
}
|
||||
}
|
||||
|
||||
const DefaultStorage = new KV(MMKV);
|
||||
@@ -148,11 +129,8 @@ export default {
|
||||
remove: (key) => DefaultStorage.remove(key),
|
||||
clear: () => DefaultStorage.clear(),
|
||||
getAllKeys: () => DefaultStorage.getAllKeys(),
|
||||
writeMulti: (items) => DefaultStorage.writeMulti(items),
|
||||
removeMulti: (keys) => DefaultStorage.removeMulti(keys),
|
||||
encrypt,
|
||||
decrypt,
|
||||
decryptMulti,
|
||||
getRandomBytes,
|
||||
checkAndCreateDir,
|
||||
requestPermission,
|
||||
@@ -160,6 +138,5 @@ export default {
|
||||
getCryptoKey,
|
||||
removeCryptoKey,
|
||||
hash,
|
||||
generateCryptoKey,
|
||||
encryptMulti
|
||||
generateCryptoKey
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
getFileNameWithExtension,
|
||||
isImage,
|
||||
isDocument
|
||||
} from "@notesnook/core/dist/utils/filename";
|
||||
} from "@notesnook/core/utils/filename";
|
||||
import React from "react";
|
||||
import { Platform } from "react-native";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
@@ -34,8 +34,6 @@ import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { db } from "../database";
|
||||
import Storage from "../database/storage";
|
||||
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
|
||||
import { createCacheDir, exists } from "./io";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
|
||||
export const FileDownloadStatus = {
|
||||
Success: 1,
|
||||
@@ -64,7 +62,6 @@ export async function downloadAttachments(
|
||||
canceled,
|
||||
groupId
|
||||
) {
|
||||
await createCacheDir();
|
||||
if (!attachments || !attachments.length) return;
|
||||
const result = new Map();
|
||||
|
||||
@@ -182,13 +179,9 @@ export default async function downloadAttachment(
|
||||
silent: false,
|
||||
cache: false,
|
||||
throwError: false,
|
||||
groupId: undefined,
|
||||
base64: false,
|
||||
text: false
|
||||
groupId: undefined
|
||||
}
|
||||
) {
|
||||
await createCacheDir();
|
||||
|
||||
let attachment = db.attachments.attachment(hash);
|
||||
if (!attachment) {
|
||||
console.log("attachment not found");
|
||||
@@ -215,17 +208,10 @@ export default async function downloadAttachment(
|
||||
options.groupId || attachment.metadata.hash,
|
||||
attachment.metadata.hash
|
||||
);
|
||||
|
||||
if (!(await exists(attachment.metadata.hash))) {
|
||||
if (
|
||||
!(await RNFetchBlob.fs.exists(`${cacheDir}/${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,
|
||||
@@ -243,8 +229,7 @@ export default async function downloadAttachment(
|
||||
mime: attachment.metadata.type,
|
||||
fileName: options.cache ? undefined : filename,
|
||||
uri: options.cache ? undefined : folder.uri,
|
||||
chunkSize: attachment.chunkSize,
|
||||
appGroupId: IOS_APPGROUPID
|
||||
chunkSize: attachment.chunkSize
|
||||
};
|
||||
|
||||
let fileUri = await Sodium.decryptFile(
|
||||
|
||||
@@ -17,25 +17,22 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import hosts from "@notesnook/core/dist/utils/constants";
|
||||
import hosts from "@notesnook/core/utils/constants";
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { ToastEvent } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { db } from "../database";
|
||||
import { cacheDir, fileCheck } from "./utils";
|
||||
import { createCacheDir, exists } from "./io";
|
||||
|
||||
export async function downloadFile(filename, data, cancelToken) {
|
||||
if (!data) return false;
|
||||
|
||||
await createCacheDir();
|
||||
|
||||
let { url, headers } = data;
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
try {
|
||||
if (await exists(filename)) {
|
||||
let exists = await RNFetchBlob.fs.exists(path);
|
||||
if (exists) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -63,55 +60,44 @@ export async function downloadFile(filename, data, cancelToken) {
|
||||
console.log("downloading: ", recieved, total);
|
||||
});
|
||||
|
||||
cancelToken.cancel = () => {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
request.cancel();
|
||||
};
|
||||
cancelToken.cancel = request.cancel;
|
||||
let response = await request;
|
||||
await fileCheck(response, totalSize);
|
||||
let status = response.info().status;
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
return status >= 200 && status < 300;
|
||||
} catch (e) {
|
||||
if (e.message !== "canceled") {
|
||||
ToastEvent.show({
|
||||
heading: "Error downloading file",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "global"
|
||||
});
|
||||
ToastEvent.show({
|
||||
heading: "Error downloading file",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
}
|
||||
ToastEvent.show({
|
||||
heading: "Error downloading file",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "global"
|
||||
});
|
||||
ToastEvent.show({
|
||||
heading: "Error downloading file",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
RNFetchBlob.fs.unlink(path).catch(console.log);
|
||||
console.log("Download file error:", e, url, headers);
|
||||
console.log("download file error: ", e, url, headers);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUploadedFileSize(hash) {
|
||||
try {
|
||||
const url = `${hosts.API_HOST}/s3?name=${hash}`;
|
||||
const token = await db.user.tokenManager.getAccessToken();
|
||||
const url = `${hosts.API_HOST}/s3?name=${hash}`;
|
||||
const token = await db.user.tokenManager.getAccessToken();
|
||||
|
||||
const attachmentInfo = await fetch(url, {
|
||||
method: "HEAD",
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
const attachmentInfo = await fetch(url, {
|
||||
method: "HEAD",
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
|
||||
const contentLength = parseInt(
|
||||
attachmentInfo.headers?.get("content-length")
|
||||
);
|
||||
return isNaN(contentLength) ? 0 : contentLength;
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
const contentLength = parseInt(attachmentInfo.headers?.get("content-length"));
|
||||
return isNaN(contentLength) ? 0 : contentLength;
|
||||
}
|
||||
|
||||
export async function checkAttachment(hash) {
|
||||
|
||||
@@ -20,28 +20,31 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { Platform } from "react-native";
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { cacheDir, cacheDirOld, getRandomId } from "./utils";
|
||||
import { cacheDir, getRandomId } from "./utils";
|
||||
import { db } from "../database";
|
||||
import { compressToBase64 } from "./compress";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
|
||||
export async function readEncrypted(filename, key, cipherData) {
|
||||
await migrateFilesFromCache();
|
||||
console.log("Read encrypted file...");
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
|
||||
try {
|
||||
if (!(await exists(filename))) {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
let exists =
|
||||
(await RNFetchBlob.fs.exists(path)) ||
|
||||
(Platform.OS === "ios" && (await RNFetchBlob.fs.exists(appGroupPath)));
|
||||
|
||||
if (!exists) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const attachment = db.attachments.attachment(filename);
|
||||
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);
|
||||
const isPng = /(png)/g.test(attachment?.metadata.type);
|
||||
const isJpeg = /(jpeg|jpg)/g.test(attachment?.metadata.type);
|
||||
|
||||
let output = await Sodium.decryptFile(
|
||||
key,
|
||||
@@ -56,7 +59,6 @@ export async function readEncrypted(filename, key, cipherData) {
|
||||
: "base64"
|
||||
: "text"
|
||||
);
|
||||
console.log("file decrypted...");
|
||||
if (cipherData.outputType === "base64" && (isPng || isJpeg)) {
|
||||
const dCachePath = `${cacheDir}/${output}`;
|
||||
output = await compressToBase64(
|
||||
@@ -68,7 +70,7 @@ export async function readEncrypted(filename, key, cipherData) {
|
||||
return output;
|
||||
} catch (e) {
|
||||
RNFetchBlob.fs.unlink(path).catch(console.log);
|
||||
console.log("readEncrypted", e);
|
||||
console.log(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -86,7 +88,6 @@ export async function hashBase64(data) {
|
||||
}
|
||||
|
||||
export async function writeEncryptedBase64({ data, key }) {
|
||||
await createCacheDir();
|
||||
let filepath = cacheDir + `/${getRandomId("imagecache_")}`;
|
||||
await RNFetchBlob.fs.writeFile(filepath, data, "base64");
|
||||
let output = await Sodium.encryptFile(key, {
|
||||
@@ -102,7 +103,6 @@ export async function writeEncryptedBase64({ data, key }) {
|
||||
}
|
||||
|
||||
export async function deleteFile(filename, data) {
|
||||
await createCacheDir();
|
||||
let delFilePath = cacheDir + `/${filename}`;
|
||||
if (!data) {
|
||||
if (!filename) return;
|
||||
@@ -128,89 +128,19 @@ export async function deleteFile(filename, data) {
|
||||
export async function clearFileStorage() {
|
||||
try {
|
||||
let files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
let oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
|
||||
|
||||
for (let file of files) {
|
||||
await RNFetchBlob.fs.unlink(cacheDir + `/${file}`).catch(console.log);
|
||||
}
|
||||
for (let file of oldCache) {
|
||||
await RNFetchBlob.fs.unlink(cacheDirOld + `/${file}`).catch(console.log);
|
||||
try {
|
||||
await RNFetchBlob.fs.unlink(cacheDir + `/${file}`);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("clearFileStorage", e);
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createCacheDir() {
|
||||
if (!(await RNFetchBlob.fs.exists(cacheDir))) {
|
||||
await RNFetchBlob.fs.mkdir(cacheDir);
|
||||
console.log("Cache directory created");
|
||||
}
|
||||
}
|
||||
|
||||
export async function migrateFilesFromCache() {
|
||||
try {
|
||||
await createCacheDir();
|
||||
const migratedFilesPath = cacheDir + "/.migrated_1";
|
||||
const migrated = await RNFetchBlob.fs.exists(migratedFilesPath);
|
||||
if (migrated) {
|
||||
console.log("Files migrated already");
|
||||
return;
|
||||
}
|
||||
|
||||
let files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
console.log("Files to migrate:", files.join(","));
|
||||
|
||||
let oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
|
||||
for (let file of oldCache) {
|
||||
if (file.startsWith("org.") || file.startsWith("com.")) continue;
|
||||
RNFetchBlob.fs
|
||||
.mv(cacheDirOld + `/${file}`, cacheDir + `/${file}`)
|
||||
.catch(console.log);
|
||||
console.log("Moved", file);
|
||||
}
|
||||
await RNFetchBlob.fs.createFile(migratedFilesPath, "1", "utf8");
|
||||
} catch (e) {
|
||||
console.log("migrateFilesFromCache", e);
|
||||
}
|
||||
}
|
||||
|
||||
const ABYTES = 17;
|
||||
export async function exists(filename) {
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
|
||||
let exists = await RNFetchBlob.fs.exists(path);
|
||||
|
||||
// Check if file is present in app group path.
|
||||
let existsInAppGroup = false;
|
||||
if (!exists && Platform.OS === "ios") {
|
||||
existsInAppGroup = await RNFetchBlob.fs.exists(appGroupPath);
|
||||
}
|
||||
|
||||
if (exists || existsInAppGroup) {
|
||||
const attachment = db.attachments.attachment(filename);
|
||||
const totalChunks = Math.ceil(attachment.length / attachment.chunkSize);
|
||||
const totalAbytes = totalChunks * ABYTES;
|
||||
const expectedFileSize = attachment.length + totalAbytes;
|
||||
|
||||
const stat = await RNFetchBlob.fs.stat(
|
||||
existsInAppGroup ? appGroupPath : path
|
||||
);
|
||||
|
||||
if (stat.size !== expectedFileSize) {
|
||||
RNFetchBlob.fs
|
||||
.unlink(existsInAppGroup ? appGroupPath : path)
|
||||
.catch(console.log);
|
||||
return false;
|
||||
}
|
||||
|
||||
exists = true;
|
||||
}
|
||||
let exists = await RNFetchBlob.fs.exists(`${cacheDir}/${filename}`);
|
||||
return exists;
|
||||
}
|
||||
|
||||
@@ -21,15 +21,14 @@ import RNFetchBlob from "react-native-blob-util";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { cacheDir } from "./utils";
|
||||
import { isImage, isDocument } from "@notesnook/core/dist/utils/filename";
|
||||
import { isImage, isDocument } from "@notesnook/core/utils/filename";
|
||||
import { Platform } from "react-native";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { createCacheDir } from "./io";
|
||||
|
||||
export async function uploadFile(filename, data, cancelToken) {
|
||||
if (!data) return false;
|
||||
let { url, headers } = data;
|
||||
await createCacheDir();
|
||||
|
||||
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
|
||||
|
||||
try {
|
||||
@@ -73,10 +72,7 @@ export async function uploadFile(filename, data, cancelToken) {
|
||||
);
|
||||
});
|
||||
|
||||
cancelToken.cancel = () => {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
request.cancel();
|
||||
};
|
||||
cancelToken.cancel = request.cancel;
|
||||
let response = await request;
|
||||
|
||||
let status = response.info().status;
|
||||
|
||||
@@ -21,12 +21,7 @@ import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
|
||||
export const cacheDirOld = RNFetchBlob.fs.dirs.CacheDir;
|
||||
|
||||
export const cacheDir =
|
||||
Platform.OS == "ios"
|
||||
? RNFetchBlob.fs.dirs.LibraryDir + "/.cache"
|
||||
: RNFetchBlob.fs.dirs.DocumentDir + "/.cache";
|
||||
export const cacheDir = RNFetchBlob.fs.dirs.CacheDir;
|
||||
|
||||
export function getRandomId(prefix) {
|
||||
return Math.random()
|
||||
@@ -71,12 +66,9 @@ export function cancelable(operation) {
|
||||
}
|
||||
|
||||
export function copyFileAsync(source, dest) {
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise((resolve) => {
|
||||
ScopedStorage.copyFile(source, dest, (e, r) => {
|
||||
if (e) {
|
||||
reject(e);
|
||||
return;
|
||||
}
|
||||
console.log(e, r);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -128,9 +128,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
fontSize={SIZE.md}
|
||||
buttonType={{
|
||||
color: color ? color : colors.primary.accent,
|
||||
text: color
|
||||
? colors.static.white
|
||||
: colors.primary.accentForeground,
|
||||
text: colors.static.white,
|
||||
selected: color ? color : colors.primary.accent,
|
||||
opacity: 1
|
||||
}}
|
||||
|
||||
@@ -34,69 +34,10 @@ import BaseDialog from "../dialog/base-dialog";
|
||||
import { allowedOnPlatform, renderItem } from "./functions";
|
||||
import { useCallback } from "react";
|
||||
|
||||
/**
|
||||
* Test announcement
|
||||
* {
|
||||
id: "some-announcement",
|
||||
type: "dialog",
|
||||
body: [
|
||||
{
|
||||
type: "title",
|
||||
text: "This is a title",
|
||||
platforms: ["all"]
|
||||
},
|
||||
{
|
||||
type: "description",
|
||||
text: "Most of you are too busy to keep up to date with what's happening in Notesnook. That is unfortunate because Notesnook has come a looooong way.",
|
||||
style: {
|
||||
marginBottom: 1
|
||||
},
|
||||
platforms: ["all"]
|
||||
},
|
||||
{
|
||||
type: "description",
|
||||
text: "To solve this, we are launching the Notesnook Digest — a newsletter to help you stay updated about Notesnook development. And to keep things interesting I'll also sprinkle this newsletter with other interesting stuff like privacy tips & news, interesting books, things I am looking forward to etc.",
|
||||
style: {
|
||||
marginBottom: 1
|
||||
},
|
||||
platforms: ["all"]
|
||||
},
|
||||
{
|
||||
type: "description",
|
||||
text: "So be sure to subscribe. There won't be a proper schedule to this (yet) maybe once or twice a month. I promise no spam — only more awesomeness.",
|
||||
style: {
|
||||
marginBottom: 1
|
||||
},
|
||||
platforms: ["all"]
|
||||
},
|
||||
{
|
||||
type: "description",
|
||||
text: "— May privacy reign.",
|
||||
style: {
|
||||
marginBottom: 1
|
||||
},
|
||||
platforms: ["all"]
|
||||
},
|
||||
{
|
||||
type: "callToActions",
|
||||
actions: [
|
||||
{
|
||||
type: "promo",
|
||||
title: "15% Off",
|
||||
platforms: ["android"],
|
||||
data: "com.streetwriters.notesnook.sub.yr.15"
|
||||
}
|
||||
],
|
||||
platforms: ["all"]
|
||||
}
|
||||
]
|
||||
}
|
||||
*/
|
||||
|
||||
export const AnnouncementDialog = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [info, setInfo] = useState();
|
||||
const [info, setInfo] = useState(null);
|
||||
const remove = useMessageStore((state) => state.remove);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -110,9 +51,7 @@ export const AnnouncementDialog = () => {
|
||||
|
||||
const open = (data) => {
|
||||
setInfo(data);
|
||||
setImmediate(() => {
|
||||
setVisible(true);
|
||||
});
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
const close = useCallback(() => {
|
||||
|
||||
@@ -100,7 +100,7 @@ const AppLockedOverlay = () => {
|
||||
appLocked && (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.primary.background,
|
||||
backgroundColor: colors.bg,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
@@ -160,7 +160,7 @@ const AppLockedOverlay = () => {
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: 12,
|
||||
backgroundColor: colors.primary.background
|
||||
backgroundColor: colors.bg
|
||||
}}
|
||||
>
|
||||
{user ? (
|
||||
|
||||
@@ -48,7 +48,7 @@ import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { formatBytes } from "@notesnook/common";
|
||||
|
||||
const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
||||
const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const contextId = attachment.metadata.hash;
|
||||
const [filename, setFilename] = useState(attachment.metadata.filename);
|
||||
@@ -64,7 +64,7 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
||||
name: "Download",
|
||||
onPress: async () => {
|
||||
if (currentProgress) {
|
||||
await db.fs.cancel(attachment.metadata.hash);
|
||||
await db.fs.cancel(attachment.metadata.hash, "download");
|
||||
useAttachmentStore.getState().remove(attachment.metadata.hash);
|
||||
}
|
||||
downloadAttachment(attachment.metadata.hash, false);
|
||||
@@ -117,7 +117,7 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
||||
});
|
||||
}
|
||||
|
||||
setAttachments();
|
||||
setAttachments([...db.attachments.all]);
|
||||
setLoading({
|
||||
name: null
|
||||
});
|
||||
@@ -140,7 +140,7 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
||||
filename: value
|
||||
});
|
||||
setFilename(value);
|
||||
setAttachments();
|
||||
setAttachments([...db.attachments.all]);
|
||||
}
|
||||
},
|
||||
positiveText: "Rename"
|
||||
@@ -152,8 +152,8 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
||||
name: "Delete",
|
||||
onPress: async () => {
|
||||
await db.attachments.remove(attachment.metadata.hash, false);
|
||||
setAttachments();
|
||||
close();
|
||||
setAttachments([...db.attachments.all]);
|
||||
eSendEvent(eCloseSheet, contextId);
|
||||
},
|
||||
icon: "delete-outline"
|
||||
}
|
||||
@@ -362,13 +362,8 @@ const Actions = ({ attachment, setAttachments, fwdRef, close }) => {
|
||||
Actions.present = (attachment, set, context) => {
|
||||
presentSheet({
|
||||
context: context,
|
||||
component: (ref, close) => (
|
||||
<Actions
|
||||
fwdRef={ref}
|
||||
setAttachments={set}
|
||||
close={close}
|
||||
attachment={attachment}
|
||||
/>
|
||||
component: (ref) => (
|
||||
<Actions fwdRef={ref} setAttachments={set} attachment={attachment} />
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
isDocument,
|
||||
isImage,
|
||||
isVideo
|
||||
} from "@notesnook/core/dist/utils/filename";
|
||||
} from "@notesnook/core/utils/filename";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
|
||||
|
||||
@@ -49,7 +49,7 @@ export const AttachmentDialog = ({ note }) => {
|
||||
const { height } = useSettingStore((state) => state.dimensions);
|
||||
const [attachments, setAttachments] = useState(
|
||||
note
|
||||
? db.attachments.ofNote(note?.id, "all")
|
||||
? db.attachments.ofNote(note.id, "all")
|
||||
: [...(db.attachments.all || [])]
|
||||
);
|
||||
|
||||
@@ -59,8 +59,8 @@ export const AttachmentDialog = ({ note }) => {
|
||||
const [currentFilter, setCurrentFilter] = useState("all");
|
||||
|
||||
const onChangeText = (text) => {
|
||||
const attachments = note?.id
|
||||
? db.attachments.ofNote(note?.id, "all")
|
||||
const attachments = note
|
||||
? db.attachments.ofNote(note.id, "all")
|
||||
: [...(db.attachments.all || [])];
|
||||
|
||||
attachmentSearchValue.current = text;
|
||||
@@ -68,7 +68,7 @@ export const AttachmentDialog = ({ note }) => {
|
||||
!attachmentSearchValue.current ||
|
||||
attachmentSearchValue.current === ""
|
||||
) {
|
||||
setAttachments(filterAttachments(currentFilter));
|
||||
setAttachments([...attachments]);
|
||||
}
|
||||
clearTimeout(searchTimer.current);
|
||||
searchTimer.current = setTimeout(() => {
|
||||
@@ -77,16 +77,13 @@ export const AttachmentDialog = ({ note }) => {
|
||||
attachmentSearchValue.current
|
||||
);
|
||||
if (results.length === 0) return;
|
||||
|
||||
setAttachments(filterAttachments(currentFilter, results));
|
||||
setAttachments(results);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const renderItem = ({ item }) => (
|
||||
<AttachmentItem
|
||||
setAttachments={() => {
|
||||
setAttachments(filterAttachments(currentFilter));
|
||||
}}
|
||||
setAttachments={setAttachments}
|
||||
attachment={item}
|
||||
context="attachments-list"
|
||||
/>
|
||||
@@ -136,13 +133,11 @@ export const AttachmentDialog = ({ note }) => {
|
||||
}
|
||||
];
|
||||
|
||||
const filterAttachments = (type, _attachments) => {
|
||||
const attachments = _attachments
|
||||
? _attachments
|
||||
: note
|
||||
? db.attachments.ofNote(note?.id, "all")
|
||||
const filterAttachments = (type) => {
|
||||
const attachments = note
|
||||
? db.attachments.ofNote(note.id, "all")
|
||||
: [...(db.attachments.all || [])];
|
||||
|
||||
isDocument;
|
||||
switch (type) {
|
||||
case "all":
|
||||
return attachments;
|
||||
@@ -246,7 +241,7 @@ export const AttachmentDialog = ({ note }) => {
|
||||
width: "100%",
|
||||
height: 50,
|
||||
flexDirection: "row",
|
||||
backgroundColor: colors.primary.background
|
||||
backgroundColor: colors.bg
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
minWidth: "100%",
|
||||
|
||||
@@ -17,16 +17,14 @@ 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 { useUserStore } from "../../stores/use-user-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { eCloseLoginDialog, eOpenLoginDialog } from "../../utils/events";
|
||||
import { sleep } from "../../utils/time";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
@@ -36,6 +34,7 @@ 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,
|
||||
@@ -69,9 +68,6 @@ const AuthModal = () => {
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: false
|
||||
});
|
||||
actionSheetRef.current?.hide();
|
||||
setCurrentAuthMode(AuthMode.login);
|
||||
setVisible(false);
|
||||
@@ -81,11 +77,6 @@ const AuthModal = () => {
|
||||
<BaseDialog
|
||||
overlayOpacity={0}
|
||||
statusBarTranslucent={false}
|
||||
onShow={() => {
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: true
|
||||
});
|
||||
}}
|
||||
onRequestClose={currentAuthMode !== AuthMode.welcomeSignup && close}
|
||||
visible={true}
|
||||
onClose={close}
|
||||
@@ -94,14 +85,11 @@ const AuthModal = () => {
|
||||
background={colors.primary.background}
|
||||
transparent={false}
|
||||
animated={false}
|
||||
centered={false}
|
||||
enableSheetKeyboardHandler
|
||||
>
|
||||
<KeyboardAwareScrollView
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
enableAutomaticScroll={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
{currentAuthMode !== AuthMode.login ? (
|
||||
|
||||
@@ -26,15 +26,12 @@ import {
|
||||
ToastEvent
|
||||
} from "../../services/event-manager";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eCloseSheet, eOpenRecoveryKeyDialog } from "../../utils/events";
|
||||
import { eCloseSheet } 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();
|
||||
@@ -67,9 +64,6 @@ 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);
|
||||
ToastEvent.show({
|
||||
@@ -79,8 +73,6 @@ export const ChangePassword = () => {
|
||||
});
|
||||
setLoading(false);
|
||||
eSendEvent(eCloseSheet);
|
||||
await sleep(300);
|
||||
eSendEvent(eOpenRecoveryKeyDialog);
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
ToastEvent.show({
|
||||
@@ -100,7 +92,6 @@ export const ChangePassword = () => {
|
||||
padding: 12
|
||||
}}
|
||||
>
|
||||
<Dialog context="change-password-dialog" />
|
||||
<DialogHeader
|
||||
title="Change password"
|
||||
paragraph="Enter your old and new passwords"
|
||||
@@ -138,14 +129,7 @@ export const ChangePassword = () => {
|
||||
/>
|
||||
|
||||
<Notice
|
||||
text={`Changing password is an irreversible process. You will be logged out from all your devices. Please make sure you do not close the app while your password is changing and have good internet connection.`}
|
||||
type="alert"
|
||||
/>
|
||||
|
||||
<View style={{ height: 10 }} />
|
||||
|
||||
<Notice
|
||||
text={`Once your password is changed, please make sure to save the new account recovery key.`}
|
||||
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."
|
||||
type="alert"
|
||||
/>
|
||||
|
||||
|
||||
@@ -17,22 +17,18 @@ 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 { Modal, View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { MMKV } from "../../common/database/mmkv";
|
||||
import BiometricService from "../../services/biometrics";
|
||||
import {
|
||||
ToastEvent,
|
||||
eSendEvent,
|
||||
eSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import { setLoginMessage } from "../../services/message";
|
||||
import SettingsService from "../../services/settings";
|
||||
import Sync from "../../services/sync";
|
||||
import { clearAllStores } from "../../stores";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { eLoginSessionExpired, eUserLoggedIn } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
@@ -46,7 +42,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,20 +64,16 @@ export const SessionExpired = () => {
|
||||
eSendEvent(eUserLoggedIn, true);
|
||||
setVisible(false);
|
||||
setFocused(false);
|
||||
},
|
||||
true
|
||||
}
|
||||
);
|
||||
|
||||
const logout = async () => {
|
||||
try {
|
||||
await db.user.logout();
|
||||
await BiometricService.resetCredentials();
|
||||
setLoginMessage();
|
||||
SettingsService.resetSettings();
|
||||
useUserStore.getState().setUser(null);
|
||||
useUserStore.getState().setSyncing(false);
|
||||
MMKV.clearStore();
|
||||
clearAllStores();
|
||||
SettingsService.set({
|
||||
introCompleted: true
|
||||
});
|
||||
setVisible(false);
|
||||
} catch (e) {
|
||||
ToastEvent.show({
|
||||
@@ -132,20 +123,15 @@ export const SessionExpired = () => {
|
||||
|
||||
return (
|
||||
visible && (
|
||||
<BaseDialog
|
||||
transparent={false}
|
||||
background={colors.primary.background}
|
||||
bounce={false}
|
||||
animated={false}
|
||||
centered={false}
|
||||
<Modal
|
||||
onShow={async () => {
|
||||
await sleep(300);
|
||||
passwordInputRef.current?.focus();
|
||||
setFocused(true);
|
||||
}}
|
||||
enableSheetKeyboardHandler={true}
|
||||
visible={true}
|
||||
>
|
||||
<SheetProvider context="two_factor_verify" />
|
||||
<View
|
||||
style={{
|
||||
width: focused ? "100%" : "99.9%",
|
||||
@@ -238,9 +224,7 @@ export const SessionExpired = () => {
|
||||
</View>
|
||||
<Toast context="local" />
|
||||
<Dialog context="session_expiry" />
|
||||
|
||||
<SheetProvider context="two_factor_verify" />
|
||||
</BaseDialog>
|
||||
</Modal>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -67,7 +67,6 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
|
||||
const signup = async () => {
|
||||
if (!validateInfo() || error) return;
|
||||
if (loading) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
await db.user.signup(email.current.toLowerCase(), password.current);
|
||||
|
||||
@@ -214,7 +214,6 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
code.current = value;
|
||||
//onNext();
|
||||
}}
|
||||
caretHidden
|
||||
inputStyle={{
|
||||
fontSize: SIZE.lg,
|
||||
height: 60,
|
||||
|
||||
@@ -33,7 +33,7 @@ export const LoginSteps = {
|
||||
passwordAuth: 3
|
||||
};
|
||||
|
||||
export const useLogin = (onFinishLogin, sessionExpired = false) => {
|
||||
export const useLogin = (onFinishLogin) => {
|
||||
const [error, setError] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const setUser = useUserStore((state) => state.setUser);
|
||||
@@ -64,12 +64,10 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
|
||||
const login = async () => {
|
||||
if (!validateInfo() || error) return;
|
||||
try {
|
||||
if (loading) return;
|
||||
setLoading(true);
|
||||
switch (step) {
|
||||
case LoginSteps.emailAuth: {
|
||||
const mfaInfo = await db.user.authenticateEmail(email.current);
|
||||
console.log("email auth", mfaInfo);
|
||||
if (mfaInfo) {
|
||||
TwoFactorVerification.present(async (mfa, callback) => {
|
||||
try {
|
||||
@@ -102,12 +100,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
|
||||
break;
|
||||
}
|
||||
case LoginSteps.passwordAuth: {
|
||||
await db.user.authenticatePassword(
|
||||
email.current,
|
||||
password.current,
|
||||
null,
|
||||
sessionExpired
|
||||
);
|
||||
await db.user.authenticatePassword(email.current, password.current);
|
||||
finishLogin();
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -17,12 +17,12 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
|
||||
export const ContainerHeader = (props: PropsWithChildren) => {
|
||||
export const ContainerHeader = ({ children }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
|
||||
@@ -34,7 +34,7 @@ export const ContainerHeader = (props: PropsWithChildren) => {
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
{children}
|
||||
</View>
|
||||
) : null;
|
||||
};
|
||||
@@ -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 { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import { Keyboard, View } from "react-native";
|
||||
import { Keyboard, Platform, View } from "react-native";
|
||||
import Animated, {
|
||||
Easing,
|
||||
useAnimatedStyle,
|
||||
@@ -35,20 +34,14 @@ import { getElevationStyle } from "../../utils/elevation";
|
||||
import { SIZE, normalize } from "../../utils/size";
|
||||
import NativeTooltip from "../../utils/tooltip";
|
||||
import { PressableButton } from "../ui/pressable";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
|
||||
interface FloatingButton {
|
||||
title?: string;
|
||||
onPress: () => void;
|
||||
color?: string;
|
||||
alwaysVisible?: boolean;
|
||||
}
|
||||
|
||||
const FloatingButton = ({
|
||||
export const FloatingButton = ({
|
||||
title,
|
||||
onPress,
|
||||
color,
|
||||
alwaysVisible = false
|
||||
}: FloatingButton) => {
|
||||
shouldShow = false
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
@@ -67,8 +60,12 @@ const FloatingButton = ({
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
animate(selectionMode ? 150 : 0);
|
||||
}, [animate, selectionMode]);
|
||||
|
||||
const animate = useCallback(
|
||||
(toValue: number) => {
|
||||
(toValue) => {
|
||||
translate.value = withTiming(toValue, {
|
||||
duration: 250,
|
||||
easing: Easing.elastic(1)
|
||||
@@ -77,39 +74,39 @@ const FloatingButton = ({
|
||||
[translate]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
animate(selectionMode ? 150 : 0);
|
||||
}, [animate, selectionMode]);
|
||||
const onKeyboardHide = useCallback(async () => {
|
||||
editorState().keyboardState = false;
|
||||
if (deviceMode !== "mobile") return;
|
||||
animate(0);
|
||||
}, [animate, deviceMode]);
|
||||
|
||||
const onKeyboardShow = useCallback(async () => {
|
||||
editorState().keyboardState = true;
|
||||
if (deviceMode !== "mobile") return;
|
||||
animate(150);
|
||||
}, [animate, deviceMode]);
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyboardHide = () => {
|
||||
editorState().keyboardState = false;
|
||||
if (deviceMode !== "mobile") return;
|
||||
animate(0);
|
||||
};
|
||||
|
||||
const onKeyboardShow = () => {
|
||||
editorState().keyboardState = true;
|
||||
if (deviceMode !== "mobile") return;
|
||||
animate(150);
|
||||
};
|
||||
|
||||
const sub = [
|
||||
Keyboard.addListener("keyboardDidShow", onKeyboardShow),
|
||||
Keyboard.addListener("keyboardDidHide", onKeyboardHide)
|
||||
];
|
||||
let sub1 = Keyboard.addListener("keyboardDidShow", onKeyboardShow);
|
||||
let sub2 = Keyboard.addListener("keyboardDidHide", onKeyboardHide);
|
||||
return () => {
|
||||
sub.forEach((sub) => sub?.remove?.());
|
||||
sub1?.remove();
|
||||
sub2?.remove();
|
||||
};
|
||||
}, [deviceMode, animate]);
|
||||
}, [deviceMode, onKeyboardHide, onKeyboardShow]);
|
||||
const paddings = {
|
||||
ios: 20,
|
||||
android: 20,
|
||||
iPad: 20
|
||||
};
|
||||
|
||||
return deviceMode !== "mobile" && !alwaysVisible ? null : (
|
||||
return deviceMode !== "mobile" && !shouldShow ? null : (
|
||||
<Animated.View
|
||||
style={[
|
||||
{
|
||||
position: "absolute",
|
||||
right: 12,
|
||||
bottom: 20,
|
||||
bottom: paddings[Platform.isPad ? "iPad" : Platform.OS],
|
||||
zIndex: 10
|
||||
},
|
||||
animatedStyle
|
||||
@@ -118,15 +115,16 @@ const FloatingButton = ({
|
||||
<PressableButton
|
||||
testID={notesnook.buttons.add}
|
||||
type="accent"
|
||||
accentColor={colors.static[color as keyof typeof colors.static]}
|
||||
buttonType={{
|
||||
color: colors.static[color],
|
||||
text: colors.static.white
|
||||
}}
|
||||
customStyle={{
|
||||
...getElevationStyle(5),
|
||||
borderRadius: 100
|
||||
}}
|
||||
onLongPress={(event) => {
|
||||
if (title) {
|
||||
NativeTooltip.show(event, title, NativeTooltip.POSITIONS.LEFT);
|
||||
}
|
||||
NativeTooltip.show(event, title, NativeTooltip.POSITIONS.LEFT);
|
||||
}}
|
||||
onPress={onPress}
|
||||
>
|
||||
@@ -140,7 +138,7 @@ const FloatingButton = ({
|
||||
>
|
||||
<Icon
|
||||
name={title === "Clear all trash" ? "delete" : "plus"}
|
||||
color={colors.primary.accentForeground}
|
||||
color="white"
|
||||
size={SIZE.xxl}
|
||||
/>
|
||||
</View>
|
||||
@@ -148,5 +146,3 @@ const FloatingButton = ({
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
export { FloatingButton };
|
||||
@@ -17,40 +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 React, { PropsWithChildren } from "react";
|
||||
import { KeyboardAvoidingView, Platform } from "react-native";
|
||||
import React from "react";
|
||||
import { KeyboardAvoidingView, Platform, View } 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) => {
|
||||
export const Container = ({ children }) => {
|
||||
const floating = useIsFloatingKeyboard();
|
||||
const introCompleted = useSettingStore(
|
||||
(state) => state.settings.introCompleted
|
||||
);
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
behavior="padding"
|
||||
enabled={Platform.OS === "ios" && !floating}
|
||||
style={{
|
||||
flex: 1,
|
||||
overflow: "hidden",
|
||||
paddingTop: Platform.OS === "android" ? 0 : insets.top,
|
||||
paddingBottom: Platform.OS === "android" ? 0 : insets.bottom
|
||||
flex: 1
|
||||
}}
|
||||
>
|
||||
{!introCompleted ? null : (
|
||||
<>
|
||||
<SelectionHeader />
|
||||
<Header />
|
||||
</>
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
overflow: "hidden",
|
||||
paddingTop: Platform.OS === "android" ? 0 : insets.top,
|
||||
paddingBottom: Platform.OS === "android" ? 0 : insets.bottom
|
||||
}}
|
||||
>
|
||||
{!introCompleted ? null : (
|
||||
<>
|
||||
<SelectionHeader />
|
||||
<Header title="Header" screen="Header" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{children}
|
||||
{children}
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
};
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import {
|
||||
KeyboardAvoidingView,
|
||||
Modal,
|
||||
@@ -31,9 +31,6 @@ import useIsFloatingKeyboard from "../../hooks/use-is-floating-keyboard";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { BouncingView } from "../ui/transitions/bouncing-view";
|
||||
import { ScopedThemeProvider } from "@notesnook/theme";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { useAppState } from "../../hooks/use-app-state";
|
||||
|
||||
const BaseDialog = ({
|
||||
visible,
|
||||
@@ -51,13 +48,9 @@ const BaseDialog = ({
|
||||
bounce = true,
|
||||
closeOnTouch = true,
|
||||
useSafeArea = true,
|
||||
avoidKeyboardResize = false,
|
||||
enableSheetKeyboardHandler = false
|
||||
avoidKeyboardResize = false
|
||||
}) => {
|
||||
const floating = useIsFloatingKeyboard();
|
||||
const appState = useAppState();
|
||||
const lockEvents = useRef(false);
|
||||
const [internalVisible, setIntervalVisible] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -65,34 +58,14 @@ const BaseDialog = ({
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (useUserStore.getState().disableAppLockRequests) return;
|
||||
if (SettingsService.get().appLockMode === "background") {
|
||||
if (appState === "background") {
|
||||
setIntervalVisible(false);
|
||||
if (useUserStore.getState().appLocked) {
|
||||
lockEvents.current = true;
|
||||
const unsub = useUserStore.subscribe((state) => {
|
||||
if (!state.appLocked) {
|
||||
setIntervalVisible(true);
|
||||
unsub();
|
||||
setTimeout(() => {
|
||||
lockEvents.current = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [appState]);
|
||||
|
||||
const Wrapper = useSafeArea ? SafeAreaView : View;
|
||||
|
||||
return (
|
||||
<ScopedThemeProvider value="dialog">
|
||||
<Modal
|
||||
visible={visible && internalVisible}
|
||||
visible={visible}
|
||||
transparent={true}
|
||||
animated
|
||||
statusBarTranslucent={statusBarTranslucent}
|
||||
supportedOrientations={[
|
||||
"portrait",
|
||||
@@ -102,17 +75,13 @@ const BaseDialog = ({
|
||||
"landscape-right"
|
||||
]}
|
||||
onShow={() => {
|
||||
if (lockEvents.current) return;
|
||||
if (onShow) {
|
||||
onShow();
|
||||
if (!enableSheetKeyboardHandler) {
|
||||
useSettingStore.getState().setSheetKeyboardHandler(false);
|
||||
}
|
||||
useSettingStore.getState().setSheetKeyboardHandler(false);
|
||||
}
|
||||
}}
|
||||
animationType={animation}
|
||||
onRequestClose={() => {
|
||||
if (lockEvents.current) return;
|
||||
if (!closeOnTouch) return null;
|
||||
useSettingStore.getState().setSheetKeyboardHandler(true);
|
||||
onRequestClose && onRequestClose();
|
||||
@@ -138,7 +107,6 @@ const BaseDialog = ({
|
||||
style={[
|
||||
styles.backdrop,
|
||||
{
|
||||
alignItems: centered ? "center" : undefined,
|
||||
justifyContent: centered
|
||||
? "center"
|
||||
: bottom
|
||||
@@ -165,7 +133,8 @@ const styles = StyleSheet.create({
|
||||
backdrop: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
justifyContent: "center"
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
},
|
||||
overlayButton: {
|
||||
width: "100%",
|
||||
|
||||
@@ -40,10 +40,7 @@ import { Button } from "../ui/button";
|
||||
export const Dialog = ({ context = "global" }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [checked, setChecked] = useState(false);
|
||||
const values = useRef({
|
||||
inputValue: undefined
|
||||
});
|
||||
const [inputValue, setInputValue] = useState(null);
|
||||
const inputRef = useRef();
|
||||
const [dialogInfo, setDialogInfo] = useState({
|
||||
title: "",
|
||||
@@ -79,8 +76,7 @@ export const Dialog = ({ context = "global" }) => {
|
||||
if (dialogInfo.positivePress) {
|
||||
inputRef.current?.blur();
|
||||
let result = await dialogInfo.positivePress(
|
||||
values.current.inputValue || dialogInfo.defaultValue,
|
||||
checked
|
||||
inputValue || dialogInfo.defaultValue
|
||||
);
|
||||
if (result === false) {
|
||||
return;
|
||||
@@ -95,16 +91,14 @@ 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 = () => {
|
||||
setChecked(false);
|
||||
values.current.inputValue = undefined;
|
||||
setInputValue(null);
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
@@ -112,6 +106,7 @@ export const Dialog = ({ context = "global" }) => {
|
||||
if (dialogInfo.onClose) {
|
||||
await dialogInfo.onClose();
|
||||
}
|
||||
|
||||
hide();
|
||||
};
|
||||
|
||||
@@ -164,7 +159,7 @@ export const Dialog = ({ context = "global" }) => {
|
||||
fwdRef={inputRef}
|
||||
autoCapitalize="none"
|
||||
onChangeText={(value) => {
|
||||
values.current.inputValue = value;
|
||||
setInputValue(value);
|
||||
}}
|
||||
testID="input-value"
|
||||
secureTextEntry={dialogInfo.secureTextEntry}
|
||||
@@ -181,10 +176,10 @@ export const Dialog = ({ context = "global" }) => {
|
||||
<>
|
||||
<Button
|
||||
onPress={() => {
|
||||
setChecked(!checked);
|
||||
setInputValue(!inputValue);
|
||||
}}
|
||||
icon={
|
||||
checked
|
||||
inputValue
|
||||
? "check-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
@@ -192,10 +187,9 @@ export const Dialog = ({ context = "global" }) => {
|
||||
justifyContent: "flex-start"
|
||||
}}
|
||||
height={35}
|
||||
iconSize={20}
|
||||
width="100%"
|
||||
title={dialogInfo.check.info}
|
||||
type={checked ? dialogInfo.check.type : "gray"}
|
||||
type={inputValue ? dialogInfo.check.type : "gray"}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
@@ -48,7 +48,7 @@ export const ProFeatures = ({ count = 6 }) => {
|
||||
content: "Instantly sync to unlimited devices"
|
||||
},
|
||||
{
|
||||
content: "A private vault to keep everything important always locked"
|
||||
content: "A private vault to keep everything imporant always locked"
|
||||
},
|
||||
{
|
||||
content:
|
||||
|
||||
@@ -442,7 +442,6 @@ export class VaultDialog extends Component {
|
||||
if (this.state.biometricUnlock && !this.state.isBiometryEnrolled) {
|
||||
await this._enrollFingerprint(this.password);
|
||||
}
|
||||
|
||||
if (this.state.goToEditor) {
|
||||
this._openInEditor(note);
|
||||
} else if (this.state.share) {
|
||||
@@ -453,6 +452,7 @@ export class VaultDialog extends Component {
|
||||
await this._copyNote(note);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
this._takeErrorAction(e);
|
||||
}
|
||||
};
|
||||
@@ -467,40 +467,44 @@ export class VaultDialog extends Component {
|
||||
}
|
||||
|
||||
async _enrollFingerprint(password) {
|
||||
this.setState(
|
||||
{
|
||||
loading: true
|
||||
},
|
||||
async () => {
|
||||
try {
|
||||
await db.vault.unlock(password);
|
||||
await BiometricService.storeCredentials(password);
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
eSendEvent("vaultUpdated");
|
||||
ToastEvent.show({
|
||||
heading: "Biometric unlocking enabled!",
|
||||
message: "Now you can unlock notes in vault with biometrics.",
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
this.close();
|
||||
} catch (e) {
|
||||
this.close();
|
||||
ToastEvent.show({
|
||||
heading: "Incorrect password",
|
||||
message:
|
||||
"Please enter the correct vault password to enable biometrics.",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
try {
|
||||
this.setState(
|
||||
{
|
||||
loading: true
|
||||
},
|
||||
async () => {
|
||||
try {
|
||||
await db.vault.unlock(password);
|
||||
await BiometricService.storeCredentials(password);
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
eSendEvent("vaultUpdated");
|
||||
ToastEvent.show({
|
||||
heading: "Biometric unlocking enabled!",
|
||||
message: "Now you can unlock notes in vault with biometrics.",
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
this.close();
|
||||
} catch (e) {
|
||||
ToastEvent.show({
|
||||
heading: "Incorrect password",
|
||||
message:
|
||||
"Please enter the correct vault password to enable biometrics.",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
);
|
||||
} catch (e) {
|
||||
this._takeErrorAction(e);
|
||||
}
|
||||
}
|
||||
|
||||
async _createVault() {
|
||||
@@ -539,7 +543,7 @@ export class VaultDialog extends Component {
|
||||
.remove(this.state.note.id, this.password)
|
||||
.then(() => {
|
||||
ToastEvent.show({
|
||||
heading: "Note permanently unlocked.",
|
||||
heading: "Note permanantly unlocked.",
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
|
||||
@@ -39,9 +39,9 @@ class ExceptionHandler extends React.Component<{
|
||||
error: Error | null;
|
||||
hasError: boolean;
|
||||
} = {
|
||||
hasError: false,
|
||||
error: null
|
||||
};
|
||||
hasError: false,
|
||||
error: null
|
||||
};
|
||||
static getDerivedStateFromError(error: Error) {
|
||||
return { hasError: true, error: error };
|
||||
}
|
||||
@@ -69,7 +69,7 @@ class ExceptionHandler extends React.Component<{
|
||||
this.props.component
|
||||
)}
|
||||
defaultTitle={this.state.error?.message}
|
||||
issueTitle="An exception occurred"
|
||||
issueTitle="An exception occured"
|
||||
/>
|
||||
<Dialog />
|
||||
</SafeAreaView>
|
||||
|
||||
@@ -43,7 +43,7 @@ const _Header = () => {
|
||||
);
|
||||
|
||||
const onScroll = useCallback(
|
||||
(data: { x: number; y: number }) => {
|
||||
(data) => {
|
||||
if (data.y > 150) {
|
||||
if (!hide) return;
|
||||
setHide(false);
|
||||
@@ -43,6 +43,12 @@ export const LeftMenus = () => {
|
||||
return;
|
||||
}
|
||||
Navigation.goBack();
|
||||
if (
|
||||
useNavigationStore.getState().currentScreen.name === "Signup" ||
|
||||
useNavigationStore.getState().currentScreen.name === "Login"
|
||||
) {
|
||||
tabBarRef.current.unlock();
|
||||
}
|
||||
};
|
||||
|
||||
return isTablet && !canGoBack ? null : (
|
||||
@@ -19,7 +19,6 @@ 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 Menu from "react-native-reanimated-material-menu";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import Navigation from "../../services/navigation";
|
||||
@@ -34,15 +33,12 @@ import { IconButton } from "../ui/icon-button";
|
||||
|
||||
export const RightMenus = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const { colors: contextMenuColors } = useThemeColors("contextMenu");
|
||||
const { contextMenuColors } = useThemeColors("contextMenu");
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const buttons = useNavigationStore((state) => state.headerRightButtons);
|
||||
const rightButtons = useNavigationStore((state) => state.headerRightButtons);
|
||||
const currentScreen = useNavigationStore((state) => state.currentScreen.name);
|
||||
const buttonAction = useNavigationStore((state) => state.buttonAction);
|
||||
const menuRef = useRef<{
|
||||
show: () => void;
|
||||
hide: () => void;
|
||||
}>();
|
||||
const menuRef = useRef();
|
||||
|
||||
return (
|
||||
<View style={styles.rightBtnContainer}>
|
||||
@@ -50,12 +46,9 @@ export const RightMenus = () => {
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
SearchService.prepareSearch();
|
||||
Navigation.navigate(
|
||||
{
|
||||
name: "Search"
|
||||
},
|
||||
{}
|
||||
);
|
||||
Navigation.navigate({
|
||||
name: "Search"
|
||||
});
|
||||
}}
|
||||
testID="icon-search"
|
||||
name="magnify"
|
||||
@@ -89,7 +82,7 @@ export const RightMenus = () => {
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{buttons && buttons.length > 0 ? (
|
||||
{rightButtons && rightButtons.length > 0 ? (
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
animationDuration={200}
|
||||
@@ -100,7 +93,7 @@ export const RightMenus = () => {
|
||||
onRequestClose={() => {
|
||||
menuRef.current?.hide();
|
||||
}}
|
||||
anchor={
|
||||
button={
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
menuRef.current?.show();
|
||||
@@ -111,7 +104,7 @@ export const RightMenus = () => {
|
||||
/>
|
||||
}
|
||||
>
|
||||
{buttons.map((item) => (
|
||||
{rightButtons.map((item) => (
|
||||
<Button
|
||||
style={{
|
||||
width: 150,
|
||||
@@ -120,7 +113,7 @@ export const RightMenus = () => {
|
||||
}}
|
||||
type="gray"
|
||||
buttonType={{
|
||||
text: contextMenuColors.primary.paragraph
|
||||
text: colors.primary.paragraph
|
||||
}}
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
@@ -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 { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import Notebook from "../../screens/notebook";
|
||||
import {
|
||||
@@ -27,12 +26,14 @@ import {
|
||||
eUnSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import useNavigationStore from "../../stores/use-navigation-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { eScrollEvent } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import Tag from "../ui/tag";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import { useCallback } from "react";
|
||||
import Tag from "../ui/tag";
|
||||
|
||||
const titleState: { [name: string]: boolean } = {};
|
||||
const titleState = {};
|
||||
|
||||
export const Title = () => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -41,12 +42,12 @@ export const Title = () => {
|
||||
const isTopic = currentScreen?.name === "TopicNotes";
|
||||
const [hide, setHide] = useState(
|
||||
isNotebook
|
||||
? typeof titleState[currentScreen.id as string] === "boolean"
|
||||
? titleState[currentScreen.id as string]
|
||||
? typeof titleState[currentScreen.id] === "boolean"
|
||||
? titleState[currentScreen.id]
|
||||
: true
|
||||
: false
|
||||
);
|
||||
const isHidden = titleState[currentScreen.id as string];
|
||||
const isHidden = titleState[currentScreen.id];
|
||||
const notebook =
|
||||
isTopic && currentScreen.notebookId
|
||||
? db.notebooks?.notebook(currentScreen.notebookId)?.data
|
||||
@@ -55,18 +56,18 @@ export const Title = () => {
|
||||
const isTag = currentScreen?.name === "TaggedNotes";
|
||||
|
||||
const onScroll = useCallback(
|
||||
(data: { x: number; y: number }) => {
|
||||
(data) => {
|
||||
if (currentScreen.name !== "Notebook") {
|
||||
setHide(false);
|
||||
return;
|
||||
}
|
||||
if (data.y > 150) {
|
||||
if (!hide) return;
|
||||
titleState[currentScreen.id as string] = false;
|
||||
titleState[currentScreen.id] = false;
|
||||
setHide(false);
|
||||
} else {
|
||||
if (hide) return;
|
||||
titleState[currentScreen.id as string] = true;
|
||||
titleState[currentScreen.id] = true;
|
||||
setHide(true);
|
||||
}
|
||||
},
|
||||
@@ -75,13 +76,13 @@ export const Title = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (currentScreen.name === "Notebook") {
|
||||
const value =
|
||||
typeof titleState[currentScreen.id as string] === "boolean"
|
||||
? titleState[currentScreen.id as string]
|
||||
let value =
|
||||
typeof titleState[currentScreen.id] === "boolean"
|
||||
? titleState[currentScreen.id]
|
||||
: true;
|
||||
setHide(value);
|
||||
} else {
|
||||
setHide(titleState[currentScreen.id as string]);
|
||||
setHide(titleState[currentScreen.id]);
|
||||
}
|
||||
}, [currentScreen.id, currentScreen.name]);
|
||||
|
||||
@@ -97,7 +98,14 @@ export const Title = () => {
|
||||
Notebook.navigate(notebook, true);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
opacity: 1,
|
||||
flexShrink: 1,
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{!hide && !isHidden ? (
|
||||
<Heading
|
||||
onPress={navigateToNotebook}
|
||||
@@ -114,17 +122,10 @@ export const Title = () => {
|
||||
#
|
||||
</Heading>
|
||||
) : null}
|
||||
{title}{" "}
|
||||
<Tag
|
||||
visible={currentScreen.beta}
|
||||
text="BETA"
|
||||
style={{
|
||||
backgroundColor: "transparent"
|
||||
}}
|
||||
textColor={colors.primary.accent}
|
||||
/>
|
||||
{title}
|
||||
</Heading>
|
||||
) : null}
|
||||
</>
|
||||
<Tag visible={currentScreen.beta} text="BETA" />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import dataurl from "@notesnook/core/dist/utils/dataurl";
|
||||
import dataurl from "@notesnook/core/utils/dataurl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
|
||||
@@ -17,25 +17,35 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { TouchableOpacity, useWindowDimensions, View } from "react-native";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useRef } from "react";
|
||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
import { eSendEvent, presentSheet } from "../../../services/event-manager";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import { ColorValues } from "../../../utils/colors";
|
||||
import {
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent,
|
||||
presentSheet
|
||||
} from "../../../services/event-manager";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { GROUP } from "../../../utils/constants";
|
||||
import { ColorValues } from "../../../utils/colors";
|
||||
import { db } from "../../../common/database";
|
||||
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 { Button } from "../../ui/button";
|
||||
import Sort from "../../sheets/sort";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import { useCallback } from "react";
|
||||
|
||||
export const SectionHeader = React.memo(
|
||||
function SectionHeader({ item, index, type, color, screen, groupOptions }) {
|
||||
function SectionHeader({ item, index, type, color, screen }) {
|
||||
const { colors } = useThemeColors();
|
||||
const { fontScale } = useWindowDimensions();
|
||||
const [groupOptions, setGroupOptions] = useState(
|
||||
db.settings?.getGroupOptions(type)
|
||||
);
|
||||
let groupBy = Object.keys(GROUP).find(
|
||||
(key) => GROUP[key] === groupOptions.groupBy
|
||||
);
|
||||
@@ -55,6 +65,17 @@ export const SectionHeader = React.memo(
|
||||
? "Default"
|
||||
: groupBy.slice(0, 1).toUpperCase() + groupBy.slice(1, groupBy.length);
|
||||
|
||||
const onUpdate = useCallback(() => {
|
||||
setGroupOptions({ ...db.settings?.getGroupOptions(type) });
|
||||
}, [type]);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent("groupOptionsUpdate", onUpdate);
|
||||
return () => {
|
||||
eUnSubscribeEvent("groupOptionsUpdate", onUpdate);
|
||||
};
|
||||
}, [onUpdate]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
@@ -101,7 +122,7 @@ export const SectionHeader = React.memo(
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{index === 0 ? (
|
||||
{index === 0 && (
|
||||
<>
|
||||
<Button
|
||||
onPress={() => {
|
||||
@@ -109,6 +130,7 @@ export const SectionHeader = React.memo(
|
||||
component: <Sort screen={screen} type={type} />
|
||||
});
|
||||
}}
|
||||
hidden={screen === "Reminders"}
|
||||
tooltipText="Change sorting of items in list"
|
||||
fwdRef={sortRef}
|
||||
title={groupBy}
|
||||
@@ -159,7 +181,7 @@ export const SectionHeader = React.memo(
|
||||
size={SIZE.lg - 2}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { getUpcomingReminder } from "@notesnook/core/dist/collections/reminders";
|
||||
import { getUpcomingReminder } from "@notesnook/core/collections/reminders";
|
||||
import { decode, EntityLevel } from "entities";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
@@ -40,7 +40,6 @@ 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";
|
||||
|
||||
function navigateToTag(item) {
|
||||
const tag = db.tags.tag(item.id);
|
||||
@@ -99,9 +98,6 @@ const NoteItem = ({
|
||||
dateBy = "dateCreated",
|
||||
noOpen = false
|
||||
}) => {
|
||||
const isEditingNote = useEditorStore(
|
||||
(state) => state.currentEditingNote === item.id
|
||||
);
|
||||
const { colors } = useThemeColors();
|
||||
const compactMode = useIsCompactModeEnabled(item);
|
||||
const attachmentCount = db.attachments?.ofNote(item.id, "all")?.length || 0;
|
||||
@@ -112,8 +108,6 @@ const NoteItem = ({
|
||||
const reminder = getUpcomingReminder(reminders);
|
||||
const noteColor = ColorValues[item.color?.toLowerCase()];
|
||||
const tags = getTags(item);
|
||||
const primaryColors = isEditingNote ? colors.selected : colors.primary;
|
||||
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
@@ -154,7 +148,7 @@ const NoteItem = ({
|
||||
borderRadius: 5,
|
||||
marginRight: 5,
|
||||
borderWidth: 0.5,
|
||||
borderColor: primaryColors.border,
|
||||
borderColor: colors.primary.border,
|
||||
paddingHorizontal: 6,
|
||||
marginBottom: 5
|
||||
}}
|
||||
@@ -185,7 +179,7 @@ const NoteItem = ({
|
||||
<Paragraph
|
||||
numberOfLines={1}
|
||||
color={
|
||||
ColorValues[item.color?.toLowerCase()] || primaryColors.heading
|
||||
ColorValues[item.color?.toLowerCase()] || colors.primary.heading
|
||||
}
|
||||
style={{
|
||||
flexWrap: "wrap"
|
||||
@@ -198,7 +192,7 @@ const NoteItem = ({
|
||||
<Heading
|
||||
numberOfLines={1}
|
||||
color={
|
||||
ColorValues[item.color?.toLowerCase()] || primaryColors.heading
|
||||
ColorValues[item.color?.toLowerCase()] || colors.primary.heading
|
||||
}
|
||||
style={{
|
||||
flexWrap: "wrap"
|
||||
@@ -214,7 +208,6 @@ const NoteItem = ({
|
||||
style={{
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
color={primaryColors.paragraph}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{decode(item.headline, {
|
||||
@@ -269,7 +262,7 @@ const NoteItem = ({
|
||||
<Icon
|
||||
name="attachment"
|
||||
size={SIZE.md}
|
||||
color={primaryColors.icon}
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
@@ -290,7 +283,7 @@ const NoteItem = ({
|
||||
}}
|
||||
color={
|
||||
ColorValues[item.color?.toLowerCase()] ||
|
||||
primaryColors.accent
|
||||
colors.primary.accent
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
@@ -303,7 +296,7 @@ const NoteItem = ({
|
||||
style={{
|
||||
marginRight: 6
|
||||
}}
|
||||
color={primaryColors.icon}
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -361,7 +354,7 @@ const NoteItem = ({
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph
|
||||
color={primaryColors.accent}
|
||||
color={colors.primary.accent}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
marginRight: 6
|
||||
@@ -431,7 +424,7 @@ const NoteItem = ({
|
||||
|
||||
<IconButton
|
||||
testID={notesnook.listitem.menu}
|
||||
color={primaryColors.paragraph}
|
||||
color={colors.primary.paragraph}
|
||||
name="dots-horizontal"
|
||||
size={SIZE.xl}
|
||||
onPress={() => !noOpen && showActionSheet(item, isTrash)}
|
||||
|
||||
@@ -77,7 +77,7 @@ export const openNotebookTopic = (item) => {
|
||||
useTrashStore.getState().setTrash();
|
||||
useSelectionStore.getState().setSelectionMode(false);
|
||||
ToastEvent.show({
|
||||
heading: "Permanently deleted items",
|
||||
heading: "Permanantly deleted items",
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
|
||||
@@ -25,13 +25,13 @@ import { useEditorStore } from "../../../stores/use-editor-store";
|
||||
|
||||
export const Filler = ({ item }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const isEditingNote = useEditorStore(
|
||||
(state) => state.currentEditingNote === item.id
|
||||
const currentEditingNote = useEditorStore(
|
||||
(state) => state.currentEditingNote
|
||||
);
|
||||
|
||||
const [selected] = useIsSelected(item);
|
||||
|
||||
return isEditingNote || selected ? (
|
||||
return currentEditingNote === item.id || selected ? (
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
@@ -39,11 +39,12 @@ export const Filler = ({ item }) => {
|
||||
height: "150%",
|
||||
backgroundColor: colors.selected.background,
|
||||
borderLeftWidth: 5,
|
||||
borderLeftColor: isEditingNote
|
||||
? item.color
|
||||
? colors.static[item.color]
|
||||
: colors.selected.accent
|
||||
: "transparent"
|
||||
borderLeftColor:
|
||||
currentEditingNote === item.id
|
||||
? item.color
|
||||
? colors.static[item.color]
|
||||
: colors.selected.accent
|
||||
: "transparent"
|
||||
}}
|
||||
collapsable={false}
|
||||
/>
|
||||
|
||||
@@ -43,9 +43,6 @@ const SelectionWrapper = ({
|
||||
|
||||
const onLongPress = () => {
|
||||
if (!useSelectionStore.getState().selectionMode) {
|
||||
useSelectionStore.setState({
|
||||
selectedItemsList: []
|
||||
});
|
||||
useSelectionStore.getState().setSelectionMode(true);
|
||||
}
|
||||
useSelectionStore.getState().setSelectedItem(item);
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { Dimensions, View } from "react-native";
|
||||
import { View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { useMessageStore } from "../../stores/use-message-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
@@ -32,7 +32,6 @@ export const Card = ({ color, warning }) => {
|
||||
color = color ? color : colors.primary.accent;
|
||||
const messageBoardState = useMessageStore((state) => state.message);
|
||||
const announcement = useMessageStore((state) => state.announcement);
|
||||
const fontScale = Dimensions.get("window").fontScale;
|
||||
|
||||
return !messageBoardState.visible || announcement || warning ? null : (
|
||||
<View
|
||||
@@ -45,6 +44,7 @@ export const Card = ({ color, warning }) => {
|
||||
type="gray"
|
||||
customStyle={{
|
||||
paddingVertical: 12,
|
||||
width: "95%",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
@@ -54,18 +54,17 @@ export const Card = ({ color, warning }) => {
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
flexShrink: 1
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 40 * fontScale,
|
||||
width: 40,
|
||||
backgroundColor:
|
||||
messageBoardState.type === "error"
|
||||
? hexToRGBA(colors.static.red, 0.15)
|
||||
: hexToRGBA(color, 0.15),
|
||||
height: 40 * fontScale,
|
||||
height: 40,
|
||||
borderRadius: 100,
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
@@ -76,7 +75,6 @@ export const Card = ({ color, warning }) => {
|
||||
color={
|
||||
messageBoardState.type === "error" ? colors.error.icon : color
|
||||
}
|
||||
allowFontScaling
|
||||
name={messageBoardState.icon}
|
||||
/>
|
||||
</View>
|
||||
@@ -93,7 +91,7 @@ export const Card = ({ color, warning }) => {
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
style={{
|
||||
flexWrap: "nowrap",
|
||||
flexWrap: "wrap",
|
||||
flexShrink: 1
|
||||
}}
|
||||
color={colors.primary.heading}
|
||||
@@ -103,24 +101,22 @@ export const Card = ({ color, warning }) => {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{fontScale > 1 ? null : (
|
||||
<View
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="chevron-right"
|
||||
color={
|
||||
messageBoardState.type === "error" ? colors.error.icon : color
|
||||
}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="chevron-right"
|
||||
color={
|
||||
messageBoardState.type === "error" ? colors.error.icon : color
|
||||
}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
</View>
|
||||
</PressableButton>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -39,7 +39,6 @@ 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";
|
||||
|
||||
const renderItems = {
|
||||
note: NoteWrapper,
|
||||
@@ -54,10 +53,15 @@ const renderItems = {
|
||||
const RenderItem = ({ item, index, type, ...restArgs }) => {
|
||||
if (!item) return <View />;
|
||||
const Item = renderItems[item.itemType || item.type] || View;
|
||||
const groupOptions = db.settings?.getGroupOptions(type);
|
||||
const dateBy =
|
||||
groupOptions.sortBy !== "title" ? groupOptions.sortBy : "dateEdited";
|
||||
const totalNotes = getTotalNotes(item);
|
||||
|
||||
return (
|
||||
<Item
|
||||
item={item}
|
||||
dateBy={dateBy}
|
||||
index={index}
|
||||
type={type}
|
||||
totalNotes={totalNotes}
|
||||
@@ -99,13 +103,6 @@ const List = ({
|
||||
(type === "notes" && notesListMode === "compact") ||
|
||||
type === "notebooks" ||
|
||||
notebooksListMode === "compact";
|
||||
const groupType =
|
||||
screen === "Home" ? "home" : screen === "Favorites" ? "favorites" : type;
|
||||
|
||||
const groupOptions = useGroupOptions(groupType);
|
||||
|
||||
const dateBy =
|
||||
groupOptions.sortBy !== "title" ? groupOptions.sortBy : "dateEdited";
|
||||
|
||||
const renderItem = React.useCallback(
|
||||
({ item, index }) => (
|
||||
@@ -114,22 +111,18 @@ const List = ({
|
||||
index={index}
|
||||
color={headerProps?.color}
|
||||
title={headerProps?.heading}
|
||||
dateBy={dateBy}
|
||||
type={groupType}
|
||||
type={
|
||||
screen === "Notes"
|
||||
? "home"
|
||||
: screen === "Favorites"
|
||||
? "favorites"
|
||||
: type
|
||||
}
|
||||
screen={screen}
|
||||
isSheet={isSheet}
|
||||
groupOptions={groupOptions}
|
||||
/>
|
||||
),
|
||||
[
|
||||
headerProps?.color,
|
||||
headerProps?.heading,
|
||||
screen,
|
||||
isSheet,
|
||||
dateBy,
|
||||
groupType,
|
||||
groupOptions
|
||||
]
|
||||
[headerProps?.color, headerProps?.heading, screen, type, isSheet]
|
||||
);
|
||||
|
||||
const _onRefresh = async () => {
|
||||
@@ -235,7 +228,7 @@ const List = ({
|
||||
<JumpToSectionDialog
|
||||
screen={screen}
|
||||
data={listData}
|
||||
type={screen === "Home" ? "home" : type}
|
||||
type={screen === "Notes" ? "home" : type}
|
||||
scrollRef={scrollRef}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -245,17 +245,13 @@ const MergeConflicts = () => {
|
||||
};
|
||||
|
||||
return !visible ? null : (
|
||||
<BaseDialog
|
||||
<Modal
|
||||
statusBarTranslucent
|
||||
transparent={false}
|
||||
animationType="slide"
|
||||
animated={false}
|
||||
bounce={false}
|
||||
onRequestClose={() => {
|
||||
close();
|
||||
}}
|
||||
centered={false}
|
||||
background={colors?.primary.background}
|
||||
supportedOrientations={[
|
||||
"portrait",
|
||||
"portrait-upside-down",
|
||||
@@ -367,7 +363,7 @@ const MergeConflicts = () => {
|
||||
</Animated.View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</BaseDialog>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ export default function NotePreview({ session, content, note }) {
|
||||
useTrashStore.getState().setTrash();
|
||||
useSelectionStore.getState().setSelectionMode(false);
|
||||
ToastEvent.show({
|
||||
heading: "Permanently deleted items",
|
||||
heading: "Permanantly deleted items",
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
@@ -109,6 +109,7 @@ export default function NotePreview({ session, content, note }) {
|
||||
flex: 1
|
||||
}}
|
||||
>
|
||||
<EditorOverlay editorId={editorId} />
|
||||
<Editor
|
||||
noHeader
|
||||
noToolbar
|
||||
@@ -116,6 +117,7 @@ export default function NotePreview({ session, content, note }) {
|
||||
editorId={editorId}
|
||||
onLoad={async () => {
|
||||
const _note = note || db.notes.note(session?.noteId)?.data;
|
||||
await sleep(1000);
|
||||
eSendEvent(eOnLoadNote + editorId, {
|
||||
..._note,
|
||||
content: {
|
||||
|
||||
@@ -56,7 +56,7 @@ export const CompactFeatures = ({
|
||||
icon: "shield"
|
||||
},
|
||||
{
|
||||
highlight: "Daily, weekly & monthly",
|
||||
highlight: "Daily, weekly & montly",
|
||||
content: "recurring reminders",
|
||||
icon: "bell"
|
||||
},
|
||||
|
||||
@@ -165,12 +165,7 @@ export const Component = ({ close, promo }) => {
|
||||
}}
|
||||
size={SIZE.md}
|
||||
>
|
||||
(
|
||||
{Platform.OS === "android"
|
||||
? pricing.product?.subscriptionOfferDetails[0].pricingPhases
|
||||
.pricingPhaseList?.[0].formattedPrice
|
||||
: pricing.product?.localizedPrice}{" "}
|
||||
/ mo)
|
||||
({pricing?.product?.localizedPrice} / mo)
|
||||
</Paragraph>
|
||||
)}
|
||||
|
||||
|
||||
@@ -48,23 +48,23 @@ export const Expiring = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [status, setStatus] = useState({
|
||||
title: "Your trial is ending soon",
|
||||
offer: "Get 30% off",
|
||||
offer: null,
|
||||
extend: true
|
||||
});
|
||||
const pricing = usePricing("yearly");
|
||||
const promo =
|
||||
status.offer && pricing?.info
|
||||
? {
|
||||
promoCode:
|
||||
pricing?.info?.discount > 30
|
||||
? pricing.info.sku
|
||||
: "com.streetwriters.notesnook.sub.yr.trialoffer",
|
||||
text: `GET ${
|
||||
pricing?.info?.discount > 30 ? pricing?.info?.discount : 30
|
||||
}% OFF on yearly`,
|
||||
discount: pricing?.info?.discount > 30 ? pricing?.info?.discount : 30
|
||||
}
|
||||
: null;
|
||||
|
||||
const promo = status.offer
|
||||
? {
|
||||
promoCode:
|
||||
pricing?.info?.discount > 30
|
||||
? pricing.info.sku
|
||||
: "com.streetwriters.notesnook.sub.yr.trialoffer",
|
||||
text: `GET ${
|
||||
pricing?.info?.discount > 30 ? pricing?.info?.discount : 30
|
||||
}% OFF on yearly`,
|
||||
discount: pricing?.info?.discount > 30 ? pricing?.info?.discount : 30
|
||||
}
|
||||
: null;
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eOpenTrialEndingDialog, open);
|
||||
|
||||
@@ -98,7 +98,7 @@ export const features = [
|
||||
icon: "bell"
|
||||
},
|
||||
{
|
||||
highlight: "Daily, weekly & monthly",
|
||||
highlight: "Daily, weekly & montly",
|
||||
content: "reminders",
|
||||
icon: "refresh",
|
||||
pro: true
|
||||
@@ -194,7 +194,7 @@ export const features = [
|
||||
"Having the right tool at the right time is crucial for note taking. Lists, tables, codeblocks — you name it, we have it.",
|
||||
features: [
|
||||
{
|
||||
highlight: "Basic formatting",
|
||||
highlight: "Basic formating",
|
||||
content: "and lists",
|
||||
icon: "format-bold"
|
||||
},
|
||||
|
||||
@@ -18,29 +18,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import { View } from "react-native";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { PressableButton } from "../ui/pressable";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import RNIap from "react-native-iap";
|
||||
|
||||
export const PricingItem = ({
|
||||
product,
|
||||
onPress,
|
||||
compact,
|
||||
strikethrough
|
||||
}: {
|
||||
product: {
|
||||
type: "yearly" | "monthly";
|
||||
data?: RNIap.Subscription;
|
||||
info: string;
|
||||
offerType?: "yearly" | "monthly";
|
||||
};
|
||||
strikethrough?: boolean;
|
||||
onPress?: () => void;
|
||||
compact?: boolean;
|
||||
}) => {
|
||||
export const PricingItem = ({ product, onPress, compact }) => {
|
||||
return (
|
||||
<PressableButton
|
||||
onPress={onPress}
|
||||
@@ -52,10 +36,8 @@ export const PricingItem = ({
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: compact ? 15 : 10,
|
||||
width: compact ? null : "100%",
|
||||
minWidth: 150,
|
||||
opacity: strikethrough ? 0.7 : 1
|
||||
minWidth: 150
|
||||
}}
|
||||
disabled={strikethrough}
|
||||
>
|
||||
{!compact && (
|
||||
<View>
|
||||
@@ -71,25 +53,8 @@ export const PricingItem = ({
|
||||
)}
|
||||
|
||||
<View>
|
||||
<Paragraph
|
||||
style={{
|
||||
textDecorationLine: strikethrough ? "line-through" : undefined
|
||||
}}
|
||||
size={SIZE.sm}
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
textDecorationLine: strikethrough ? "line-through" : undefined
|
||||
}}
|
||||
size={SIZE.lg - 2}
|
||||
>
|
||||
{Platform.OS === "android"
|
||||
? (product.data as RNIap.SubscriptionAndroid | undefined)
|
||||
?.subscriptionOfferDetails[0].pricingPhases
|
||||
.pricingPhaseList?.[0]?.formattedPrice
|
||||
: (product.data as RNIap.SubscriptionIOS | undefined)
|
||||
?.localizedPrice}
|
||||
</Heading>
|
||||
<Paragraph size={SIZE.sm}>
|
||||
<Heading size={SIZE.lg - 2}>{product?.data?.localizedPrice}/</Heading>
|
||||
{product?.type === "yearly" || product?.offerType === "yearly"
|
||||
? "/year"
|
||||
: "/month"}
|
||||
@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { ActivityIndicator, Platform, Text, View } from "react-native";
|
||||
import * as RNIap from "react-native-iap";
|
||||
import { DatabaseLogger, db } from "../../common/database";
|
||||
import { db } from "../../common/database";
|
||||
import { usePricing } from "../../hooks/use-pricing";
|
||||
import {
|
||||
eSendEvent,
|
||||
@@ -47,29 +47,11 @@ import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { Walkthrough } from "../walkthroughs";
|
||||
import { PricingItem } from "./pricing-item";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
|
||||
const UUID_PREFIX = "0bdaea";
|
||||
const UUID_VERSION = "4";
|
||||
const UUID_VARIANT = "a";
|
||||
|
||||
function toUUID(str: string) {
|
||||
return [
|
||||
UUID_PREFIX + str.substring(0, 2), // 6 digit prefix + first 2 oid digits
|
||||
str.substring(2, 6), // # next 4 oid digits
|
||||
UUID_VERSION + str.substring(6, 9), // # 1 digit version(0x4) + next 3 oid digits
|
||||
UUID_VARIANT + str.substring(9, 12), // # 1 digit variant(0b101) + 1 zero bit + next 3 oid digits
|
||||
str.substring(12)
|
||||
].join("-");
|
||||
}
|
||||
|
||||
const promoCyclesMonthly = {
|
||||
1: "first month",
|
||||
2: "first 2 months",
|
||||
3: "first 3 months",
|
||||
4: "first 4 months",
|
||||
5: "first 5 months",
|
||||
6: "first 3 months"
|
||||
3: "first 3 months"
|
||||
};
|
||||
|
||||
const promoCyclesYearly = {
|
||||
@@ -83,24 +65,10 @@ export const PricingPlans = ({
|
||||
marginTop,
|
||||
heading = true,
|
||||
compact = false
|
||||
}: {
|
||||
promo?: {
|
||||
promoCode: string;
|
||||
};
|
||||
marginTop?: any;
|
||||
heading?: boolean;
|
||||
compact?: boolean;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const user = useUserStore((state) => state.user);
|
||||
const [product, setProduct] = useState<{
|
||||
type: string;
|
||||
offerType: "monthly" | "yearly";
|
||||
data: RNIap.Subscription;
|
||||
cycleText: string;
|
||||
info: string;
|
||||
}>();
|
||||
|
||||
const [product, setProduct] = useState(null);
|
||||
const [buying, setBuying] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const userCanRequestTrial =
|
||||
@@ -122,40 +90,27 @@ export const PricingPlans = ({
|
||||
}
|
||||
}, [promo?.promoCode]);
|
||||
|
||||
const getPromo = async (code: string) => {
|
||||
const getPromo = async (code) => {
|
||||
try {
|
||||
let skuId: string;
|
||||
let productId;
|
||||
if (code.startsWith("com.streetwriters.notesnook")) {
|
||||
skuId = code;
|
||||
productId = code;
|
||||
} else {
|
||||
skuId = await db.offers?.getCode(code.split(":")[0], Platform.OS);
|
||||
productId = await db.offers.getCode(code.split(":")[0], Platform.OS);
|
||||
}
|
||||
|
||||
const products = await PremiumService.getProducts();
|
||||
const product = products.find((p) => p.productId === skuId);
|
||||
let products = await PremiumService.getProducts();
|
||||
let product = products.find((p) => p.productId === productId);
|
||||
if (!product) return false;
|
||||
const isMonthly = product.productId.indexOf(".mo") > -1;
|
||||
|
||||
const cycleText = isMonthly
|
||||
let isMonthly = product.productId.indexOf(".mo") > -1;
|
||||
let cycleText = isMonthly
|
||||
? promoCyclesMonthly[
|
||||
(Platform.OS === "android"
|
||||
? (product as RNIap.SubscriptionAndroid)
|
||||
.subscriptionOfferDetails[0]?.pricingPhases
|
||||
.pricingPhaseList?.[0].billingCycleCount
|
||||
: parseInt(
|
||||
(product as RNIap.SubscriptionIOS)
|
||||
.introductoryPriceNumberOfPeriodsIOS as string
|
||||
)) as keyof typeof promoCyclesMonthly
|
||||
product.introductoryPriceCyclesAndroid ||
|
||||
product.introductoryPriceNumberOfPeriodsIOS
|
||||
]
|
||||
: promoCyclesYearly[
|
||||
(Platform.OS === "android"
|
||||
? (product as RNIap.SubscriptionAndroid)
|
||||
.subscriptionOfferDetails[0]?.pricingPhases
|
||||
.pricingPhaseList?.[0].billingCycleCount
|
||||
: parseInt(
|
||||
(product as RNIap.SubscriptionIOS)
|
||||
.introductoryPriceNumberOfPeriodsIOS as string
|
||||
)) as keyof typeof promoCyclesYearly
|
||||
product.introductoryPriceCyclesAndroid ||
|
||||
product.introductoryPriceNumberOfPeriodsIOS
|
||||
];
|
||||
|
||||
setProduct({
|
||||
@@ -163,7 +118,7 @@ export const PricingPlans = ({
|
||||
offerType: isMonthly ? "monthly" : "yearly",
|
||||
data: product,
|
||||
cycleText: cycleText,
|
||||
info: `Pay ${isMonthly ? "monthly" : "yearly"}, cancel anytime`
|
||||
info: "Pay monthly, cancel anytime"
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
@@ -176,41 +131,22 @@ export const PricingPlans = ({
|
||||
getSkus();
|
||||
}, [getSkus]);
|
||||
|
||||
const buySubscription = async (product: RNIap.Subscription) => {
|
||||
if (buying || !product) return;
|
||||
const buySubscription = async (product) => {
|
||||
if (buying) return;
|
||||
setBuying(true);
|
||||
try {
|
||||
if (!user) {
|
||||
setBuying(false);
|
||||
return;
|
||||
}
|
||||
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
|
||||
const androidOfferToken =
|
||||
Platform.OS === "android"
|
||||
? (product as RNIap.SubscriptionAndroid).subscriptionOfferDetails[0]
|
||||
.offerToken
|
||||
: null;
|
||||
|
||||
DatabaseLogger.info(
|
||||
`Subscription Requested initiated for user ${toUUID(user.id)}`
|
||||
await RNIap.requestSubscription(
|
||||
product?.productId,
|
||||
false,
|
||||
null,
|
||||
-1,
|
||||
user.id,
|
||||
user.id
|
||||
);
|
||||
|
||||
await RNIap.requestSubscription({
|
||||
sku: product?.productId,
|
||||
obfuscatedAccountIdAndroid: user.id,
|
||||
obfuscatedProfileIdAndroid: user.id,
|
||||
appAccountToken: toUUID(user.id),
|
||||
andDangerouslyFinishTransactionAutomaticallyIOS: false,
|
||||
subscriptionOffers: androidOfferToken
|
||||
? [
|
||||
{
|
||||
offerToken: androidOfferToken,
|
||||
sku: product?.productId
|
||||
}
|
||||
]
|
||||
: undefined
|
||||
});
|
||||
useSettingStore.getState().setAppDidEnterBackgroundForAction(false);
|
||||
setBuying(false);
|
||||
eSendEvent(eCloseSheet);
|
||||
eSendEvent(eClosePremiumDialog);
|
||||
@@ -231,35 +167,6 @@ export const PricingPlans = ({
|
||||
}
|
||||
};
|
||||
|
||||
function getStandardPrice() {
|
||||
if (!product) return;
|
||||
const productType = product.offerType;
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
const pricingPhaseListItem = (product.data as RNIap.SubscriptionAndroid)
|
||||
?.subscriptionOfferDetails[0]?.pricingPhases.pricingPhaseList?.[1];
|
||||
|
||||
if (!pricingPhaseListItem) {
|
||||
const product =
|
||||
productType === "monthly"
|
||||
? monthlyPlan?.product
|
||||
: yearlyPlan?.product;
|
||||
return (product as RNIap.SubscriptionAndroid)
|
||||
?.subscriptionOfferDetails[0]?.pricingPhases.pricingPhaseList?.[0]
|
||||
?.formattedPrice;
|
||||
}
|
||||
|
||||
return pricingPhaseListItem?.formattedPrice;
|
||||
} else {
|
||||
const productDefault =
|
||||
productType === "monthly" ? monthlyPlan?.product : yearlyPlan?.product;
|
||||
return (
|
||||
(product.data as RNIap.SubscriptionIOS)?.localizedPrice ||
|
||||
(productDefault as RNIap.SubscriptionIOS)?.localizedPrice
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return loading ? (
|
||||
<View
|
||||
style={{
|
||||
@@ -291,14 +198,7 @@ export const PricingPlans = ({
|
||||
}}
|
||||
size={SIZE.lg}
|
||||
>
|
||||
{(Platform.OS === "android"
|
||||
? (monthlyPlan?.product as RNIap.SubscriptionAndroid | undefined)
|
||||
?.subscriptionOfferDetails[0]?.pricingPhases
|
||||
.pricingPhaseList?.[0]?.formattedPrice
|
||||
: (monthlyPlan?.product as RNIap.SubscriptionIOS | undefined)
|
||||
?.localizedPrice) ||
|
||||
(PremiumService.getMontlySub() as any)?.localizedPrice}
|
||||
/ mo
|
||||
{PremiumService.getMontlySub().localizedPrice} / mo
|
||||
</Paragraph>
|
||||
<Button
|
||||
onPress={() => {
|
||||
@@ -318,7 +218,7 @@ export const PricingPlans = ({
|
||||
<Button
|
||||
onPress={async () => {
|
||||
try {
|
||||
await db.user?.activateTrial();
|
||||
await db.user.activateTrial();
|
||||
eSendEvent(eClosePremiumDialog);
|
||||
eSendEvent(eCloseSheet);
|
||||
await sleep(300);
|
||||
@@ -339,91 +239,33 @@ export const PricingPlans = ({
|
||||
) : (
|
||||
<>
|
||||
{product?.type === "promo" ? (
|
||||
<View
|
||||
<Heading
|
||||
style={{
|
||||
paddingVertical: 15,
|
||||
alignItems: "center"
|
||||
alignSelf: "center",
|
||||
textAlign: "center"
|
||||
}}
|
||||
size={SIZE.lg - 4}
|
||||
>
|
||||
{product?.offerType === "monthly" ? (
|
||||
<PricingItem
|
||||
product={{
|
||||
type: "monthly",
|
||||
data: monthlyPlan?.product,
|
||||
info: "Pay once a month, cancel anytime."
|
||||
}}
|
||||
strikethrough={true}
|
||||
/>
|
||||
) : (
|
||||
<PricingItem
|
||||
onPress={() => {
|
||||
if (!monthlyPlan?.product) return;
|
||||
buySubscription(monthlyPlan?.product);
|
||||
}}
|
||||
product={{
|
||||
type: "yearly",
|
||||
data: yearlyPlan?.product,
|
||||
info: "Pay once a year, cancel anytime."
|
||||
}}
|
||||
strikethrough={true}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Heading
|
||||
{product.data.introductoryPrice}
|
||||
<Paragraph
|
||||
style={{
|
||||
paddingTop: 15,
|
||||
fontSize: SIZE.lg
|
||||
textDecorationLine: "line-through",
|
||||
color: colors.secondary.paragraph
|
||||
}}
|
||||
size={SIZE.sm}
|
||||
>
|
||||
Special offer for you
|
||||
</Heading>
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingVertical: 20,
|
||||
paddingBottom: 10
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
textAlign: "center"
|
||||
}}
|
||||
size={SIZE.xxl}
|
||||
>
|
||||
{Platform.OS === "android"
|
||||
? (product.data as RNIap.SubscriptionAndroid)
|
||||
?.subscriptionOfferDetails[0].pricingPhases
|
||||
.pricingPhaseList?.[0]?.formattedPrice
|
||||
: (product.data as RNIap.SubscriptionIOS)
|
||||
?.introductoryPrice ||
|
||||
(product.data as RNIap.SubscriptionIOS)
|
||||
?.localizedPrice}{" "}
|
||||
{product?.cycleText
|
||||
? `for ${product.cycleText}`
|
||||
: product?.offerType}
|
||||
</Heading>
|
||||
{product?.cycleText ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
color: colors.secondary.paragraph,
|
||||
alignSelf: "center",
|
||||
textAlign: "center"
|
||||
}}
|
||||
size={SIZE.md}
|
||||
>
|
||||
then {getStandardPrice()} {product?.offerType}.
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
({product.data.localizedPrice})
|
||||
</Paragraph>{" "}
|
||||
for {product.cycleText}
|
||||
</Heading>
|
||||
) : null}
|
||||
|
||||
{user && !product ? (
|
||||
<>
|
||||
{heading || (monthlyPlan?.info?.discount || 0) > 0 ? (
|
||||
{heading || monthlyPlan?.info?.discount > 0 ? (
|
||||
<>
|
||||
{monthlyPlan && (monthlyPlan?.info?.discount || 0) > 0 ? (
|
||||
{monthlyPlan && monthlyPlan?.info?.discount > 0 ? (
|
||||
<View
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
@@ -431,12 +273,7 @@ export const PricingPlans = ({
|
||||
marginBottom: 20
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
<Heading color={colors.primary.accent}>
|
||||
Get {monthlyPlan?.info?.discount}% off in{" "}
|
||||
{monthlyPlan?.info?.country}
|
||||
</Heading>
|
||||
@@ -463,15 +300,12 @@ export const PricingPlans = ({
|
||||
}}
|
||||
>
|
||||
<PricingItem
|
||||
onPress={() => {
|
||||
if (!monthlyPlan?.product) return;
|
||||
buySubscription(monthlyPlan?.product);
|
||||
}}
|
||||
onPress={() => buySubscription(monthlyPlan?.product)}
|
||||
compact={compact}
|
||||
product={{
|
||||
type: "monthly",
|
||||
data: monthlyPlan?.product,
|
||||
info: "Pay once a month, cancel anytime."
|
||||
info: "Pay monthly, cancel anytime."
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -485,15 +319,12 @@ export const PricingPlans = ({
|
||||
)}
|
||||
|
||||
<PricingItem
|
||||
onPress={() => {
|
||||
if (!yearlyPlan?.product) return;
|
||||
buySubscription(yearlyPlan?.product);
|
||||
}}
|
||||
onPress={() => buySubscription(yearlyPlan?.product)}
|
||||
compact={compact}
|
||||
product={{
|
||||
type: "yearly",
|
||||
data: yearlyPlan?.product,
|
||||
info: "Pay once a year, cancel anytime."
|
||||
info: "Pay yearly"
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
@@ -515,7 +346,7 @@ export const PricingPlans = ({
|
||||
eSendEvent(eCloseSimpleDialog);
|
||||
setBuying(true);
|
||||
try {
|
||||
if (!(await getPromo(value as string)))
|
||||
if (!(await getPromo(value)))
|
||||
throw new Error("Error applying promo code");
|
||||
ToastEvent.show({
|
||||
heading: "Discount applied!",
|
||||
@@ -527,7 +358,7 @@ export const PricingPlans = ({
|
||||
setBuying(false);
|
||||
ToastEvent.show({
|
||||
heading: "Promo code invalid or expired",
|
||||
message: (e as Error).message,
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -565,7 +396,7 @@ export const PricingPlans = ({
|
||||
width={250}
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
marginTop: product?.type === "promo" ? 0 : 30,
|
||||
marginTop: 30,
|
||||
marginBottom: 10
|
||||
}}
|
||||
/>
|
||||
@@ -596,10 +427,7 @@ export const PricingPlans = ({
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
onPress={() => {
|
||||
if (!product?.data) return;
|
||||
buySubscription(product.data);
|
||||
}}
|
||||
onPress={() => buySubscription(product.data)}
|
||||
height={40}
|
||||
width="50%"
|
||||
type="accent"
|
||||
@@ -608,7 +436,7 @@ export const PricingPlans = ({
|
||||
|
||||
<Button
|
||||
onPress={() => {
|
||||
setProduct(undefined);
|
||||
setProduct(null);
|
||||
}}
|
||||
style={{
|
||||
marginTop: 5
|
||||
@@ -672,7 +500,7 @@ export const PricingPlans = ({
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
By subscribing, you will be charged on your Google Account, and
|
||||
By subscribing, your will be charged on your Google Account, and
|
||||
your subscription will automatically renew until you cancel prior
|
||||
to the end of the then current period.
|
||||
</Paragraph>
|
||||
@@ -695,7 +523,7 @@ export const PricingPlans = ({
|
||||
<Paragraph
|
||||
size={SIZE.xs}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/tos")
|
||||
openLinkInBrowser("https://notesnook.com/tos", colors)
|
||||
.catch(() => {})
|
||||
.then(() => {});
|
||||
}}
|
||||
@@ -710,7 +538,7 @@ export const PricingPlans = ({
|
||||
<Paragraph
|
||||
size={SIZE.xs}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/privacy")
|
||||
openLinkInBrowser("https://notesnook.com/privacy", colors)
|
||||
.catch(() => {})
|
||||
.then(() => {});
|
||||
}}
|
||||
@@ -16,8 +16,8 @@ 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 } from "react";
|
||||
import { Dimensions, Platform, View, useWindowDimensions } from "react-native";
|
||||
import React from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import { db } from "../../common/database";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
@@ -54,6 +54,7 @@ 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 (
|
||||
<Paragraph style={{ marginVertical: 10, alignSelf: "center" }}>
|
||||
@@ -61,7 +62,6 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
|
||||
</Paragraph>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
keyboardShouldPersistTaps="always"
|
||||
@@ -135,7 +135,6 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
|
||||
}, 1000);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Synced item={item} close={close} />
|
||||
|
||||
{DDS.isTab ? (
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { Dimensions, FlatList, ScrollView, View } from "react-native";
|
||||
import { FlatList, ScrollView, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { useActions } from "../../hooks/use-actions";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
@@ -33,11 +33,9 @@ export const Items = ({ item, buttons, close }) => {
|
||||
const dimensions = useSettingStore((state) => state.dimensions);
|
||||
const actions = useActions({ item, close });
|
||||
const data = actions.filter((i) => buttons.indexOf(i.id) > -1 && !i.hidden);
|
||||
|
||||
let width = dimensions.width > 600 ? 600 : dimensions.width;
|
||||
const shouldShrink =
|
||||
Dimensions.get("window").fontScale > 1 &&
|
||||
Dimensions.get("window").width < 450;
|
||||
let columnItemsCount = DDS.isLargeTablet() ? 7 : shouldShrink ? 4 : 5;
|
||||
let columnItemsCount = DDS.isLargeTablet() ? 7 : 5;
|
||||
let columnItemWidth = DDS.isTab
|
||||
? (width - 12) / columnItemsCount
|
||||
: (width - 12) / columnItemsCount;
|
||||
@@ -68,7 +66,6 @@ export const Items = ({ item, buttons, close }) => {
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
allowFontScaling
|
||||
name={item.icon}
|
||||
size={DDS.isTab ? SIZE.xxl : SIZE.lg}
|
||||
color={
|
||||
@@ -81,11 +78,7 @@ export const Items = ({ item, buttons, close }) => {
|
||||
/>
|
||||
</PressableButton>
|
||||
|
||||
<Paragraph
|
||||
size={SIZE.xs}
|
||||
textBreakStrategy="simple"
|
||||
style={{ textAlign: "center" }}
|
||||
>
|
||||
<Paragraph size={SIZE.xs} style={{ textAlign: "center" }}>
|
||||
{item.title}
|
||||
</Paragraph>
|
||||
</View>
|
||||
@@ -146,7 +139,6 @@ export const Items = ({ item, buttons, close }) => {
|
||||
>
|
||||
<Icon
|
||||
name={item.icon}
|
||||
allowFontScaling
|
||||
size={DDS.isTab ? SIZE.xxl : SIZE.md + 4}
|
||||
color={
|
||||
item.on
|
||||
@@ -158,11 +150,7 @@ export const Items = ({ item, buttons, close }) => {
|
||||
/>
|
||||
</PressableButton>
|
||||
|
||||
<Paragraph
|
||||
textBreakStrategy="simple"
|
||||
size={SIZE.xxs + 1}
|
||||
style={{ textAlign: "center" }}
|
||||
>
|
||||
<Paragraph size={SIZE.xxs + 1} style={{ textAlign: "center" }}>
|
||||
{item.title}
|
||||
</Paragraph>
|
||||
</PressableButton>
|
||||
@@ -175,12 +163,9 @@ export const Items = ({ item, buttons, close }) => {
|
||||
"copy",
|
||||
"share",
|
||||
"export",
|
||||
"lock-unlock"
|
||||
"lock-unlock",
|
||||
"publish"
|
||||
];
|
||||
if (!shouldShrink) {
|
||||
topBarItemsList.push("publish");
|
||||
}
|
||||
|
||||
const topBarItems = data.filter(
|
||||
(item) => topBarItemsList.indexOf(item.id) > -1
|
||||
);
|
||||
|
||||
@@ -17,10 +17,10 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { View, useWindowDimensions } from "react-native";
|
||||
import { View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { openLinkInBrowser } from "../../utils/functions";
|
||||
import { SIZE } from "../../utils/size";
|
||||
@@ -33,8 +33,6 @@ export const Synced = ({ item, close }) => {
|
||||
const user = useUserStore((state) => state.user);
|
||||
const lastSynced = useUserStore((state) => state.lastSynced);
|
||||
|
||||
const dimensions = useWindowDimensions();
|
||||
const shouldShrink = dimensions.fontScale > 1 && dimensions.width < 450;
|
||||
return user && lastSynced >= item.dateModified ? (
|
||||
<View
|
||||
style={{
|
||||
@@ -51,11 +49,7 @@ export const Synced = ({ item, close }) => {
|
||||
borderTopColor: colors.secondary.background
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="shield-key-outline"
|
||||
color={colors.primary.accent}
|
||||
size={shouldShrink ? SIZE.xxl : SIZE.xxxl}
|
||||
/>
|
||||
<Icon name="shield-key-outline" color={colors.primary.accent} size={SIZE.xxxl} />
|
||||
|
||||
<View
|
||||
style={{
|
||||
@@ -73,17 +67,15 @@ export const Synced = ({ item, close }) => {
|
||||
>
|
||||
Encrypted and synced
|
||||
</Heading>
|
||||
{shouldShrink ? null : (
|
||||
<Paragraph
|
||||
style={{
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
color={colors.primary.paragraph}
|
||||
>
|
||||
No one can view this {item.itemType || item.type} except you.
|
||||
</Paragraph>
|
||||
)}
|
||||
<Paragraph
|
||||
style={{
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
color={colors.primary.paragraph}
|
||||
>
|
||||
No one can view this {item.itemType || item.type} except you.
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
@@ -99,8 +91,8 @@ export const Synced = ({ item, close }) => {
|
||||
console.error(e);
|
||||
}
|
||||
}}
|
||||
title="Learn more"
|
||||
fontSize={SIZE.xs}
|
||||
title="Learn more"
|
||||
height={30}
|
||||
type="grayAccent"
|
||||
/>
|
||||
|
||||
@@ -207,6 +207,7 @@ export const SelectionHeader = React.memo(() => {
|
||||
|
||||
{screen === "Trash" ||
|
||||
screen === "Notebooks" ||
|
||||
screen === "Notebook" ||
|
||||
screen === "Reminders" ? null : (
|
||||
<>
|
||||
<IconButton
|
||||
@@ -223,21 +224,6 @@ export const SelectionHeader = React.memo(() => {
|
||||
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);
|
||||
@@ -253,6 +239,20 @@ export const SelectionHeader = React.memo(() => {
|
||||
name="plus"
|
||||
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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -317,8 +317,23 @@ export const SelectionHeader = React.memo(() => {
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
onPress={() => {
|
||||
deleteItems();
|
||||
onPress={async () => {
|
||||
presentDialog({
|
||||
title: `Delete ${
|
||||
selectedItemsList.length > 1 ? "items" : "item"
|
||||
}`,
|
||||
paragraph: `Are you sure you want to delete ${
|
||||
selectedItemsList.length > 1 ? "these items?" : "this item?"
|
||||
}`,
|
||||
positiveText: "Delete",
|
||||
negativeText: "Cancel",
|
||||
positivePress: () => {
|
||||
deleteItems();
|
||||
},
|
||||
positiveType: "errorShade"
|
||||
});
|
||||
|
||||
return;
|
||||
}}
|
||||
tooltipText="Move to trash"
|
||||
tooltipPosition={1}
|
||||
|
||||
@@ -101,13 +101,8 @@ const SheetProvider = ({ context = "global" }) => {
|
||||
setVisible(false);
|
||||
setData(null);
|
||||
}}
|
||||
keyboardHandlerDisabled={data?.keyboardHandlerDisabled}
|
||||
bottomPadding={!data.noBottomPadding}
|
||||
enableGesturesInScrollView={
|
||||
typeof data.enableGesturesInScrollView === "undefined"
|
||||
? true
|
||||
: data.enableGesturesInScrollView
|
||||
}
|
||||
enableGesturesInScrollView={data.enableGesturesInScrollView}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
@@ -132,7 +127,7 @@ const SheetProvider = ({ context = "global" }) => {
|
||||
|
||||
{data?.icon ? (
|
||||
<Icon
|
||||
color={colors[data.iconColor] || colors.primary.accent}
|
||||
color={colors[dialogData.iconColor] || colors.primary.accent}
|
||||
name={data.icon}
|
||||
size={50}
|
||||
/>
|
||||
@@ -199,6 +194,7 @@ const SheetProvider = ({ context = "global" }) => {
|
||||
key={data.actionText}
|
||||
title={data.actionText}
|
||||
accentColor={data.iconColor}
|
||||
accentText="light"
|
||||
type="accent"
|
||||
height={40}
|
||||
width={250}
|
||||
|
||||
@@ -18,7 +18,7 @@ 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 { FlatList } from "react-native-actions-sheet";
|
||||
import { db } from "../../../common/database";
|
||||
import { ListHeaderInputItem } from "./list-header-item.js";
|
||||
|
||||
@@ -54,7 +54,7 @@ export const FilteredList = ({
|
||||
}, [data, onChangeText]);
|
||||
|
||||
return (
|
||||
<FlashList
|
||||
<FlatList
|
||||
{...restProps}
|
||||
data={filtered}
|
||||
ref={listRef}
|
||||
|
||||
@@ -161,6 +161,9 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
: "deselected";
|
||||
if (itemState[notebook.id] === "selected") {
|
||||
count++;
|
||||
contextValue.select(notebook);
|
||||
} else {
|
||||
contextValue.deselect(notebook);
|
||||
}
|
||||
for (let topic of notebook.topics) {
|
||||
itemState[topic.id] = state
|
||||
@@ -173,6 +176,9 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
: "deselected";
|
||||
if (itemState[topic.id] === "selected") {
|
||||
count++;
|
||||
contextValue.select(topic);
|
||||
} else {
|
||||
contextValue.deselect(topic);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,7 +189,7 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
}
|
||||
useItemSelectionStore.getState().setItemState(itemState);
|
||||
},
|
||||
[getSelectedNotesCountInItem, selectedItemsList]
|
||||
[contextValue, getSelectedNotesCountInItem, selectedItemsList]
|
||||
);
|
||||
|
||||
const getItemsForItem = (item) => {
|
||||
@@ -208,7 +214,7 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
}
|
||||
|
||||
const updateItemState = useCallback(function (item, state) {
|
||||
const itemState = { ...useItemSelectionStore.getState().itemState };
|
||||
const itemState = useItemSelectionStore.getState().itemState;
|
||||
const mergeState = {
|
||||
[item.id]: state
|
||||
};
|
||||
@@ -364,93 +370,56 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
</View>
|
||||
|
||||
<SelectionProvider value={contextValue}>
|
||||
<View
|
||||
<FilteredList
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
maxHeight: dimensions.height * 0.85,
|
||||
height: 50 * (notebooks.length + 2)
|
||||
maxHeight: dimensions.height * 0.85
|
||||
}}
|
||||
>
|
||||
<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();
|
||||
ListEmptyComponent={
|
||||
notebooks.length > 0 ? null : (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
hasSubList={true}
|
||||
hasHeaderSearch={false}
|
||||
type="grayBg"
|
||||
sublistItemType="topic"
|
||||
onAddItem={(title) => {
|
||||
return onAddTopic(title, item);
|
||||
}}
|
||||
onAddSublistItem={(item) => {
|
||||
openAddTopicDialog(item);
|
||||
}}
|
||||
onPress={(item) => {
|
||||
>
|
||||
<Icon
|
||||
name="book-outline"
|
||||
color={colors.primary.icon}
|
||||
size={100}
|
||||
/>
|
||||
<Paragraph style={{ marginBottom: 10 }}>
|
||||
You do not have any notebooks.
|
||||
</Paragraph>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
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];
|
||||
@@ -460,16 +429,47 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
} else {
|
||||
contextValue.deselect(item);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
itemType="notebook"
|
||||
onAddItem={async (title) => {
|
||||
return await onAddNotebook(title);
|
||||
}}
|
||||
ListFooterComponent={<View style={{ height: 20 }} />}
|
||||
/>
|
||||
</View>
|
||||
},
|
||||
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 }} />}
|
||||
/>
|
||||
</SelectionProvider>
|
||||
</View>
|
||||
</>
|
||||
|
||||
@@ -273,7 +273,6 @@ export const ListItem = ({
|
||||
alignSelf: "flex-end",
|
||||
maxHeight: 250
|
||||
}}
|
||||
estimatedItemSize={40}
|
||||
itemType={sublistItemType}
|
||||
hasHeaderSearch={hasHeaderSearch}
|
||||
renderItem={({ item, index }) => (
|
||||
|
||||
@@ -43,7 +43,6 @@ 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 = ({ notes, update }) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -98,16 +97,6 @@ const ExportNotesSheet = ({ notes, update }) => {
|
||||
id: notesnook.ids.dialogs.export.md,
|
||||
pro: premium
|
||||
},
|
||||
{
|
||||
title: "Markdown + Frontmatter",
|
||||
func: async () => {
|
||||
await save("md-frontmatter");
|
||||
},
|
||||
icon: "language-markdown",
|
||||
desc: "View in any text or markdown editor",
|
||||
id: notesnook.ids.dialogs.export.md,
|
||||
pro: premium
|
||||
},
|
||||
{
|
||||
title: "Plain Text",
|
||||
func: async () => {
|
||||
@@ -158,8 +147,6 @@ const ExportNotesSheet = ({ notes, update }) => {
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<Dialog context="export-notes" />
|
||||
|
||||
<View style={styles.buttonContainer}>
|
||||
{!exporting && !complete ? (
|
||||
actions.map((item) => (
|
||||
@@ -191,9 +178,7 @@ const ExportNotesSheet = ({ notes, update }) => {
|
||||
>
|
||||
<Icon
|
||||
name={item.icon}
|
||||
color={
|
||||
item.pro ? colors.primary.accent : colors.primary.icon
|
||||
}
|
||||
color={item.pro ? colors.primary.accent : colors.primary.icon}
|
||||
size={SIZE.xxxl + 10}
|
||||
/>
|
||||
</View>
|
||||
@@ -349,8 +334,7 @@ ExportNotesSheet.present = (notes, allNotes) => {
|
||||
notes={allNotes ? db.notes.all : notes}
|
||||
update={update}
|
||||
/>
|
||||
),
|
||||
keyboardHandlerDisabled: true
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -35,14 +35,10 @@ import { presentDialog } from "../../dialog/functions";
|
||||
import { Button } from "../../ui/button";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { useStoredRef } from "../../../hooks/use-stored-ref";
|
||||
|
||||
export const Issue = ({ defaultTitle, defaultBody, issueTitle }) => {
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
const body = useStoredRef("issueBody", defaultBody);
|
||||
const title = useStoredRef("issueTitle", defaultTitle);
|
||||
|
||||
const body = useRef(defaultBody);
|
||||
const title = useRef(defaultTitle);
|
||||
const user = useUserStore((state) => state.user);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const bodyRef = useRef();
|
||||
@@ -74,8 +70,6 @@ Logged in: ${user ? "yes" : "no"}`,
|
||||
});
|
||||
setLoading(false);
|
||||
eSendEvent(eCloseSheet);
|
||||
body.reset();
|
||||
title.reset();
|
||||
await sleep(300);
|
||||
presentDialog({
|
||||
title: "Issue reported",
|
||||
@@ -111,7 +105,7 @@ Logged in: ${user ? "yes" : "no"}`,
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
ToastEvent.show({
|
||||
heading: "An error occurred",
|
||||
heading: "An error occured",
|
||||
message: e.message,
|
||||
type: "error"
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { EVENTS } from "@notesnook/core/dist/common";
|
||||
import { EVENTS } from "@notesnook/core/common";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import { db } from "../../../common/database";
|
||||
@@ -177,7 +177,7 @@ export default function Migrate() {
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
An error occurred while migrating your data. You can logout of your
|
||||
An error occured while migrating your data. You can logout of your
|
||||
account and try to relogin. However this is not recommended as it
|
||||
may result in some data loss if your data was not synced.
|
||||
</Paragraph>
|
||||
|
||||
@@ -64,7 +64,7 @@ export const Progress = () => {
|
||||
width={null}
|
||||
animated={true}
|
||||
useNativeDriver
|
||||
indeterminate
|
||||
progress={currentProgress || 0.1}
|
||||
unfilledColor={colors.secondary.background}
|
||||
color={colors.primary.accent}
|
||||
borderWidth={0}
|
||||
@@ -74,7 +74,7 @@ export const Progress = () => {
|
||||
{progress ? (
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
{progress.type?.slice(0, 1).toUpperCase() + progress.type?.slice(1)}
|
||||
ing {progress?.current}
|
||||
ing {progress?.current}/{progress?.total}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
@@ -37,18 +37,17 @@ import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { requestInAppReview } from "../../../services/app-review";
|
||||
|
||||
const PublishNoteSheet = ({ note: item }) => {
|
||||
const PublishNoteSheet = ({ note: item, update }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const actionSheetRef = useRef();
|
||||
|
||||
const attachmentDownloads = useAttachmentStore((state) => state.downloading);
|
||||
const downloading = attachmentDownloads[`monograph-${item.id}`];
|
||||
const loading = useAttachmentStore((state) => state.loading);
|
||||
const [selfDestruct, setSelfDestruct] = useState(false);
|
||||
const [isLocked, setIsLocked] = useState(false);
|
||||
const [note, setNote] = useState(item);
|
||||
const [publishing, setPublishing] = useState(false);
|
||||
const publishUrl =
|
||||
note && `https://monogr.ph/${db?.monographs.monograph(note?.id)}`;
|
||||
note &&
|
||||
`https://monograph.notesnook.com/${db?.monographs.monograph(note?.id)}`;
|
||||
const isPublished = note && db?.monographs.isPublished(note?.id);
|
||||
const pwdInput = useRef();
|
||||
const passwordValue = useRef();
|
||||
@@ -138,9 +137,9 @@ const PublishNoteSheet = ({ note: item }) => {
|
||||
}}
|
||||
>
|
||||
Please wait...
|
||||
{downloading && downloading.current && downloading.total
|
||||
{loading && loading.current && loading.total
|
||||
? `\nDownloading attachments (${
|
||||
downloading?.current / downloading?.total
|
||||
loading?.current / loading?.total
|
||||
})`
|
||||
: ""}
|
||||
</Paragraph>
|
||||
|
||||
@@ -16,7 +16,6 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { RefObject, useRef, useState } from "react";
|
||||
import { Platform, TextInput, View } from "react-native";
|
||||
import { ActionSheetRef, ScrollView } from "react-native-actions-sheet";
|
||||
@@ -26,6 +25,7 @@ import {
|
||||
ToastEvent,
|
||||
presentSheet
|
||||
} from "../../../services/event-manager";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Button } from "../../ui/button";
|
||||
import Input from "../../ui/input";
|
||||
@@ -223,7 +223,7 @@ export default function ReminderSheet({
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
maxHeight: DDS.isTab ? "99.99%" : "99.99%"
|
||||
maxHeight: DDS.isTab ? "90%" : "99.99%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -247,63 +247,7 @@ export default function ReminderSheet({
|
||||
/>
|
||||
</View>
|
||||
<Dialog context="local" />
|
||||
<ScrollView
|
||||
bounces={false}
|
||||
style={{
|
||||
marginBottom: DDS.isTab ? 25 : undefined
|
||||
}}
|
||||
>
|
||||
{reminderMode === ReminderModes.Permanent ? null : (
|
||||
<ScrollView
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
borderWidth: 1,
|
||||
marginTop: 12,
|
||||
borderRadius: 5,
|
||||
borderColor: colors.primary.border,
|
||||
paddingLeft: 12,
|
||||
height: 50
|
||||
}}
|
||||
horizontal
|
||||
>
|
||||
{Object.keys(ReminderNotificationModes).map((mode) => (
|
||||
<Button
|
||||
key={mode}
|
||||
title={mode}
|
||||
style={{
|
||||
marginRight: 12,
|
||||
borderRadius: 100
|
||||
}}
|
||||
icon={
|
||||
mode === "Silent"
|
||||
? "minus-circle"
|
||||
: mode === "Vibrate"
|
||||
? "vibrate"
|
||||
: "volume-high"
|
||||
}
|
||||
height={35}
|
||||
type={
|
||||
reminderNotificationMode ===
|
||||
ReminderNotificationModes[
|
||||
mode as keyof typeof ReminderNotificationModes
|
||||
]
|
||||
? "grayAccent"
|
||||
: "gray"
|
||||
}
|
||||
onPress={() => {
|
||||
const _mode = ReminderNotificationModes[
|
||||
mode as keyof typeof ReminderNotificationModes
|
||||
] as Reminder["priority"];
|
||||
SettingsService.set({
|
||||
reminderNotificationMode: _mode
|
||||
});
|
||||
setReminderNotificatioMode(_mode);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
)}
|
||||
|
||||
<ScrollView>
|
||||
<Input
|
||||
fwdRef={titleRef}
|
||||
defaultValue={reminder?.title || referencedItem?.title}
|
||||
@@ -331,21 +275,15 @@ export default function ReminderSheet({
|
||||
}}
|
||||
height={80}
|
||||
wrapperStyle={{
|
||||
marginBottom: 12
|
||||
marginBottom: 20
|
||||
}}
|
||||
/>
|
||||
|
||||
<ScrollView
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
marginBottom: 12,
|
||||
height: 50,
|
||||
borderWidth: 1,
|
||||
borderRadius: 5,
|
||||
borderColor: colors.primary.border,
|
||||
paddingLeft: 12
|
||||
marginBottom: 12
|
||||
}}
|
||||
horizontal
|
||||
>
|
||||
{Object.keys(ReminderModes).map((mode) => (
|
||||
<Button
|
||||
@@ -384,7 +322,7 @@ export default function ReminderSheet({
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{reminderMode === ReminderModes.Repeat ? (
|
||||
<View
|
||||
@@ -531,7 +469,9 @@ export default function ReminderSheet({
|
||||
androidVariant="nativeAndroid"
|
||||
is24hourSource="locale"
|
||||
locale={
|
||||
db.settings?.getTimeFormat() === "24-hour" ? "en_GB" : "en_US"
|
||||
db.settings?.getTimeFormat() === "24-hour"
|
||||
? "en_GB.UTF8"
|
||||
: "en_US.UTF8"
|
||||
}
|
||||
mode={reminderMode === ReminderModes.Repeat ? "time" : "datetime"}
|
||||
/>
|
||||
@@ -553,6 +493,53 @@ export default function ReminderSheet({
|
||||
</View>
|
||||
)}
|
||||
|
||||
{reminderMode === ReminderModes.Permanent ? null : (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
marginBottom: 12,
|
||||
paddingTop: 12,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.secondary.background
|
||||
}}
|
||||
>
|
||||
{Object.keys(ReminderNotificationModes).map((mode) => (
|
||||
<Button
|
||||
key={mode}
|
||||
title={mode}
|
||||
style={{
|
||||
marginRight: 12,
|
||||
borderRadius: 100
|
||||
}}
|
||||
icon={
|
||||
mode === "Silent"
|
||||
? "minus-circle"
|
||||
: mode === "Vibrate"
|
||||
? "vibrate"
|
||||
: "volume-high"
|
||||
}
|
||||
height={35}
|
||||
type={
|
||||
reminderNotificationMode ===
|
||||
ReminderNotificationModes[
|
||||
mode as keyof typeof ReminderNotificationModes
|
||||
]
|
||||
? "grayAccent"
|
||||
: "gray"
|
||||
}
|
||||
onPress={() => {
|
||||
const _mode = ReminderNotificationModes[
|
||||
mode as keyof typeof ReminderNotificationModes
|
||||
] as Reminder["priority"];
|
||||
SettingsService.set({
|
||||
reminderNotificationMode: _mode
|
||||
});
|
||||
setReminderNotificatioMode(_mode);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
{reminderMode === ReminderModes.Once ||
|
||||
reminderMode === ReminderModes.Permanent ? null : (
|
||||
<View
|
||||
@@ -603,11 +590,6 @@ export default function ReminderSheet({
|
||||
}}
|
||||
/>
|
||||
</ScrollView>
|
||||
<View
|
||||
style={{
|
||||
height: 10
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -619,7 +601,7 @@ ReminderSheet.present = (
|
||||
) => {
|
||||
presentSheet({
|
||||
context: isSheet ? "local" : undefined,
|
||||
enableGesturesInScrollView: false,
|
||||
enableGesturesInScrollView: true,
|
||||
noBottomPadding: true,
|
||||
component: (ref, close, update) => (
|
||||
<ReminderSheet
|
||||
|
||||
@@ -17,19 +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 { getFormattedDate } from "@notesnook/common";
|
||||
import { EVENTS } from "@notesnook/core/dist/common";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { EVENTS } from "@notesnook/core/common";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, Platform, View } from "react-native";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import DocumentPicker from "react-native-document-picker";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { unzip } from "react-native-zip-archive";
|
||||
import { db } from "../../../common/database";
|
||||
import storage from "../../../common/database/storage";
|
||||
import { cacheDir, copyFileAsync } from "../../../common/filesystem/utils";
|
||||
import {
|
||||
ToastEvent,
|
||||
eSubscribeEvent,
|
||||
@@ -37,6 +33,7 @@ import {
|
||||
} from "../../../services/event-manager";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { initialize } from "../../../stores";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { eCloseRestoreDialog, eOpenRestoreDialog } from "../../../utils/events";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Dialog } from "../../dialog";
|
||||
@@ -47,6 +44,7 @@ import { Button } from "../../ui/button";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import SheetWrapper from "../../ui/sheet";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { getFormattedDate } from "@notesnook/common";
|
||||
|
||||
const RestoreDataSheet = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
@@ -131,78 +129,75 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
checkBackups();
|
||||
}, 1000);
|
||||
}, 300);
|
||||
}, []);
|
||||
|
||||
const restore = async (item) => {
|
||||
if (restoring) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const file = Platform.OS === "ios" ? item.path : item.uri;
|
||||
console.log(file);
|
||||
if (file.endsWith(".nnbackupz")) {
|
||||
setRestoring(true);
|
||||
setRestoring(true);
|
||||
let prefix = Platform.OS === "ios" ? "" : "file:/";
|
||||
let backup;
|
||||
if (Platform.OS === "android") {
|
||||
backup = await ScopedStorage.readFile(item.uri, "utf8");
|
||||
} else {
|
||||
backup = await RNFetchBlob.fs.readFile(prefix + item.path, "utf8");
|
||||
}
|
||||
backup = JSON.parse(backup);
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
const cacheFile = `file://${RNFetchBlob.fs.dirs.CacheDir}/backup.zip`;
|
||||
if (await RNFetchBlob.fs.exists(cacheFile)) {
|
||||
await RNFetchBlob.fs.unlink(cacheFile);
|
||||
if (backup.data.iv && backup.data.salt) {
|
||||
withPassword(
|
||||
async (value) => {
|
||||
try {
|
||||
await restoreBackup(backup, value);
|
||||
close();
|
||||
setRestoring(false);
|
||||
return true;
|
||||
} catch (e) {
|
||||
backupError(e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
() => {
|
||||
setRestoring(false);
|
||||
}
|
||||
await RNFetchBlob.fs.createFile(cacheFile, "", "utf8");
|
||||
console.log("copying");
|
||||
await copyFileAsync(file, cacheFile);
|
||||
console.log("copied");
|
||||
await restoreFromZip(cacheFile);
|
||||
} else {
|
||||
await restoreFromZip(file, false);
|
||||
}
|
||||
} else if (file.endsWith(".nnbackup")) {
|
||||
let backup;
|
||||
if (Platform.OS === "android") {
|
||||
backup = await ScopedStorage.readFile(file, "utf8");
|
||||
} else {
|
||||
backup = await RNFetchBlob.fs.readFile(file, "utf8");
|
||||
}
|
||||
await restoreFromNNBackup(JSON.parse(backup));
|
||||
);
|
||||
} else {
|
||||
await restoreBackup(backup);
|
||||
close();
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("error", e);
|
||||
setRestoring(false);
|
||||
backupError(e);
|
||||
}
|
||||
};
|
||||
|
||||
const withPassword = () => {
|
||||
return new Promise((resolve) => {
|
||||
let resolved = false;
|
||||
presentDialog({
|
||||
context: "local",
|
||||
title: "Encrypted backup",
|
||||
input: true,
|
||||
inputPlaceholder: "Password",
|
||||
paragraph: "Please enter password of this backup file",
|
||||
positiveText: "Restore",
|
||||
secureTextEntry: true,
|
||||
onClose: () => {
|
||||
if (resolved) return;
|
||||
resolve(undefined);
|
||||
},
|
||||
negativeText: "Cancel",
|
||||
positivePress: async (password, isEncryptionKey) => {
|
||||
resolve({
|
||||
value: password,
|
||||
isEncryptionKey
|
||||
const withPassword = (onsubmit, onclose = () => {}) => {
|
||||
presentDialog({
|
||||
context: "local",
|
||||
title: "Encrypted backup",
|
||||
input: true,
|
||||
inputPlaceholder: "Password",
|
||||
paragraph: "Please enter password of this backup file to restore it",
|
||||
positiveText: "Restore",
|
||||
secureTextEntry: true,
|
||||
onClose: onclose,
|
||||
negativeText: "Cancel",
|
||||
positivePress: async (password) => {
|
||||
try {
|
||||
return await onsubmit(password);
|
||||
} catch (e) {
|
||||
ToastEvent.show({
|
||||
heading: "Failed to backup data",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "global"
|
||||
});
|
||||
resolved = true;
|
||||
return true;
|
||||
},
|
||||
check: {
|
||||
info: "Use encryption key",
|
||||
type: "transparent"
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -222,180 +217,21 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
|
||||
let path = await storage.checkAndCreateDir("/backups/");
|
||||
files = await RNFetchBlob.fs.lstat(path);
|
||||
}
|
||||
files = files
|
||||
.filter((file) => {
|
||||
const name = Platform.OS === "android" ? file.name : file.filename;
|
||||
return name.endsWith(".nnbackup") || name.endsWith(".nnbackupz");
|
||||
})
|
||||
.sort(function (a, b) {
|
||||
let timeA = a.lastModified;
|
||||
let timeB = b.lastModified;
|
||||
return timeB - timeA;
|
||||
});
|
||||
files = files.sort(function (a, b) {
|
||||
let timeA = a.lastModified;
|
||||
let timeB = b.lastModified;
|
||||
return timeB - timeA;
|
||||
});
|
||||
setFiles(files);
|
||||
setLoading(false);
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
}, 1000);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const restoreBackup = async (backup, password, key) => {
|
||||
await db.backup.import(backup, password, key);
|
||||
await db.initCollections();
|
||||
initialize();
|
||||
ToastEvent.show({
|
||||
heading: "Backup restored successfully.",
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
const backupError = (e) => {
|
||||
ToastEvent.show({
|
||||
heading: "Restore failed",
|
||||
message:
|
||||
e.message ||
|
||||
"The selected backup data file is invalid. You must select a *.nnbackup file to restore.",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} file
|
||||
*/
|
||||
async function restoreFromZip(file, remove) {
|
||||
try {
|
||||
const zipOutputFolder = `${cacheDir}/backup_extracted`;
|
||||
if (await RNFetchBlob.fs.exists(zipOutputFolder)) {
|
||||
await RNFetchBlob.fs.unlink(zipOutputFolder);
|
||||
await RNFetchBlob.fs.mkdir(zipOutputFolder);
|
||||
}
|
||||
await unzip(file, zipOutputFolder);
|
||||
console.log("Unzipped files successfully to", zipOutputFolder);
|
||||
|
||||
const backupFiles = await RNFetchBlob.fs.ls(zipOutputFolder);
|
||||
|
||||
if (backupFiles.findIndex((file) => file === ".nnbackup") === -1) {
|
||||
throw new Error("Backup file is invalid");
|
||||
}
|
||||
|
||||
let password;
|
||||
let key;
|
||||
|
||||
console.log(`Found ${backupFiles?.length} files to restore from backup`);
|
||||
for (const path of backupFiles) {
|
||||
if (path === ".nnbackup") continue;
|
||||
const filePath = `${zipOutputFolder}/${path}`;
|
||||
const data = await RNFetchBlob.fs.readFile(filePath, "utf8");
|
||||
const parsed = JSON.parse(data);
|
||||
|
||||
if (parsed.encrypted && !password) {
|
||||
console.log("Backup is encrypted...", "requesting password");
|
||||
const { value, isEncryptionKey } = await withPassword();
|
||||
|
||||
if (isEncryptionKey) {
|
||||
key = value;
|
||||
} else {
|
||||
password = value;
|
||||
}
|
||||
if (!password && !key) throw new Error("Failed to decrypt backup");
|
||||
}
|
||||
await db.backup.import(parsed, password, key);
|
||||
console.log("Imported", path);
|
||||
}
|
||||
// Remove files from cache
|
||||
RNFetchBlob.fs.unlink(zipOutputFolder).catch(console.log);
|
||||
if (remove) {
|
||||
RNFetchBlob.fs.unlink(file).catch(console.log);
|
||||
}
|
||||
|
||||
await db.initCollections();
|
||||
initialize();
|
||||
setRestoring(false);
|
||||
close();
|
||||
ToastEvent.show({
|
||||
heading: "Backup restored successfully.",
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
} catch (e) {
|
||||
backupError(e);
|
||||
setRestoring(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} file
|
||||
*/
|
||||
async function restoreFromNNBackup(backup) {
|
||||
try {
|
||||
if (backup.data.iv && backup.data.salt) {
|
||||
const { value, isEncryptionKey } = await withPassword();
|
||||
|
||||
let key;
|
||||
let password;
|
||||
if (isEncryptionKey) {
|
||||
key = value;
|
||||
} else {
|
||||
password = value;
|
||||
}
|
||||
|
||||
if (key || password) {
|
||||
try {
|
||||
await restoreBackup(backup, password, key);
|
||||
close();
|
||||
setRestoring(false);
|
||||
} catch (e) {
|
||||
setRestoring(false);
|
||||
backupError(e);
|
||||
}
|
||||
} else {
|
||||
setRestoring(false);
|
||||
}
|
||||
} else {
|
||||
await restoreBackup(backup);
|
||||
setRestoring(false);
|
||||
close();
|
||||
}
|
||||
} catch (e) {
|
||||
setRestoring(false);
|
||||
backupError(e);
|
||||
}
|
||||
}
|
||||
|
||||
const button = {
|
||||
title: "Restore from files",
|
||||
onPress: async () => {
|
||||
if (restoring) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const file = await DocumentPicker.pickSingle({
|
||||
copyTo: "cachesDirectory"
|
||||
});
|
||||
|
||||
if (file.name.endsWith(".nnbackupz")) {
|
||||
setRestoring(true);
|
||||
await restoreFromZip(file.fileCopyUri, true);
|
||||
} else if (file.name.endsWith(".nnbackup")) {
|
||||
RNFetchBlob.fs.unlink(file.fileCopyUri).catch(console.log);
|
||||
setRestoring(true);
|
||||
const data = await fetch(file.uri);
|
||||
await restoreFromNNBackup(await data.json());
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("error", e.stack);
|
||||
setRestoring(false);
|
||||
backupError(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderItem = ({ item, index }) => (
|
||||
<View
|
||||
style={{
|
||||
@@ -435,6 +271,76 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
|
||||
</View>
|
||||
);
|
||||
|
||||
const restoreBackup = async (backup, password) => {
|
||||
await db.backup.import(backup, password);
|
||||
setRestoring(false);
|
||||
initialize();
|
||||
ToastEvent.show({
|
||||
heading: "Backup restored successfully.",
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
};
|
||||
|
||||
const backupError = (e) => {
|
||||
ToastEvent.show({
|
||||
heading: "Restore failed",
|
||||
message:
|
||||
e.message ||
|
||||
"The selected backup data file is invalid. You must select a *.nnbackup file to restore.",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
};
|
||||
|
||||
const button = {
|
||||
title: "Restore from files",
|
||||
onPress: () => {
|
||||
if (restoring) {
|
||||
return;
|
||||
}
|
||||
|
||||
DocumentPicker.pickSingle()
|
||||
.then((r) => {
|
||||
setRestoring(true);
|
||||
fetch(r.uri)
|
||||
.then(async (r) => {
|
||||
try {
|
||||
let backup = await r.json();
|
||||
if (backup.data.iv && backup.data.salt) {
|
||||
withPassword(
|
||||
async (value) => {
|
||||
try {
|
||||
restoreBackup(backup, value).then(() => {
|
||||
close();
|
||||
setRestoring(false);
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
backupError(e);
|
||||
setRestoring(false);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
() => {
|
||||
setRestoring(false);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
await restoreBackup(backup);
|
||||
close();
|
||||
}
|
||||
} catch (e) {
|
||||
setRestoring(false);
|
||||
backupError(e);
|
||||
}
|
||||
})
|
||||
.catch(console.log);
|
||||
})
|
||||
.catch(console.log);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<View>
|
||||
@@ -471,10 +377,7 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
|
||||
height: 100
|
||||
}}
|
||||
>
|
||||
<ActivityIndicator
|
||||
color={colors.primary.accent}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
<ActivityIndicator color={colors.primary.accent} size={SIZE.lg} />
|
||||
</View>
|
||||
) : (
|
||||
<View
|
||||
@@ -531,9 +434,7 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
|
||||
</Paragraph>
|
||||
</>
|
||||
) : (
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
No backups found
|
||||
</Paragraph>
|
||||
<Paragraph color={colors.secondary.paragraph}>No backups found</Paragraph>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -88,14 +88,10 @@ const Sort = ({ type, screen }) => {
|
||||
? groupOptions.groupBy === "abc" ||
|
||||
groupOptions.sortBy === "title"
|
||||
? "A - Z"
|
||||
: groupOptions.sortBy === "dueDate"
|
||||
? "Earliest first"
|
||||
: "Old - New"
|
||||
: groupOptions.groupBy === "abc" ||
|
||||
groupOptions.sortBy === "title"
|
||||
? "Z - A"
|
||||
: groupOptions.sortBy === "dueDate"
|
||||
? "Latest first"
|
||||
: "New - Old"
|
||||
}
|
||||
icon={
|
||||
@@ -146,7 +142,6 @@ const Sort = ({ type, screen }) => {
|
||||
/>
|
||||
) : (
|
||||
Object.keys(SORT).map((item) =>
|
||||
(item === "dueDate" && screen !== "Reminders") ||
|
||||
(item === "title" && groupOptions.groupBy !== "none") ||
|
||||
((screen !== "Tags" || screen !== "Reminders") &&
|
||||
item === "dateModified") ||
|
||||
|
||||
@@ -16,7 +16,6 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import qclone from "qclone";
|
||||
import React, {
|
||||
createContext,
|
||||
@@ -27,7 +26,7 @@ import React, {
|
||||
useRef,
|
||||
useState
|
||||
} from "react";
|
||||
import { RefreshControl, useWindowDimensions, View } from "react-native";
|
||||
import { RefreshControl, View } from "react-native";
|
||||
import ActionSheet, {
|
||||
ActionSheetRef,
|
||||
FlatList
|
||||
@@ -46,6 +45,7 @@ import {
|
||||
import useNavigationStore, {
|
||||
NotebookScreenParams
|
||||
} from "../../../stores/use-navigation-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import {
|
||||
eOnNewTopicAdded,
|
||||
eOnTopicSheetUpdate,
|
||||
@@ -54,17 +54,18 @@ import {
|
||||
import { normalize, SIZE } from "../../../utils/size";
|
||||
import { GroupHeader, NotebookType, TopicType } from "../../../utils/types";
|
||||
|
||||
import { getTotalNotes } from "@notesnook/common";
|
||||
import { groupArray } from "@notesnook/core/dist/utils/grouping";
|
||||
import { groupArray } from "@notesnook/core/utils/grouping";
|
||||
import Config from "react-native-config";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { MMKV } from "../../../common/database/mmkv";
|
||||
import { openEditor } from "../../../screens/notes/common";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { getTotalNotes } from "@notesnook/common";
|
||||
import { deleteItems } from "../../../utils/functions";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
import { Properties } from "../../properties";
|
||||
import Sort from "../sort";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
|
||||
type ConfigItem = { id: string; type: string };
|
||||
class TopicSheetConfig {
|
||||
@@ -107,8 +108,7 @@ export const TopicsSheet = () => {
|
||||
)
|
||||
: []
|
||||
);
|
||||
const currentItem = useRef<string>();
|
||||
const { fontScale } = useWindowDimensions();
|
||||
|
||||
const [groupOptions, setGroupOptions] = useState(
|
||||
db.settings?.getGroupOptions("topics")
|
||||
);
|
||||
@@ -206,11 +206,6 @@ export const TopicsSheet = () => {
|
||||
if (canShow) {
|
||||
setTimeout(() => {
|
||||
const id = isTopic ? currentScreen?.notebookId : currentScreen?.id;
|
||||
if (currentItem.current !== id) {
|
||||
setSelection([]);
|
||||
setEnabled(false);
|
||||
}
|
||||
currentItem.current = id;
|
||||
const notebook = db.notebooks?.notebook(id as string)?.data;
|
||||
const snapPoint = isTopic
|
||||
? 0
|
||||
@@ -231,8 +226,6 @@ export const TopicsSheet = () => {
|
||||
}
|
||||
}, 300);
|
||||
} else {
|
||||
setSelection([]);
|
||||
setEnabled(false);
|
||||
ref.current?.hide();
|
||||
}
|
||||
}, [
|
||||
@@ -278,7 +271,7 @@ export const TopicsSheet = () => {
|
||||
backgroundColor: colors.secondary.background
|
||||
}}
|
||||
keyboardHandlerEnabled={false}
|
||||
snapPoints={Config.isTesting === "true" ? [100] : [20, 100]}
|
||||
snapPoints={Config.isTesting === "true" ? [100] : [25, 100]}
|
||||
initialSnapIndex={1}
|
||||
backgroundInteractionEnabled
|
||||
gestureEnabled
|
||||
@@ -312,8 +305,8 @@ export const TopicsSheet = () => {
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
maxHeight: 450,
|
||||
height: 450,
|
||||
maxHeight: 300,
|
||||
height: 300,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
@@ -336,19 +329,30 @@ export const TopicsSheet = () => {
|
||||
{enabled ? (
|
||||
<IconButton
|
||||
customStyle={{
|
||||
marginLeft: 10,
|
||||
width: 40 * fontScale,
|
||||
height: 40 * fontScale
|
||||
marginLeft: 10
|
||||
}}
|
||||
onPress={async () => {
|
||||
//@ts-ignore
|
||||
useSelectionStore.setState({
|
||||
selectedItemsList: selection
|
||||
});
|
||||
await deleteItems();
|
||||
useSelectionStore.getState().clearSelection();
|
||||
setEnabled(false);
|
||||
setSelection([]);
|
||||
presentDialog({
|
||||
title: `Delete ${
|
||||
selection.length > 1 ? "topics" : "topics"
|
||||
}`,
|
||||
paragraph: `Are you sure you want to delete ${
|
||||
selection.length > 1 ? "these topics?" : "this topic?"
|
||||
}`,
|
||||
positiveText: "Delete",
|
||||
negativeText: "Cancel",
|
||||
positivePress: async () => {
|
||||
await deleteItems();
|
||||
useSelectionStore.getState().clearSelection();
|
||||
setEnabled(false);
|
||||
setSelection([]);
|
||||
},
|
||||
positiveType: "errorShade"
|
||||
});
|
||||
return;
|
||||
}}
|
||||
color={colors.primary.icon}
|
||||
@@ -374,8 +378,8 @@ export const TopicsSheet = () => {
|
||||
color={colors.primary.icon}
|
||||
size={22}
|
||||
customStyle={{
|
||||
width: 40 * fontScale,
|
||||
height: 40 * fontScale
|
||||
width: 40,
|
||||
height: 40
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
@@ -385,8 +389,8 @@ export const TopicsSheet = () => {
|
||||
color={colors.primary.icon}
|
||||
size={22}
|
||||
customStyle={{
|
||||
width: 40 * fontScale,
|
||||
height: 40 * fontScale
|
||||
width: 40,
|
||||
height: 40
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -404,8 +408,8 @@ export const TopicsSheet = () => {
|
||||
color={colors.primary.icon}
|
||||
size={22}
|
||||
customStyle={{
|
||||
width: 40 * fontScale,
|
||||
height: 40 * fontScale
|
||||
width: 40,
|
||||
height: 40
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
@@ -479,7 +483,6 @@ const TopicItem = ({
|
||||
selection.selection.findIndex((selected) => selected.id === item.id) > -1;
|
||||
const isFocused = screen.id === item.id;
|
||||
const notesCount = getTotalNotes(item);
|
||||
const { fontScale } = useWindowDimensions();
|
||||
|
||||
return (
|
||||
<PressableButton
|
||||
@@ -516,10 +519,6 @@ const TopicItem = ({
|
||||
<IconButton
|
||||
size={SIZE.lg}
|
||||
color={isSelected ? colors.selected.icon : colors.primary.icon}
|
||||
top={0}
|
||||
left={0}
|
||||
bottom={0}
|
||||
right={0}
|
||||
name={
|
||||
isSelected
|
||||
? "check-circle-outline"
|
||||
@@ -539,8 +538,8 @@ const TopicItem = ({
|
||||
<IconButton
|
||||
name="dots-horizontal"
|
||||
customStyle={{
|
||||
width: 40 * fontScale,
|
||||
height: 40 * fontScale
|
||||
width: 40,
|
||||
height: 40
|
||||
}}
|
||||
testID={notesnook.ids.notebook.menu}
|
||||
onPress={() => {
|
||||
|
||||
@@ -28,8 +28,7 @@ import { useNoteStore } from "../../stores/use-notes-store";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { MenuItemsList } from "../../utils/menu-items";
|
||||
import { SUBSCRIPTION_STATUS } from "../../utils/constants";
|
||||
import { MenuItemsList, SUBSCRIPTION_STATUS } from "../../utils/constants";
|
||||
import { eOpenPremiumDialog } from "../../utils/events";
|
||||
import { ColorSection } from "./color-section";
|
||||
import { MenuItem } from "./menu-item";
|
||||
|
||||
@@ -17,18 +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, { useCallback, useEffect, useState } from "react";
|
||||
import { Dimensions, View } from "react-native";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import ToggleSwitch from "toggle-switch-react-native";
|
||||
import Navigation from "../../services/navigation";
|
||||
import useNavigationStore from "../../stores/use-navigation-store";
|
||||
import { SIZE, normalize } from "../../utils/size";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { normalize, SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import { PressableButton } from "../ui/pressable";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { useCallback } from "react";
|
||||
import Tag from "../ui/tag";
|
||||
|
||||
export const MenuItem = React.memo(
|
||||
function MenuItem({ item, index, testID, rightBtn }) {
|
||||
@@ -38,7 +40,6 @@ export const MenuItem = React.memo(
|
||||
);
|
||||
const screenId = item.name.toLowerCase() + "_navigation";
|
||||
let isFocused = headerTextState?.id === screenId;
|
||||
const primaryColors = isFocused ? colors.selected : colors.primary;
|
||||
|
||||
const _onPress = () => {
|
||||
if (item.func) {
|
||||
@@ -109,7 +110,6 @@ export const MenuItem = React.memo(
|
||||
textAlignVertical: "center",
|
||||
textAlign: "left"
|
||||
}}
|
||||
allowFontScaling
|
||||
name={item.icon}
|
||||
color={
|
||||
item.icon === "crown"
|
||||
@@ -128,28 +128,14 @@ export const MenuItem = React.memo(
|
||||
<Paragraph size={SIZE.md}>{item.name}</Paragraph>
|
||||
)}
|
||||
|
||||
{item.isBeta ? (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
backgroundColor: primaryColors.accent,
|
||||
paddingHorizontal: 4,
|
||||
marginLeft: 5,
|
||||
paddingVertical: 2
|
||||
}}
|
||||
>
|
||||
<Paragraph color={primaryColors.accentForeground} size={SIZE.xxs}>
|
||||
BETA
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null}
|
||||
<Tag visible={item.isBeta} text="BETA" />
|
||||
</View>
|
||||
|
||||
{item.switch ? (
|
||||
<ToggleSwitch
|
||||
isOn={item.on}
|
||||
onColor={primaryColors.accent}
|
||||
offColor={primaryColors.icon}
|
||||
onColor={colors.primary.accent}
|
||||
offColor={colors.primary.icon}
|
||||
size="small"
|
||||
animationSpeed={150}
|
||||
onToggle={_onPress}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user