Compare commits

..

1 Commits

Author SHA1 Message Date
ammarahm-ed
33509ca25e mobile: add oss licenses 2023-02-27 16:01:31 +05:00
733 changed files with 114832 additions and 56083 deletions

View File

@@ -20,7 +20,6 @@ const SCOPES = [
"logger",
"theme",
"core",
"fs",
"clipper",
"config",
"ci",

View File

@@ -21,7 +21,4 @@ packages/editor/styles/
packages/editor/languages/
# editor mobile
packages/editor-mobile/build.bundle
# snapshots
tap-snapshots
packages/editor-mobile/build.bundle

View File

@@ -1,27 +0,0 @@
name: 'Setup node with cache action'
description: 'Setups node with cache'
runs:
using: "composite"
steps:
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: "npm"
cache-dependency-path: |
package-lock.json
apps/mobile/package-lock.json
apps/desktop/package-lock.json
apps/web/package-lock.json
extensions/web-clipper/package-lock.json
packages/core/package-lock.json
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
packages/streamable-fs/package-lock.json
packages/theme/package-lock.json

4
.github/dco.yml vendored
View File

@@ -1,4 +0,0 @@
allowRemediationCommits:
individual: true
require:
members: false

View File

@@ -11,8 +11,21 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- uses: actions/setup-node@master
with:
node-version: '16'
cache: 'npm'
cache-dependency-path: |
apps/mobile/package-lock.json
apps/web/package-lock.json
packages/core/package-lock.json
packages/crypto/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
packages/streamable-fs/package-lock.json
packages/theme/package-lock.json
- name: Use specific Java version for the builds
uses: joschi/setup-jdk@v2

View File

@@ -11,8 +11,21 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- uses: actions/setup-node@master
with:
node-version: '16'
cache: 'npm'
cache-dependency-path: |
apps/mobile/package-lock.json
apps/web/package-lock.json
packages/core/package-lock.json
packages/crypto/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
packages/streamable-fs/package-lock.json
packages/theme/package-lock.json
- name: Use specific Java version for the builds
uses: joschi/setup-jdk@v2
@@ -37,18 +50,7 @@ jobs:
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.ALIAS }}
keyStorePassword: ${{ secrets.KEY_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
- name: Publish to Playstore
id: deploy
uses: r0adkll/upload-google-play@v1.1.1
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName: com.streetwriters.notesnook
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
track: production
status: completed
whatsNewDirectory: apps/mobile/native/android/releasenotes/
keyPassword: ${{ secrets.KEY_PASSWORD }}
- name: Build apks for Github release
run: yarn release:android
@@ -89,4 +91,15 @@ jobs:
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-arm64-v8a.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-armeabi-v7a.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
- name: Publish to Playstore
id: deploy
uses: r0adkll/upload-google-play@v1.1.1
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName: com.streetwriters.notesnook
releaseFile: ${{steps.sign_app.outputs.signedReleaseFile}}
track: production
status: completed
whatsNewDirectory: apps/mobile/native/android/releasenotes/

View File

@@ -24,7 +24,21 @@ jobs:
- uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: "npm"
cache-dependency-path: |
apps/mobile/package-lock.json
apps/web/package-lock.json
packages/core/package-lock.json
packages/crypto/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
packages/streamable-fs/package-lock.json
packages/theme/package-lock.json
- name: Prepare environment
run: |

View File

@@ -28,7 +28,21 @@ jobs:
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: "npm"
cache-dependency-path: |
apps/mobile/package-lock.json
apps/web/package-lock.json
packages/core/package-lock.json
packages/crypto/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
packages/streamable-fs/package-lock.json
packages/theme/package-lock.json
- name: Install packages
run: |
@@ -50,7 +64,7 @@ jobs:
- name: Collect app metadata
id: app_metadata
working-directory: ./apps/desktop
working-directory: ./apps/web/desktop
run: |
echo ::set-output name=app_version::$(cat package.json | jq -r .version)
@@ -77,9 +91,6 @@ jobs:
- name: Check out Git repository
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Setup notarization
run: |
mkdir -p ~/private_keys/
@@ -87,7 +98,7 @@ jobs:
- name: Collect app metadata
id: app_metadata
working-directory: ./apps/desktop
working-directory: ./apps/web/desktop
run: |
echo ::set-output name=apple_app_id::$(cat package.json | jq -r .appAppleId)
echo ::set-output name=app_bundle_id::$(cat package.json | jq -r .build.appId)
@@ -104,20 +115,26 @@ jobs:
private-key-raw: ${{ secrets.api_key }}
platform: "MAC_OS"
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 16.x
- name: Download build
uses: actions/download-artifact@v3
with:
name: build
path: ./apps/desktop/build
path: ./apps/web/desktop/build
- name: Install packages
run: |
npm i --ignore-scripts --prefer-offline --no-audit
npx lerna bootstrap --ignore=@notesnook/mobile -- --prefer-offline --no-audit
npm i
npm i dmg-license
working-directory: ./apps/web/desktop
- name: Install provisioning profile
run: echo "${{ secrets.MAC_PROVISIONING_PROFILE }}" | base64 --decode > embedded.provisionprofile
working-directory: ./apps/desktop
working-directory: ./apps/web/desktop
- name: Build app for Mac App Store
if: inputs.publish-apple && steps.appstore.outputs.app-version-latest != steps.app_metadata.outputs.app_version
@@ -125,9 +142,9 @@ jobs:
CSC_LINK: ${{ secrets.mac_certs }}
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
run: |
npx nx run release --project @notesnook/desktop -- --variant=mas
npm run builder -- --mac mas --universal -p never
working-directory: ./apps/desktop
npm run build:mas
npx electron-builder --mac mas --universal -p never
working-directory: ./apps/web/desktop
- name: Build zip and dmg
env:
@@ -138,13 +155,13 @@ jobs:
APPLE_API_KEY_ID: ${{ secrets.api_key_id }}
APPLE_API_KEY_ISSUER: ${{ secrets.api_key_issuer_id }}
run: |
npx nx run release --project @notesnook/desktop
npm run build
if [ ${{ inputs.publish-github }} == true ]; then
npm run builder -- --mac zip dmg --arm64 --x64 -p always
npx electron-builder --mac zip dmg --arm64 --x64 -p always
else
npm run builder -- --mac zip dmg --arm64 --x64 -p never
npx electron-builder --mac zip dmg --arm64 --x64 -p never
fi
working-directory: ./apps/desktop
working-directory: ./apps/web/desktop
- name: Deploy to Testflight
if: inputs.publish-apple && steps.appstore.outputs.app-version-latest != steps.app_metadata.outputs.app_version
@@ -152,12 +169,12 @@ jobs:
API_KEY_ID: ${{ secrets.api_key_id }}
API_KEY_ISSUER_ID: ${{ secrets.api_key_issuer_id }}
run: |
package=$(find ./output/mas-universal/notesnook*.pkg)
package=$(find ./dist/mas-universal/notesnook*.pkg)
echo "Uploading ${package} using altool..."
xcrun altool --upload-package $package -t osx --apiKey $API_KEY_ID --apiIssuer $API_KEY_ISSUER_ID --apple-id ${{ steps.app_metadata.outputs.apple_app_id }} --bundle-id ${{ steps.app_metadata.outputs.app_bundle_id }} --bundle-short-version-string ${{ steps.app_metadata.outputs.app_version }} --bundle-version ${{ steps.app_metadata.outputs.bundle_version }}
working-directory: ./apps/desktop
working-directory: ./apps/web/desktop
build-linux:
name: Build for Linux
@@ -168,9 +185,6 @@ jobs:
- name: Check out Git repository
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Setup Snapcraft Auth
if: inputs.publish-snap
run: echo "SNAPCRAFT_STORE_CREDENTIALS=${{ secrets.snapcraft_token }}" >> $GITHUB_ENV
@@ -178,44 +192,48 @@ jobs:
- name: Install Snapcraft
uses: samuelmeuli/action-snapcraft@v1
if: inputs.publish-snap
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 16.x
- name: Download build
uses: actions/download-artifact@v3
with:
name: build
path: ./apps/desktop/build
path: ./apps/web/desktop/build
- name: Install packages
run: |
npm i --ignore-scripts --prefer-offline --no-audit
npx lerna bootstrap --ignore=@notesnook/mobile -- --prefer-offline --no-audit
run: npm i
working-directory: ./apps/web/desktop
- name: Build Electron wrapper
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
run: npm run build
working-directory: ./apps/web/desktop
- name: Build snap
if: inputs.publish-snap
run: |
npm run builder -- --linux snap:x64 -p never
working-directory: ./apps/desktop
npx electron-builder --linux snap -p never
working-directory: ./apps/web/desktop
- name: Build AppImage
- name: Build AppImage deb and rpm
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ ${{ inputs.publish-github }} == true ]; then
npm run builder -- --linux AppImage:x64 AppImage:arm64 -p always
npx electron-builder --linux AppImage deb rpm -p always
else
npm run builder -- --linux AppImage:x64 AppImage:arm64 -p never
npx electron-builder --linux AppImage deb rpm -p never
fi
working-directory: ./apps/desktop
working-directory: ./apps/web/desktop
- name: Publish on Snapcraft
if: inputs.publish-snap
run: |
snapcraft upload --release=stable ./output/notesnook_linux_amd64.snap
working-directory: ./apps/desktop
snapcraft upload --release=stable ./dist/notesnook_linux_amd64.snap
working-directory: ./apps/web/desktop
build-windows:
name: Build for Windows
@@ -227,30 +245,31 @@ jobs:
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
uses: actions/setup-node@v3
with:
node-version: 16.x
- name: Download build
uses: actions/download-artifact@v3
with:
name: build
path: ./apps/desktop/build
path: ./apps/web/desktop/build
- name: Install packages
run: |
npm i --ignore-scripts --prefer-offline --no-audit
npx lerna bootstrap --ignore=@notesnook/mobile -- --prefer-offline --no-audit
run: npm i
working-directory: ./apps/web/desktop
- name: Build Electron wrapper
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
run: npm run build
working-directory: ./apps/web/desktop
- name: Build app
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if ($${{ inputs.publish-github }} -eq $true) {
yarn electron-builder --win --publish always
npx electron-builder --win --publish always
} else {
yarn electron-builder --win --publish never
npx electron-builder --win --publish never
}
working-directory: ./apps/desktop
working-directory: ./apps/web/desktop

View File

@@ -1,43 +0,0 @@
name: Test @notesnook/editor
on:
workflow_dispatch:
push:
branches:
- "master"
paths:
- "packages/editor/**"
# re-run workflow if workflow file changes
- ".github/workflows/editor.tests.yml"
pull_request:
types:
- "ready_for_review"
- "opened"
- "synchronize"
- "reopened"
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 i --ignore-scripts --prefer-offline --no-audit
npx lerna bootstrap --ignore=@notesnook/mobile -- --prefer-offline --no-audit
- name: Setup environment
run: |
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
- name: Build editor
run: npx nx build @notesnook/editor
- name: Run all @notesnook/editor tests
run: npx nx test @notesnook/editor

View File

@@ -11,7 +11,21 @@ jobs:
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: "npm"
cache-dependency-path: |
apps/mobile/package-lock.json
apps/web/package-lock.json
packages/core/package-lock.json
packages/crypto/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
packages/streamable-fs/package-lock.json
packages/theme/package-lock.json
- name: Install packages
run: |

View File

@@ -25,7 +25,21 @@ jobs:
uses: actions/checkout@v3
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: "npm"
cache-dependency-path: |
apps/mobile/package-lock.json
apps/web/package-lock.json
packages/core/package-lock.json
packages/crypto/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
packages/streamable-fs/package-lock.json
packages/theme/package-lock.json
- name: Install packages
run: |
@@ -62,7 +76,21 @@ jobs:
path: ./apps/web/build
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: "npm"
cache-dependency-path: |
apps/mobile/package-lock.json
apps/web/package-lock.json
packages/core/package-lock.json
packages/crypto/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
packages/streamable-fs/package-lock.json
packages/theme/package-lock.json
- name: Install packages
run: |

3
.gitignore vendored
View File

@@ -6,5 +6,4 @@ dist
nx-cloud.env
.idea
.eslintcache
.env.local
.nyc_output
.env.local

View File

@@ -8,9 +8,9 @@ Thank you so much for considering to contribute to Notesnook! If you have no ide
Ugh! Bugs!
> A bug is when software behaves in a way you didn't expect, which the developer didn't intend.
> A bug is when software behaves in a way that you didn't expect, and the developer didn't intend.
To help us understand what's happening, we first want to **make sure you're using the latest version of Notesnook**.
To help us understand what's going on, we first want to **make sure you're using the latest version of Notesnook**.
Once you've **confirmed that the bug still exists in the latest version**, you'll want to check to make sure it's not something we already know about in the [opened GitHub issues](https://github.com/streetwriters/notesnook/issues).
@@ -30,7 +30,7 @@ Before you open a new feature request, please make sure it's not a duplicate. **
### Helping out in the issue tracker
New issues are always opened that need to be triaged, sorted & organized, so the developers can easily find the most critical and/or relevant bugs to fix. Any help in this regard is appreciated.
There are always new issues getting opened that need to be triaged, sorted & organized, so the developers can easily find the most critical and/or relevant bugs to fix. Any help in this regard is appreciated.
In addition to this, you can help out in the following ways:
@@ -62,7 +62,7 @@ Once you are done, [open a new pull request](https://docs.github.com/en/pull-req
1. Fork [the repository](https://github.com/streetwriters/notesnook) and create your branch from `master` (you can name your branch anything).
2. Run `npm run bootstrap` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
3. If youve fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`npm run test`).
5. Format your code with prettier (`npm run prettier`).
6. Make sure your code lints (`npm run lint`). Tip: `npm run linc` to only check changed files.
@@ -131,11 +131,11 @@ We use an automatic code formatter called [Prettier](https://prettier.io/). Run
Then, our linter will catch most issues that may exist in your code. You can check the status of your code styling by simply running `npm run lint`.
However, there are still some styles that the linter cannot pick up. If you are unsure about something, looking at [Airbnb's Style Guide](https://github.com/airbnb/javascript) will guide you in the right direction.
However, there are still some styles that the linter cannot pick up. If you are unsure about something, looking at [Airbnbs Style Guide](https://github.com/airbnb/javascript) will guide you in the right direction.
## Git Branch Organization
Submit all changes directly to the [`master branch`](https://github.com/streetwriters/notesnook). We don't use separate branches for development or for upcoming releases. This requires us to always keep the `master` branch in a deployable state which means:
Submit all changes directly to the [`master branch`](https://github.com/streetwriters/notesnook). We dont use separate branches for development or for upcoming releases. This requires us to always keep the `master` branch in a deployable state which means:
1. All tests must be passing at all times
2. There should be as few breaking changes as possible

View File

@@ -10,11 +10,11 @@
## Overview
Notesnook is a free (as in speech) & open-source note-taking app focused on user privacy & ease of use. To ensure zero knowledge principles, Notesnook encrypts everything on your device using `XChaCha20-Poly1305` & `Argon2`.
Notesnook is a free (as in speech) & open source note taking app focused on user privacy & ease of use. To ensure zero knowledge principles, Notesnook encrypts everything on your device using `XChaCha20-Poly1305` & `Argon2`.
Notesnook is our **proof** that privacy does _not_ (always) have to come at the cost of convenience. We aim to provide users peace of mind & 100% confidence that their notes are safe and secure. The decision to go fully open source is one of the most crucial steps towards that.
Notesnook is our **proof** that privacy does _not_ (always) have to come at the cost of convenience. Our goal is to provide users peace of mind & 100% confidence that their notes are safe and secure. The decision to go fully open source is one of the most crucial steps towards that.
This repository contains all the code required to build & use the Notesnook web, desktop & mobile clients. If you are looking for a full feature list or screenshots, please check the [website](https://notesnook.com/).
This repository contains all the code required to build & use the Notesnook web, desktop & mobile clients. If you are looking for a full features list or screenshots, please check the [website](https://notesnook.com/).
## Developer guide
@@ -25,30 +25,25 @@ Notesnook is built using the following technologies:
1. JavaScript/Typescript — this repo is in a hybrid state. A lot of the newer code is being written in Typescript & the old code is slowly being ported over.
2. React — the whole front-end across all platforms is built using React.
3. React Native — For mobile apps we are using React Native
4. Electron — For desktop app
5. NPM — listed here because we **don't** use Yarn or PNPM or XYZ across any of our projects.
6. Lerna & Nx — maintaining monorepos is hard but Nx makes it easier.
4. NPM — listed here because we **don't** use Yarn or PNPM or XYZ across any of our projects.
5. Lerna & Nx — maintaining monorepos is hard but Nx makes it easier.
> **Note: Each project in the monorepo contains its own architecture details which you can refer to.**
### Monorepo structure
| Name | Path | Description |
| -------------------------- | -------------------------------------------------- | -------------------------------------------------------------------- |
| `@notesnook/web` | [/apps/web](/apps/web) | Web client |
| `@notesnook/desktop` | [/apps/desktop](/apps/desktop) | Desktop client |
| `@notesnook/mobile` | [/apps/mobile](/apps/mobile) | Android/iOS clients |
| `@notesnook/web-clipper` | [/extensions/web-clipper](/extensions/web-clipper) | Web clipper |
| `@notesnook/core` | [/packages/core](/packages/core) | Shared core between all platforms |
| `@notesnook/crypto` | [/packages/crypto](/packages/crypto) | Cryptography library wrapper around libsodium |
| `@notesnook/clipper` | [/packages/clipper](/packages/clipper) | Web clipper core handling everything related to actual page clipping |
| `@notesnook/editor` | [/packages/editor](/packages/editor) | Notesnook editor + all extensions |
| `@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 |
| Name | Path | Description |
| -------------------------- | -------------------------------------------------- | ----------------------------------------------------------------- |
| `@notesnook/web` | [/apps/web](/apps/web) | Web/Desktop clients |
| `@notesnook/mobile` | [/apps/mobile](/apps/mobile) | Android/iOS clients |
| `@notesnook/core` | [/packages/core](/packages/core) | Shared core between all platforms |
| `@notesnook/crypto` | [/packages/crypto](/packages/crypto) | Cryptography library wrapper around libsodium |
| `@notesnook/editor` | [/packages/editor](/packages/editor) | Notesnook editor + all extensions |
| `@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/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 |
### Contributing guidelines

View File

@@ -1,4 +0,0 @@
node_modules
build
output
dist

View File

@@ -1,58 +0,0 @@
<p align="center">
<img style="align:center;" src="/resources/screenshots/web.jpg" alt="Notesnook desktop screenshot" width="600" />
</p>
<h1 align="center">Notesnook Desktop</h1>
<h3 align="center">The desktop app is built using Electron & Typescript.</h3>
<p align="center">
<a href="https://notesnook.com/downloads">Downloads</a> | <a href="#developer-guide">Developer guide</a> | <a href="#build-instructions">How to build?</a>
</p>
## Getting started
## Build instructions
> **Before you start, it is recommended that you read [the contributing guidelines](/CONTRIBUTING.md).**
### Setting up the development environment
Requirements:
1. [Node.js](https://nodejs.org/en/download/)
2. [git](https://git-scm.com/downloads)
3. NPM (not yarn or pnpm)
Before you can do anything, you'll need to [install Node.js](https://nodejs.org/en/download/) v16 or later on your system.
Once you have completed the setup, the first step is to `clone` the monorepo:
```bash
git clone https://github.com/streetwriters/notesnook.git
# change directory
cd notesnook
```
Once you are inside the `./notesnook` directory, run the preparation step:
```bash
# this might take a while to complete
npm install
```
Now you can finally start the desktop app:
```bash
npm run start:desktop
```
## Developer guide
### The tech stack
We try to keep the stack as lean as possible:
1. Electron
2. tRPC: for cross-communication between the web & desktop parts
3. zod: for runtime typechecking
4. yargs: for CLI argument parsing

View File

@@ -1,52 +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/>.
*/
/* eslint-disable no-var */
import { BrowserWindow } from "electron";
import * as undici_types from "undici";
declare global {
export const {
fetch,
FormData,
Headers,
Request,
Response
}: typeof import("undici");
type FormData = undici_types.FormData;
type Headers = undici_types.Headers;
type HeadersInit = undici_types.HeadersInit;
type BodyInit = undici_types.BodyInit;
type Request = undici_types.Request;
type RequestInit = undici_types.RequestInit;
type RequestInfo = undici_types.RequestInfo;
type RequestMode = undici_types.RequestMode;
type RequestRedirect = undici_types.RequestRedirect;
type RequestCredentials = undici_types.RequestCredentials;
type RequestDestination = undici_types.RequestDestination;
type ReferrerPolicy = undici_types.ReferrerPolicy;
type Response = undici_types.Response;
type ResponseInit = undici_types.ResponseInit;
type ResponseType = undici_types.ResponseType;
var window: BrowserWindow | null;
var RELEASE: boolean;
var MAC_APP_STORE: boolean;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,72 +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 path from "path";
import fs from "fs/promises";
import { $, argv, os } from "zx";
$.env = process.env;
const sodiumNativePrebuildPath = path.join(
`node_modules`,
`@notesnook`,
`crypto`,
`node_modules`,
`@notesnook`,
`sodium`,
`node_modules`,
`sodium-native`,
`prebuilds`,
`${os.platform()}-x64`
);
if (argv.rebuild) {
await fs.rm("./build/", { force: true, recursive: true });
await $`cd ../web/ && npm run build:desktop`;
await fs.cp(path.join("..", "web", "build"), "build", {
recursive: true,
force: true
});
}
if (argv.variant === "mas") {
await $`npm run bundle:mas`;
} else {
await $`npm run bundle`;
}
await $`tsc`;
await $`npm rebuild`;
await fs.cp(
sodiumNativePrebuildPath,
path.join("build", "prebuilds", `${process.platform}-x64`),
{
recursive: true,
force: true
}
);
if (argv.run) {
await $`npm run builder -- --linux AppImage:x64`;
await $`./output/notesnook_linux_x86_64.AppImage`;
}

View File

@@ -1,117 +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 path from "path";
import fs from "fs/promises";
import { watch } from "turbowatch";
import { $, within } from "zx";
const sodiumNativePrebuildPath = path.join(
`node_modules`,
`@notesnook`,
`crypto`,
`node_modules`,
`@notesnook`,
`sodium`,
`node_modules`,
`sodium-native`,
`prebuilds`
);
let isServerRunning = false;
async function main() {
const { shutdown } = await watch({
project: path.join(__dirname, ".."),
cwd: path.join(__dirname, ".."),
triggers: [
{
expression: [
"allof",
["not", ["dirname", "node_modules"]],
["match", "*.ts", "basename"]
],
name: "dev",
// retry: { retries: 0 },
onTeardown: async () => {
await fs.rm("./build/", { force: true, recursive: true });
},
onChange: async ({ spawn: $, first, log }) => {
if (first) {
await fs.rm("./build/", { force: true, recursive: true });
}
await $`npm run build:electron`;
await $`tsc`;
if (first) {
await fs.cp(sodiumNativePrebuildPath, "build/prebuilds", {
recursive: true,
force: true
});
}
if (!isServerRunning) {
await spawnAndWaitUntil(
path.join(__dirname, "..", "..", "web"),
"npm run start:desktop",
(data) => data.includes("Compiled successfully!")
);
isServerRunning = true;
}
log("Starting desktop app!");
await $`npx electron ${path.join("build", "electron.js")}`;
}
}
]
});
// SIGINT is the signal sent when we press Ctrl+C
process.once("SIGINT", () => {
void shutdown();
});
}
main();
function spawnAndWaitUntil(
cwd: string,
cmd: string,
predicate: (data: string) => boolean
) {
return new Promise((resolve) => {
within(async () => {
$.env = process.env;
$.quote = (c) => c;
try {
const s = $`cd ${cwd} && ${cmd}`;
s.stdout.on("data", (data) => {
if (predicate(data)) resolve(undefined); //
});
await s;
} catch (e) {
//ignore
} finally {
isServerRunning = false;
}
});
});
}

View File

@@ -1,58 +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 { initTRPC } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { EventEmitter } from "events";
import TypedEventEmitter from "typed-emitter";
export type AppEvents = {
onCreateItem(name: "note" | "notebook" | "reminder"): void;
};
const emitter = new EventEmitter();
const typedEmitter = emitter as TypedEventEmitter<AppEvents>;
const t = initTRPC.create();
export const bridgeRouter = t.router({
onCreateItem: createSubscription("onCreateItem")
});
export const bridge: AppEvents = new Proxy({} as AppEvents, {
get(_t, name) {
if (typeof name === "symbol") return;
return (...args: unknown[]) => {
emitter.emit(name, ...args);
};
}
});
function createSubscription<TName extends keyof AppEvents>(eventName: TName) {
return t.procedure.subscription(() => {
return observable<Parameters<AppEvents[TName]>[0]>((emit) => {
const listener: AppEvents[TName] = (...args: any[]) => {
emit.next(args[0]);
};
typedEmitter.addListener(eventName, listener);
return () => {
typedEmitter.removeListener(eventName, listener);
};
});
});
}

View File

@@ -1,39 +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 zlib from "node:zlib";
import utils from "node:util";
import { initTRPC } from "@trpc/server";
import { z } from "zod";
const t = initTRPC.create();
const gzipAsync = utils.promisify(zlib.gzip);
const gunzipAsync = utils.promisify(zlib.gunzip);
export const compressionRouter = t.router({
gzip: t.procedure
.input(z.object({ data: z.string(), level: z.number() }))
.query(async ({ input }) => {
const { data, level } = input;
return (await gzipAsync(data, { level })).toString("base64");
}),
gunzip: t.procedure.input(z.string()).query(async ({ input }) => {
return (await gunzipAsync(Buffer.from(input, "base64"))).toString("utf-8");
})
});

View File

@@ -1,41 +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 { initTRPC } from "@trpc/server";
import { compressionRouter } from "./compression";
import { osIntegrationRouter } from "./os-integration";
import { spellCheckerRouter } from "./spell-checker";
import { updaterRouter } from "./updater";
import { bridgeRouter } from "./bridge";
const t = initTRPC.create();
export const router = t.router({
compress: compressionRouter,
integration: osIntegrationRouter,
spellChecker: spellCheckerRouter,
updater: updaterRouter,
bridge: bridgeRouter
});
export const api = router.createCaller({});
// Export type router type signature,
// NOT the router itself.
export type AppRouter = typeof router;

View File

@@ -1,160 +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 { initTRPC } from "@trpc/server";
import { z } from "zod";
import { dialog, nativeTheme, Notification, shell } from "electron";
import { AutoLaunch } from "../utils/autolaunch";
import { config, DesktopIntegration } from "../utils/config";
import { bringToFront } from "../utils/bring-to-front";
import { getTheme, setTheme, Theme } from "../utils/theme";
import { mkdirSync, writeFileSync } from "fs";
import { dirname, join } from "path";
import { platform } from "os";
import { resolvePath } from "../utils/resolve-path";
import { observable } from "@trpc/server/observable";
const t = initTRPC.create();
const NotificationOptions = z.object({
title: z.string().optional(),
body: z.string().optional(),
silent: z.boolean().optional(),
timeoutType: z.union([z.literal("default"), z.literal("never")]).optional(),
urgency: z
.union([z.literal("normal"), z.literal("critical"), z.literal("low")])
.optional(),
tag: z.string()
});
export const osIntegrationRouter = t.router({
zoomFactor: t.procedure.query(() => config.zoomFactor),
setZoomFactor: t.procedure.input(z.number()).mutation(({ input: factor }) => {
globalThis.window?.webContents.setZoomFactor(factor);
config.zoomFactor = factor;
}),
privacyMode: t.procedure.query(() => config.privacyMode),
setPrivacyMode: t.procedure
.input(z.boolean())
.mutation(({ input: privacyMode }) => {
if (!globalThis.window || !["win32", "darwin"].includes(process.platform))
return;
globalThis.window.setContentProtection(privacyMode);
if (process.platform === "win32") {
globalThis.window.setThumbnailClip(
privacyMode
? { x: 0, y: 0, width: 1, height: 1 }
: { x: 0, y: 0, width: 0, height: 0 }
);
}
config.privacyMode = privacyMode;
}),
desktopIntegration: t.procedure.query(() => config.desktopSettings),
setDesktopIntegration: t.procedure
.input(DesktopIntegration)
.mutation(({ input: settings }) => {
if (settings.autoStart) {
AutoLaunch.enable(settings.startMinimized);
} else {
AutoLaunch.disable();
}
config.desktopSettings = settings;
}),
selectDirectory: t.procedure
.input(
z.object({
title: z.string().optional(),
buttonLabel: z.string().optional(),
defaultPath: z.string().optional()
})
)
.query(async ({ input }) => {
if (!globalThis.window) return undefined;
const { title, buttonLabel, defaultPath } = input;
const result = await dialog.showOpenDialog(globalThis.window, {
title,
buttonLabel,
properties: ["openDirectory"],
defaultPath: defaultPath && resolvePath(defaultPath)
});
if (result.canceled) return undefined;
return result.filePaths[0];
}),
saveFile: t.procedure
.input(z.object({ data: z.string(), filePath: z.string() }))
.query(({ input }) => {
const { data, filePath } = input;
if (!data || !filePath) return;
const resolvedPath = resolvePath(filePath);
mkdirSync(dirname(resolvedPath), { recursive: true });
writeFileSync(resolvedPath, data);
}),
showNotification: t.procedure
.input(NotificationOptions)
.query(({ input }) => {
const notification = new Notification({
...input,
icon: join(
__dirname,
platform() === "win32" ? "app.ico" : "favicon-72x72.png"
)
});
notification.show();
if (input.urgency === "critical") {
shell.beep();
}
return new Promise((resolve) => {
notification.once("close", () => resolve(undefined));
notification.once("click", () => resolve(input.tag));
});
}),
openPath: t.procedure
.input(z.object({ type: z.literal("path"), link: z.string() }))
.query(({ input }) => {
const { type, link } = input;
if (type === "path") return shell.openPath(resolvePath(link));
}),
bringToFront: t.procedure.query(() => bringToFront()),
changeTheme: t.procedure
.input(Theme)
.mutation(({ input }) => setTheme(input)),
onThemeChanged: t.procedure.subscription(() =>
observable<"dark" | "light">((emit) => {
nativeTheme.on("updated", () => {
if (getTheme() === "system") {
emit.next(nativeTheme.shouldUseDarkColors ? "dark" : "light");
}
});
})
)
});

View File

@@ -1,71 +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 { initTRPC } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { CancellationToken, autoUpdater } from "electron-updater";
import type { AppUpdaterEvents } from "electron-updater/out/AppUpdater";
type UpdateInfo = { version: string };
type Progress = { percent: number };
const t = initTRPC.create();
export const updaterRouter = t.router({
install: t.procedure.query(() => autoUpdater.quitAndInstall()),
download: t.procedure.query(async () => {
const cancellationToken = new CancellationToken();
await autoUpdater.downloadUpdate(cancellationToken);
}),
check: t.procedure.query(async () => {
await autoUpdater.checkForUpdates();
}),
onChecking: createSubscription("checking-for-update"),
onDownloaded: createSubscription<"update-downloaded", UpdateInfo>(
"update-downloaded"
),
onDownloadProgress: createSubscription<"download-progress", Progress>(
"download-progress"
),
onNotAvailable: createSubscription<"update-not-available", UpdateInfo>(
"update-not-available"
),
onAvailable: createSubscription<"update-available", UpdateInfo>(
"update-available"
),
onError: createSubscription("error")
});
function createSubscription<
TName extends keyof AppUpdaterEvents,
TReturnType = Parameters<AppUpdaterEvents[TName]>[0]
>(eventName: TName) {
return t.procedure.subscription(() => {
return observable<TReturnType>((emit) => {
const listener: AppUpdaterEvents[TName] = (...args: any[]) => {
emit.next(args[0]);
};
autoUpdater.addListener(eventName, listener);
return () => {
autoUpdater.removeListener(eventName, listener);
};
});
});
}

View File

@@ -1,44 +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/>.
*/
/* eslint-disable no-var */
import { ELECTRON_TRPC_CHANNEL } from "electron-trpc/main";
import { type RendererGlobalElectronTRPC } from "electron-trpc/src/types";
import { NNCrypto } from "@notesnook/crypto";
import { ipcRenderer } from "electron";
declare global {
var os: string;
var electronTRPC: RendererGlobalElectronTRPC;
var NativeNNCrypto: new () => NNCrypto;
}
console.log("HELLO", process);
process.once("loaded", async () => {
console.log("HELLO!");
const electronTRPC: RendererGlobalElectronTRPC = {
sendMessage: (operation) =>
ipcRenderer.send(ELECTRON_TRPC_CHANNEL, operation),
onMessage: (callback) =>
ipcRenderer.on(ELECTRON_TRPC_CHANNEL, (_event, args) => callback(args))
};
globalThis.electronTRPC = electronTRPC;
});
globalThis.NativeNNCrypto = NNCrypto;
globalThis.os = MAC_APP_STORE ? "mas" : process.platform;

View File

@@ -1,55 +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 { nativeTheme } from "electron";
import { JSONStorage } from "./json-storage";
import { z } from "zod";
export const DesktopIntegration = z.object({
autoStart: z.boolean(),
startMinimized: z.boolean(),
minimizeToSystemTray: z.boolean(),
closeToSystemTray: z.boolean()
});
export type DesktopIntegration = z.infer<typeof DesktopIntegration>;
export const config = {
desktopSettings: <DesktopIntegration>{
autoStart: false,
startMinimized: false,
minimizeToSystemTray: false,
closeToSystemTray: false
},
privacyMode: false,
isSpellCheckerEnabled: false,
zoomFactor: 0,
theme: nativeTheme.themeSource
};
type ConfigKey = keyof typeof config;
for (const key in config) {
const defaultValue = config[<ConfigKey>key];
if (Object.hasOwn(config, key)) {
Object.defineProperty(config, key, {
get: () => JSONStorage.get(key, defaultValue),
set: (value) => JSONStorage.set(key, value)
});
}
}

View File

@@ -1,8 +0,0 @@
{
"extends": "../../tsconfig",
"compilerOptions": {
"outDir": "./dist/",
"lib": ["ESNext"]
},
"include": ["src", "global.d.ts"]
}

View File

@@ -19,9 +19,9 @@
## Build instructions
> **Before you start, it is recommended that you read [the contributing guidelines](/CONTRIBUTING.md).**
> **Before you start it is recommended that you read [the contributing guidelines](/CONTRIBUTING.md).**
### Setting up the development environment
### Setting up development environment
Requirements:
@@ -57,7 +57,7 @@ npm install
### Running the app on Android
[Setup an Android emulator from Android Studio](https://developer.android.com/studio/run/managing-avds) if you haven't already, and then run the following command to start the app in the Emulator:
[Setup an Android emulator from Android Studio](https://developer.android.com/studio/run/managing-avds) if you haven't already and then run the following command to start the app in the Emulator:
```bash
npm run start:android
@@ -78,11 +78,11 @@ npm run start:ios
## Developer guide
> This project is in a transition state between Javascript & Typescript. We are gradually porting everything over to Typescript, so if you can help with that, it'd be great!
> This project is in a transition state between Javascript & Typescript. We are gradually porting everything over to Typescript so if you can help with that, it'd be great!
### The tech stack
We try to keep the stack as lean as possible:
We try to keep the stack as lean as possible
1. React Native
2. Typescript/Javascript
@@ -95,17 +95,17 @@ We try to keep the stack as lean as possible:
The app codebase is distributed over two primary directories. `native/` and `app/`.
- `native/`: Includes `android/` and `ios/` folders and everything related to react native core functionality like bundling, development, and packaging. Any react-native dependency with native code, i.e., android & ios folders, is installed here.
- `native/`: Includes `android/` and `ios/` folders and everything related to react native core functionality like bundling, development and packaging. Any react-native dependency that has native code i.e android & ios folders, is installed here.
- `app/`: Includes all the app code other than the native part. All JS-only dependencies are installed here.
- `components/`: Each component serves a specific purpose in the app UI. For example, the `Paragraph` component is used to render paragraphs in the app, and a `Header` component is used to render a `header` on all screens.
- `common/`: Features that are integral to the app's functionality. For example, the notesnook core is initialized here.
- `app/`: Includes all the app code other than the native part. All JS only dependencies are installed here.
- `components/`: Each component serves a specific purpose in the app UI, for example the `Paragraph` component is used to render paragraphs in the app and a `Header` component is used to render a `header` on all screens.
- `common/`: Features that have integral role in app functionality, for example, notesnook core is initialized here.
- `hooks/`: Hooks for different app logic
- `navigation/`: Includes app navigation-specific code. Here the app navigation, editor & side menu are rendered side by side in fluid tabs.
- `navigation/`: Includes app navigation specific code. Here the app navigation, editor & side menu are rendered side by side in fluid tabs.
- `screens`: Navigator screens.
- `services`: Parts of code that do a specific function. For example, the `sync` service runs Sync from anywhere in the app.
- `stores`: We use `zustand` for global state management in the app. Multiple stores provide the state for different parts of the app.
- `utils`: General purpose stuff such as constant values, utility functions, etc.
- `services`: Parts of code that do a specific function, for example, the `sync` service is responsibe for running Sync from anywhere in the app.
- `stores`: We use `zustand` for global state management in the app. There are multiple stores that provide the state for different parts of the app.
- `utils`: General purpose stuff such as constant values, utility functions etc.
There are several other folders at the root:
@@ -115,11 +115,11 @@ There are several other folders at the root:
### Running the tests
When you are done making the required changes, you must run the tests to ensure you didn't break anything. We use Detox as the testing framework & the tests can be started as follows:
When you are done making the required changes, you will need to run the tests to make sure you didn't break anything. We use Detox as the testing framework & the tests can be started as follows:
### Android
To run the tests on Android, you will need to create an emulator device on your system:
To run the tests on android, you will need to create an emulator device on your system:
```
$ANDROID_HOME/tools/bin/avdmanager create avd -n Pixel_5_API_31 -d pixel --package "system-images;android-31;default;x86_64"
@@ -127,13 +127,13 @@ $ANDROID_HOME/tools/bin/avdmanager create avd -n Pixel_5_API_31 -d pixel --packa
If you face problems, follow the detailed guide in [Detox documentation](https://wix.github.io/Detox/docs/introduction/android-dev-env). Keep the emulator name set to `Pixel_5_API_31`.
Once you have created an emulator device, build the Android apks:
Once you have created an emulator device, build the android apks
```
npm run build:android
```
Finally, run the tests:
Finally run the tests
```
npm run test:android
@@ -141,9 +141,9 @@ npm run test:android
### iOS
To run e2e tests on the iOS simulator, you must be on a Mac with XCode installed.
To run e2e tests on iOS simulator, you must be on a Mac with XCode installed.
First, install [AppleSimulatorUtils](https://github.com/wix/AppleSimulatorUtils):
First install [AppleSimulatorUtils](https://github.com/wix/AppleSimulatorUtils)
```
brew tap wix/brew
@@ -156,7 +156,7 @@ Now build the iOS app for testing:
npm run build:ios
```
Finally, run the tests:
Finally run the tests:
```
npm run test:ios

View File

@@ -31,6 +31,14 @@ import { TipManager } from "./services/tip-manager";
import { useUserStore } from "./stores/use-user-store";
import { View } from "react-native";
import { useState } from "react";
import NetInfo from "@react-native-community/netinfo";
NetInfo.configure({
reachabilityUrl: "https://notesnook.com",
reachabilityTest: (response) => {
if (!response) return false;
return response?.status >= 200 && response?.status < 300;
}
});
SettingsService.init();
SettingsService.checkOrientation();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -22,17 +22,10 @@ import "react-native-get-random-values";
import * as Keychain from "react-native-keychain";
import { generateSecureRandom } from "react-native-securerandom";
import Sodium from "@ammarahmed/react-native-sodium";
import { MMKV } from "./mmkv";
const IOS_KEYCHAIN_ACCESS_GROUP = "group.org.streetwriters.notesnook";
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
const IOS_KEYCHAIN_UPGRAGE_KEY = "keychain-ios:upgraded";
const KEYSTORE_CONFIG = Platform.select({
ios: {
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
accessGroup: IOS_KEYCHAIN_ACCESS_GROUP,
service: IOS_KEYCHAIN_SERVICE_NAME
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY
},
android: {}
});
@@ -46,27 +39,12 @@ export async function deriveCryptoKey(name, data) {
credentials.key,
KEYSTORE_CONFIG
);
MMKV.setBool(IOS_KEYCHAIN_UPGRAGE_KEY, true);
return credentials.key;
} catch (e) {
console.error(e);
}
}
async function upgradeIOSKeychain(username, password) {
if (Platform.OS !== "ios") return;
if (!MMKV.getBool(IOS_KEYCHAIN_UPGRAGE_KEY)) {
await Keychain.setInternetCredentials(
"notesnook",
username,
password,
KEYSTORE_CONFIG
);
console.log("IOS KEYCHAIN MIGRATION COMPLETED!");
MMKV.setBool(IOS_KEYCHAIN_UPGRAGE_KEY, true);
}
}
export async function getCryptoKey(_name) {
try {
if (await Keychain.hasInternetCredentials("notesnook")) {
@@ -74,9 +52,6 @@ export async function getCryptoKey(_name) {
"notesnook",
KEYSTORE_CONFIG
);
// upgrades ios keychain to use accessGroups
// so we have access to keychain in share extension.
await upgradeIOSKeychain(credentials.username, credentials.password);
return credentials.password;
} else {
return null;

View File

@@ -68,3 +68,14 @@ db.host(
ISSUES_HOST: "https://issues.streetwriters.co"
}
);
export async function loadDatabase() {
// if (!DB) {
// let module = await import(/* webpackChunkName: "notes-core" */ 'notes-core/api/index');
// DB = module.default;
// }
// db = new DB(Storage, Platform.OS === 'ios' ? EventSource : AndroidEventSource, filesystem);
// if (DOMParser) {
// await DOMParser.prepare();
// }
}

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import RNFetchBlob from "rn-fetch-blob";
import {
decrypt,
deriveCryptoKey,

View File

@@ -1,50 +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 { Dimensions, Platform } from "react-native";
import ImageResizer from "@bam.tech/react-native-image-resizer";
import RNFetchBlob from "react-native-blob-util";
/**
* Scale down & compress images to screen width
* for loading in editor.
* @returns
*/
export async function compressToBase64(path, type) {
const { width, scale } = Dimensions.get("window");
const response = await ImageResizer.createResizedImage(
path,
width * scale,
9999,
type,
80,
0,
undefined,
true,
{
mode: "contain",
onlyScaleDown: true
}
);
const base64 = await RNFetchBlob.fs.readFile(
Platform.OS === "ios" ? response.uri?.replace("file://", "") : response.uri,
"base64"
);
RNFetchBlob.fs.unlink(path.replace("file://", "")).catch(console.log);
RNFetchBlob.fs.unlink(response.uri.replace("file://", "")).catch(console.log);
return base64;
}

View File

@@ -17,171 +17,19 @@ 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 Sodium from "@ammarahmed/react-native-sodium";
import {
getFileNameWithExtension,
isImage,
isDocument
} from "@notesnook/core/utils/filename";
import React from "react";
import { Platform } from "react-native";
import * as ScopedStorage from "react-native-scoped-storage";
import { subscribe, zip } from "react-native-zip-archive";
import RNFetchBlob from "react-native-blob-util";
import Sodium from "@ammarahmed/react-native-sodium";
import RNFetchBlob from "rn-fetch-blob";
import { ShareComponent } from "../../components/sheets/export-notes/share";
import { ToastEvent, presentSheet } from "../../services/event-manager";
import { presentSheet, ToastEvent } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { db } from "../database";
import Storage from "../database/storage";
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
import { cacheDir } from "./utils";
export const FileDownloadStatus = {
Success: 1,
Fail: 0
};
/**
* Download all user's attachments
* @returns
*/
export function downloadAllAttachments() {
const attachments = db.attachments.all;
return downloadAttachments(attachments);
}
/**
* Downloads provided attachments to a .zip file
* on user's device.
* @param attachments
* @param onProgress
* @returns
*/
export async function downloadAttachments(
attachments,
onProgress,
canceled,
groupId
) {
if (!attachments || !attachments.length) return;
const result = new Map();
let outputFolder;
if (Platform.OS === "android") {
// Ask the user to select a directory to store the file
let file = await ScopedStorage.openDocumentTree(true);
outputFolder = file.uri;
if (!outputFolder) return;
} else {
outputFolder = await Storage.checkAndCreateDir("/downloads/");
}
// Create the folder to zip;
const zipSourceFolder = `${cacheDir}/notesnook-attachments`;
const zipOutputFile =
Platform.OS === "ios"
? `${outputFolder}/notesnook-attachments-${Date.now()}.zip`
: `${cacheDir}/notesnook-attachments.zip`;
if (await RNFetchBlob.fs.exists(zipSourceFolder))
await RNFetchBlob.fs.unlink(zipSourceFolder);
await RNFetchBlob.fs.mkdir(zipSourceFolder);
for (let i = 0; i < attachments.length; i++) {
let attachment = attachments[i];
const hash = attachment.metadata.hash;
try {
if (canceled.current) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
return;
}
onProgress?.(
i + 1 / attachments.length,
`Downloading attachments (${i + 1}/${
attachments.length
})... Please wait`
);
// Download to cache
let uri = await downloadAttachment(hash, false, {
silent: true,
cache: true,
groupId: groupId
});
if (canceled.current) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
return;
}
if (!uri) throw new Error("Failed to download file");
// Move file to the source folder we will zip eventually and rename the file to it's actual name.
const filePath = `${zipSourceFolder}/${attachment.metadata.filename}`;
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, filePath);
result.set(hash, {
filename: attachment.metadata.filename,
status: FileDownloadStatus.Success
});
} catch (e) {
result.set(hash, {
filename: attachment.metadata.filename,
status: FileDownloadStatus.Fail,
reason: e
});
console.log("Error downloading attachment", e);
}
}
if (canceled.current) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
return;
}
if (result?.size) {
let sub;
try {
onProgress?.(0, `Zipping... Please wait`);
// If all goes well, zip the notesnook-attachments folder in cache.
sub = subscribe(({ progress }) => {
onProgress(
progress,
`Saving zip file (${(progress * 100).toFixed(1)}%)... Please wait`
);
});
await zip(zipSourceFolder, zipOutputFile);
sub?.remove();
onProgress(1, `Saving zip file... Please wait`);
if (Platform.OS === "android") {
// Move the zip to user selected directory.
const file = await ScopedStorage.createFile(
outputFolder,
`notesnook-attachments-${Date.now()}.zip`,
"application/zip"
);
await copyFileAsync(`file://${zipOutputFile}`, file.uri);
}
onProgress?.(1, `Done`);
releasePermissions(outputFolder);
} catch (e) {
releasePermissions(outputFolder);
sub?.remove();
console.log("Error zipping attachments", e);
}
// Remove source & zip file from cache.
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
if (Platform.OS === "android") {
RNFetchBlob.fs.unlink(zipOutputFile).catch(console.log);
}
}
return result;
}
export default async function downloadAttachment(
hash,
global = true,
options = {
silent: false,
cache: false,
throwError: false,
groupId: undefined
}
) {
export default async function downloadAttachment(hash, global = true) {
let attachment = db.attachments.attachment(hash);
if (!attachment) {
console.log("attachment not found");
@@ -189,23 +37,16 @@ export default async function downloadAttachment(
}
let folder = {};
if (!options.cache) {
if (Platform.OS === "android") {
folder = await ScopedStorage.openDocumentTree();
if (!folder) return;
} else {
folder.uri = await Storage.checkAndCreateDir("/downloads/");
}
if (Platform.OS === "android") {
folder = await ScopedStorage.openDocumentTree();
if (!folder) return;
} else {
folder.uri = await Storage.checkAndCreateDir("/downloads/");
}
try {
console.log(
"starting download attachment",
attachment.metadata.hash,
options.groupId
);
await db.fs.downloadFile(
options.groupId || attachment.metadata.hash,
attachment.metadata.hash,
attachment.metadata.hash
);
if (
@@ -213,11 +54,6 @@ export default async function downloadAttachment(
)
return;
let filename = getFileNameWithExtension(
attachment.metadata.filename,
attachment.metadata.type
);
let key = await db.attachments.decryptKey(attachment.key);
let info = {
iv: attachment.iv,
@@ -227,51 +63,46 @@ export default async function downloadAttachment(
hash: attachment.metadata.hash,
hashType: attachment.metadata.hashType,
mime: attachment.metadata.type,
fileName: options.cache ? undefined : filename,
uri: options.cache ? undefined : folder.uri,
fileName: attachment.metadata.filename,
uri: folder.uri,
chunkSize: attachment.chunkSize
};
let fileUri = await Sodium.decryptFile(
key,
info,
options.cache ? "cache" : "file"
);
let fileUri = await Sodium.decryptFile(key, info, "file");
ToastEvent.show({
heading: "Download successful",
message: attachment.metadata.filename + " downloaded",
type: "success"
});
if (!options.silent) {
ToastEvent.show({
heading: "Download successful",
message: filename + " downloaded",
type: "success"
});
}
if (
attachment.dateUploaded &&
!isImage(attachment.metadata?.type) &&
!isDocument(attachment.metadata?.type)
) {
if (attachment.dateUploaded) {
RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.metadata.hash}`)
.catch(console.log);
}
if (Platform.OS === "ios" && !options.cache) {
fileUri = folder.uri + `/${filename}`;
}
if (!options.silent) {
presentSheet({
title: "File downloaded",
paragraph: `${filename} saved to ${
Platform.OS === "android"
? "selected path"
: "File Manager/Notesnook/downloads"
}`,
icon: "download",
context: global ? null : attachment.metadata.hash,
component: <ShareComponent uri={fileUri} name={filename} padding={12} />
});
}
if (Platform.OS === "ios") {
fileUri = folder.uri + `/${attachment.metadata.filename}`;
}
console.log("saved file uri: ", fileUri);
presentSheet({
title: "File downloaded",
paragraph: `${attachment.metadata.filename} saved to ${
Platform.OS === "android"
? "selected path"
: "File Manager/Notesnook/downloads"
}`,
icon: "download",
context: global ? null : attachment.metadata.hash,
component: (
<ShareComponent
uri={fileUri}
name={attachment.metadata.filename}
padding={12}
/>
)
});
return fileUri;
} catch (e) {
console.log("download attachment error: ", e);
@@ -281,8 +112,5 @@ export default async function downloadAttachment(
.catch(console.log);
}
useAttachmentStore.getState().remove(attachment.metadata.hash);
if (options.throwError) {
throw e;
}
}
}

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import hosts from "@notesnook/core/utils/constants";
import NetInfo from "@react-native-community/netinfo";
import RNFetchBlob from "react-native-blob-util";
import RNFetchBlob from "rn-fetch-blob";
import { ToastEvent } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { db } from "../database";

View File

@@ -23,16 +23,14 @@ import {
deleteFile,
exists,
readEncrypted,
writeEncryptedBase64,
hashBase64
writeEncrypted
} from "./io";
import { uploadFile } from "./upload";
import { cancelable } from "./utils";
export default {
readEncrypted,
writeEncryptedBase64,
hashBase64,
writeEncrypted,
uploadFile: cancelable(uploadFile),
downloadFile: cancelable(downloadFile),
deleteFile,

View File

@@ -19,54 +19,25 @@ 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 RNFetchBlob from "rn-fetch-blob";
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) {
let path = `${cacheDir}/${filename}`;
try {
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)));
let exists = await RNFetchBlob.fs.exists(path);
if (!exists) {
return false;
}
const attachment = db.attachments.attachment(filename);
const isPng = /(png)/g.test(attachment?.metadata.type);
const isJpeg = /(jpeg|jpg)/g.test(attachment?.metadata.type);
let output = await Sodium.decryptFile(
key,
{
...cipherData,
hash: filename,
appGroupId: IOS_APPGROUPID
hash: filename
},
cipherData.outputType === "base64"
? isPng || isJpeg
? "cache"
: "base64"
: "text"
cipherData.outputType === "base64" ? "base64" : "text"
);
if (cipherData.outputType === "base64" && (isPng || isJpeg)) {
const dCachePath = `${cacheDir}/${output}`;
output = await compressToBase64(
`file://${dCachePath}`,
isPng ? "PNG" : "JPEG"
);
}
return output;
} catch (e) {
RNFetchBlob.fs.unlink(path).catch(console.log);
@@ -75,26 +46,17 @@ export async function readEncrypted(filename, key, cipherData) {
}
}
export async function hashBase64(data) {
const hash = await Sodium.hashFile({
type: "base64",
data,
uri: ""
});
return {
hash: hash,
type: "xxh64"
};
}
export async function writeEncryptedBase64({ data, key }) {
export async function writeEncrypted(filename, { data, type, key }) {
console.log("file input: ", { type, key });
let filepath = cacheDir + `/${getRandomId("imagecache_")}`;
console.log(filepath);
await RNFetchBlob.fs.writeFile(filepath, data, "base64");
let output = await Sodium.encryptFile(key, {
uri: Platform.OS === "ios" ? filepath : "file://" + filepath,
type: "url"
});
RNFetchBlob.fs.unlink(filepath).catch(console.log);
console.log("encrypted file output: ", output);
return {
...output,

View File

@@ -17,13 +17,10 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import RNFetchBlob from "react-native-blob-util";
import RNFetchBlob from "rn-fetch-blob";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { db } from "../database";
import { cacheDir } from "./utils";
import { isImage, isDocument } from "@notesnook/core/utils/filename";
import { Platform } from "react-native";
import { IOS_APPGROUPID } from "../../utils/constants";
export async function uploadFile(filename, data, cancelToken) {
if (!data) return false;
@@ -38,19 +35,9 @@ export async function uploadFile(filename, data, cancelToken) {
if (!res.ok) throw new Error(`${res.status}: Unable to resolve upload url`);
const uploadUrl = await res.text();
if (!uploadUrl) throw new Error("Unable to resolve upload url");
let uploadFilePath = `${cacheDir}/${filename}`;
const iosAppGroup =
Platform.OS === "ios"
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
let exists = await RNFetchBlob.fs.exists(uploadFilePath);
if (!exists && Platform.OS === "ios") {
uploadFilePath = appGroupPath;
}
let request = RNFetchBlob.config({
IOSBackgroundTask: !globalThis["IS_SHARE_EXTENSION"]
IOSBackgroundTask: true
})
.fetch(
"PUT",
@@ -58,7 +45,7 @@ export async function uploadFile(filename, data, cancelToken) {
{
"content-type": ""
},
RNFetchBlob.wrap(uploadFilePath)
RNFetchBlob.wrap(`${cacheDir}/${filename}`)
)
.uploadProgress((sent, total) => {
useAttachmentStore
@@ -76,10 +63,7 @@ export async function uploadFile(filename, data, cancelToken) {
if (result) {
let attachment = db.attachments.attachment(filename);
if (!attachment) return result;
if (
!isImage(attachment.metadata.type) &&
!isDocument(attachment.metadata?.type)
) {
if (!attachment.metadata.type.startsWith("image/")) {
RNFetchBlob.fs.unlink(`${cacheDir}/${filename}`).catch(console.log);
}
}

View File

@@ -17,9 +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 * as ScopedStorage from "react-native-scoped-storage";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import RNFetchBlob from "rn-fetch-blob";
export const cacheDir = RNFetchBlob.fs.dirs.CacheDir;
@@ -64,22 +62,3 @@ export function cancelable(operation) {
};
};
}
export function copyFileAsync(source, dest) {
return new Promise((resolve) => {
ScopedStorage.copyFile(source, dest, (e, r) => {
console.log(e, r);
resolve();
});
});
}
export async function releasePermissions(path) {
if (Platform.OS === "ios") return;
const uris = await ScopedStorage.getPersistedUriPermissions();
for (let uri of uris) {
if (path.startsWith(uri)) {
await ScopedStorage.releasePersistableUriPermission(uri);
}
}
}

View File

@@ -148,7 +148,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
<Button
key={item.title}
title={item.title}
fontSize={SIZE.xs}
fontSize={SIZE.xs + 1}
type="gray"
onPress={() => onPress(item)}
width={null}

View File

@@ -67,7 +67,7 @@ export const Title = ({ text, style = {}, inline }) => {
right: 0
}}
iconSize={24}
fontSize={SIZE.xs}
fontSize={SIZE.xs + 1}
style={{
borderRadius: 100,
paddingVertical: 0,

View File

@@ -102,21 +102,15 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
if (res.failed) {
db.attachments.markAsFailed(attachment.id, res.failed);
setFailed(res.failed);
ToastEvent.show({
heading: "File check failed with error: " + res.failed,
type: "error",
context: "local"
});
} else {
setFailed(null);
db.attachments.markAsFailed(attachment.id, null);
ToastEvent.show({
heading: "File check passed",
type: "success",
context: "local"
});
}
ToastEvent.show({
heading: "File check passed",
type: "success",
context: "local"
});
setAttachments([...db.attachments.all]);
setLoading({
name: null
@@ -215,7 +209,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
}}
>
<Paragraph
size={SIZE.xs}
size={SIZE.xs + 1}
style={{
marginRight: 10
}}
@@ -227,7 +221,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
style={{
marginRight: 10
}}
size={SIZE.xs}
size={SIZE.xs + 1}
color={colors.icon}
>
{formatBytes(attachment.length)}
@@ -238,7 +232,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
style={{
marginRight: 10
}}
size={SIZE.xs}
size={SIZE.xs + 1}
color={colors.icon}
>
{attachment.noteIds.length} note
@@ -254,7 +248,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
context: "local"
});
}}
size={SIZE.xs}
size={SIZE.xs + 1}
color={colors.icon}
>
{attachment.metadata.hash}
@@ -310,7 +304,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
}}
key={item.id}
>
<Paragraph size={SIZE.xs}>{item.title}</Paragraph>
<Paragraph size={SIZE.xs + 1}>{item.title}</Paragraph>
</PressableButton>
))}
</>

View File

@@ -22,6 +22,7 @@ import { TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { useThemeStore } from "../../stores/use-theme-store";
import { formatBytes } from "../../utils";
import { SIZE } from "../../utils/size";
@@ -36,26 +37,20 @@ function getFileExtension(filename) {
return ext == null ? "" : ext[1];
}
export const AttachmentItem = ({
attachment,
encryption,
setAttachments,
pressable = true,
hideWhenNotDownloading
}) => {
export const AttachmentItem = ({ attachment, encryption, setAttachments }) => {
const colors = useThemeStore((state) => state.colors);
const [currentProgress, setCurrentProgress] = useAttachmentProgress(
attachment,
encryption
);
const encryptionProgress = useAttachmentStore(
(state) => state.encryptionProgress
);
const onPress = () => {
if (!pressable) return;
Actions.present(attachment, setAttachments, attachment.metadata.hash);
};
return hideWhenNotDownloading &&
(!currentProgress || !currentProgress.value) ? null : (
return (
<TouchableOpacity
activeOpacity={0.9}
onPress={onPress}
@@ -118,22 +113,20 @@ export const AttachmentItem = ({
{attachment.metadata.filename}
</Paragraph>
{!hideWhenNotDownloading ? (
<Paragraph color={colors.icon} size={SIZE.xs}>
{formatBytes(attachment.length)}{" "}
{currentProgress?.type
? "(" + currentProgress.type + "ing - tap to cancel)"
: ""}
</Paragraph>
) : null}
<Paragraph color={colors.icon} size={SIZE.xs}>
{formatBytes(attachment.length)}{" "}
{currentProgress?.type
? "(" + currentProgress.type + "ing - tap to cancel)"
: ""}
</Paragraph>
</View>
</View>
{currentProgress ? (
{currentProgress || encryptionProgress || encryption ? (
<TouchableOpacity
activeOpacity={0.9}
onPress={() => {
if (encryption || !pressable) return;
if (encryption) return;
db.fs.cancel(attachment.metadata.hash);
setCurrentProgress(null);
}}
@@ -146,7 +139,13 @@ export const AttachmentItem = ({
>
<ProgressCircleComponent
size={SIZE.xxl}
progress={currentProgress?.value ? currentProgress?.value / 100 : 0}
progress={
encryptionProgress
? encryptionProgress
: currentProgress?.value
? currentProgress?.value / 100
: 0
}
showsText
textStyle={{
fontSize: 10

View File

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

View File

@@ -17,61 +17,82 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useRef, useState } from "react";
import { ActivityIndicator, ScrollView, View } from "react-native";
import { FlatList } from "react-native-actions-sheet";
import React, { useEffect, useRef, useState } from "react";
import { View } from "react-native";
import { FlatList } from "react-native-gesture-handler";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import filesystem from "../../common/filesystem";
import { presentSheet } from "../../services/event-manager";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../services/event-manager";
import { useThemeStore } from "../../stores/use-theme-store";
import {
eCloseAttachmentDialog,
eOpenAttachmentsDialog
} from "../../utils/events";
import { SIZE } from "../../utils/size";
import SheetProvider from "../sheet-provider";
import { IconButton } from "../ui/icon-button";
import DialogHeader from "../dialog/dialog-header";
import { Toast } from "../toast";
import Input from "../ui/input";
import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import SheetWrapper from "../ui/sheet";
import Paragraph from "../ui/typography/paragraph";
import { AttachmentItem } from "./attachment-item";
import DownloadAttachments from "./download-attachments";
import { Button } from "../ui/button";
import {
isAudio,
isDocument,
isImage,
isVideo
} from "@notesnook/core/utils/filename";
export const AttachmentDialog = ({ note }) => {
export const AttachmentDialog = () => {
const colors = useThemeStore((state) => state.colors);
const [attachments, setAttachments] = useState(
note
? db.attachments.ofNote(note.id, "all")
: [...(db.attachments.all || [])]
);
const [visible, setVisible] = useState(false);
const [note, setNote] = useState(null);
const actionSheetRef = useRef();
const [attachments, setAttachments] = useState([]);
const attachmentSearchValue = useRef();
const searchTimer = useRef();
const [loading, setLoading] = useState(false);
const [currentFilter, setCurrentFilter] = useState("all");
useEffect(() => {
eSubscribeEvent(eOpenAttachmentsDialog, open);
eSubscribeEvent(eCloseAttachmentDialog, close);
return () => {
eUnSubscribeEvent(eOpenAttachmentsDialog, open);
eUnSubscribeEvent(eCloseAttachmentDialog, close);
};
}, [visible]);
const open = (data) => {
if (data?.id) {
setNote(data);
let _attachments = db.attachments.ofNote(data.id, "all");
setAttachments(_attachments);
} else {
setAttachments([...db.attachments.all]);
}
setVisible(true);
};
useEffect(() => {
if (visible) {
actionSheetRef.current?.show();
}
}, [visible]);
const close = () => {
actionSheetRef.current?.hide();
setVisible(false);
};
const onChangeText = (text) => {
const attachments = note
? db.attachments.ofNote(note.id, "all")
: [...(db.attachments.all || [])];
attachmentSearchValue.current = text;
if (
!attachmentSearchValue.current ||
attachmentSearchValue.current === ""
) {
setAttachments([...attachments]);
setAttachments([...db.attachments.all]);
}
clearTimeout(searchTimer.current);
searchTimer.current = setTimeout(() => {
let results = db.lookup.attachments(
attachments,
db.attachments.all,
attachmentSearchValue.current
);
if (results.length === 0) return;
@@ -83,239 +104,107 @@ export const AttachmentDialog = ({ note }) => {
<AttachmentItem setAttachments={setAttachments} attachment={item} />
);
const onCheck = async () => {
setLoading(true);
const checkedAttachments = [];
for (let attachment of attachments) {
let result = await filesystem.checkAttachment(attachment.metadata.hash);
if (result.failed) {
await db.attachments.markAsFailed(
attachment.metadata.hash,
result.failed
);
} else {
await db.attachments.markAsFailed(attachment.id, null);
}
checkedAttachments.push(
db.attachments.attachment(attachment.metadata.hash)
);
setAttachments([...checkedAttachments]);
}
setLoading(false);
};
const attachmentTypes = [
{
title: "All",
filterBy: "all"
},
{
title: "Images",
filterBy: "images"
},
{
title: "Documents",
filterBy: "documents"
},
{
title: "Video",
filterBy: "video"
},
{
title: "Audio",
filterBy: "audio"
}
];
const filterAttachments = (type) => {
const attachments = note
? db.attachments.ofNote(note.id, "all")
: [...(db.attachments.all || [])];
isDocument;
switch (type) {
case "all":
return attachments;
case "images":
return attachments.filter((attachment) =>
isImage(attachment.metadata.type)
);
case "video":
return attachments.filter((attachment) =>
isVideo(attachment.metadata.type)
);
case "audio":
return attachments.filter((attachment) =>
isAudio(attachment.metadata.type)
);
case "documents":
return attachments.filter((attachment) =>
isDocument(attachment.metadata.type)
);
}
};
return (
<View
style={{
width: "100%",
alignSelf: "center",
paddingHorizontal: 12
return !visible ? null : (
<SheetWrapper
centered={false}
fwdRef={actionSheetRef}
onClose={async () => {
setVisible(false);
}}
>
<SheetProvider context="attachments-list" />
<Toast context="local" />
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center"
width: "100%",
alignSelf: "center",
paddingHorizontal: 12
}}
>
<Heading>Attachments</Heading>
<DialogHeader
title={note ? "Attachments" : "Manage attachments"}
paragraph="Tap on an attachment to view properties"
button={{
title: "Check all",
type: "grayAccent",
loading: loading,
onPress: async () => {
setLoading(true);
for (let attachment of attachments) {
let result = await filesystem.checkAttachment(
attachment.metadata.hash
);
if (result.failed) {
db.attachments.markAsFailed(
attachment.metadata.hash,
result.failed
);
} else {
db.attachments.markAsFailed(attachment.id, null);
}
setAttachments([...db.attachments.all]);
}
setLoading(false);
}
}}
/>
<Seperator />
{!note ? (
<Input
placeholder="Filter attachments by filename, type or hash"
onChangeText={onChangeText}
onSubmit={() => {
onChangeText(attachmentSearchValue.current);
}}
/>
) : null}
<View
<FlatList
nestedScrollEnabled
overScrollMode="never"
scrollToOverflowEnabled={false}
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
onMomentumScrollEnd={() => {
actionSheetRef.current?.handleChildScrollEnd();
}}
ListEmptyComponent={
<View
style={{
height: 150,
justifyContent: "center",
alignItems: "center"
}}
>
<Icon name="attachment" size={60} color={colors.icon} />
<Paragraph>
{note ? "No attachments on this note" : "No attachments"}
</Paragraph>
</View>
}
ListFooterComponent={
<View
style={{
height: 350
}}
/>
}
data={attachments}
keyExtractor={(item) => item.id}
renderItem={renderItem}
/>
<Paragraph
color={colors.icon}
size={SIZE.xs}
style={{
flexDirection: "row"
textAlign: "center",
marginTop: 10
}}
>
{loading ? (
<ActivityIndicator
style={{
height: 40,
width: 40,
marginRight: 10
}}
size={SIZE.lg}
/>
) : (
<IconButton
name="check-all"
customStyle={{
height: 40,
width: 40,
marginRight: 10
}}
color={colors.pri}
size={SIZE.lg}
onPress={onCheck}
/>
)}
<IconButton
name="download"
customStyle={{
height: 40,
width: 40
}}
color={colors.pri}
onPress={() => {
DownloadAttachments.present(
"attachments-list",
attachments,
!!note
);
}}
size={SIZE.lg}
/>
</View>
<Icon name="shield-key-outline" size={SIZE.xs} color={colors.icon} />
{" "}All attachments are end-to-end encrypted.
</Paragraph>
</View>
<Seperator />
<Input
placeholder="Filter attachments by filename, type or hash"
onChangeText={onChangeText}
onSubmit={() => {
onChangeText(attachmentSearchValue.current);
}}
/>
<FlatList
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
maxToRenderPerBatch={10}
initialNumToRender={10}
windowSize={5}
stickyHeaderIndices={[0]}
ListHeaderComponent={
<ScrollView
style={{
width: "100%",
height: 50,
flexDirection: "row",
backgroundColor: colors.bg
}}
contentContainerStyle={{
minWidth: "100%"
}}
horizontal
>
{attachmentTypes.map((item) => (
<Button
type={currentFilter === item.filterBy ? "grayAccent" : "gray"}
key={item.title}
title={
item.title +
` (${filterAttachments(item.filterBy)?.length || 0})`
}
style={{
borderRadius: 0,
borderBottomWidth: 1,
flexGrow: 1,
borderBottomColor:
currentFilter !== item.filterBy
? "transparent"
: colors.accent
}}
onPress={() => {
setCurrentFilter(item.filterBy);
setAttachments(filterAttachments(item.filterBy));
}}
/>
))}
</ScrollView>
}
ListEmptyComponent={
<View
style={{
height: 150,
justifyContent: "center",
alignItems: "center"
}}
>
<Icon name="attachment" size={60} color={colors.icon} />
<Paragraph>
{note ? "No attachments on this note" : "No attachments"}
</Paragraph>
</View>
}
ListFooterComponent={
<View
style={{
height: 350
}}
/>
}
data={attachments}
keyExtractor={(item) => item.id}
renderItem={renderItem}
/>
<Paragraph
color={colors.icon}
size={SIZE.xs}
style={{
textAlign: "center",
marginTop: 10
}}
>
<Icon name="shield-key-outline" size={SIZE.xs} color={colors.icon} />
{" "}All attachments are end-to-end encrypted.
</Paragraph>
</View>
</SheetWrapper>
);
};
AttachmentDialog.present = (note) => {
presentSheet({
component: () => <AttachmentDialog note={note} />
});
};

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useRef, useState } from "react";
import { View } from "react-native";
import { Platform, View } from "react-native";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import {
eSubscribeEvent,
@@ -75,7 +75,7 @@ const AuthModal = () => {
return !visible ? null : (
<BaseDialog
overlayOpacity={0}
statusBarTranslucent={true}
statusBarTranslucent={false}
onRequestClose={currentAuthMode !== AuthMode.welcomeSignup && close}
visible={true}
onClose={close}
@@ -84,7 +84,6 @@ const AuthModal = () => {
background={colors.bg}
transparent={false}
animated={false}
avoidKeyboardResize
>
{currentAuthMode !== AuthMode.login ? (
<Signup
@@ -102,7 +101,7 @@ const AuthModal = () => {
<View
style={{
position: "absolute",
top: insets.top,
top: Platform.OS === "ios" ? insets.top : 0,
zIndex: 999,
flexDirection: "row",
alignItems: "center",
@@ -127,11 +126,11 @@ const AuthModal = () => {
{initialAuthMode.current !== AuthMode.welcomeSignup ? null : (
<Button
title="Skip"
title="Skip for now"
onPress={() => {
hideAuth();
}}
iconSize={16}
iconSize={20}
type="gray"
iconPosition="right"
icon="chevron-right"

View File

@@ -18,8 +18,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useState } from "react";
import { TouchableOpacity, View } from "react-native";
import { View } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import Animated, { FadeInDown, FadeOutUp } from "react-native-reanimated";
import { DDS } from "../../services/device-detection";
import { eSendEvent } from "../../services/event-manager";
import { useThemeStore } from "../../stores/use-theme-store";
@@ -29,12 +30,13 @@ import SheetProvider from "../sheet-provider";
import { Progress } from "../sheets/progress";
import { Button } from "../ui/button";
import Input from "../ui/input";
import { SvgView } from "../ui/svg";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { SVG } from "./background";
import { hideAuth } from "./common";
import { ForgotPassword } from "./forgot-password";
import { useLogin } from "./use-login";
import { useSettingStore } from "../../stores/use-setting-store";
const LoginSteps = {
emailAuth: 1,
@@ -62,7 +64,6 @@ export const Login = ({ changeMode }) => {
await sleep(500);
Progress.present();
});
const deviceMode = useSettingStore((state) => state.deviceMode);
useEffect(() => {
async () => {
@@ -81,70 +82,62 @@ export const Login = ({ changeMode }) => {
<>
<ForgotPassword />
<SheetProvider context="two_factor_verify" />
<View
<Animated.View
entering={FadeInDown}
exiting={FadeOutUp}
style={{
borderRadius: DDS.isTab ? 5 : 0,
backgroundColor: colors.bg,
zIndex: 10,
width: "100%",
alignSelf: "center",
height: "100%",
minHeight: "100%"
}}
>
<View
style={{
flexGrow: 0.8,
justifyContent: "flex-end",
paddingHorizontal: 20,
backgroundColor: colors.nav,
borderBottomWidth: 1,
marginBottom: 12,
borderBottomColor: colors.border,
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
borderWidth: deviceMode !== "mobile" ? 1 : null,
borderColor: deviceMode !== "mobile" ? colors.border : null,
borderRadius: deviceMode !== "mobile" ? 20 : null,
marginTop: deviceMode !== "mobile" ? 50 : null,
width: deviceMode === "mobile" ? null : "50%"
height: 250,
overflow: "hidden"
}}
>
<SvgView
src={SVG(colors.night ? colors.icon : "black")}
height={700}
/>
</View>
<View
style={{
width: "100%",
justifyContent: "center",
alignSelf: "center",
paddingHorizontal: 12,
marginBottom: 30,
marginTop: 15
}}
>
<View
style={{
flexDirection: "row"
}}
>
<View
style={{
width: 100,
height: 5,
backgroundColor: colors.accent,
borderRadius: 2,
marginRight: 7
}}
/>
<View
style={{
width: 20,
height: 5,
backgroundColor: colors.nav,
borderRadius: 2
}}
/>
</View>
<Heading
style={{
marginBottom: 25,
marginTop: 10
textAlign: "center"
}}
extraBold
size={SIZE.xxl}
size={30}
color={colors.heading}
>
Login to your {"\n"}account
Welcome back!
</Heading>
<Paragraph
style={{
textDecorationLine: "underline",
textAlign: "center",
marginTop: 5
}}
onPress={() => {
if (loading) return;
changeMode(1);
}}
size={SIZE.md}
>
{"Don't have an account? Sign up"}
</Paragraph>
</View>
<View
style={{
width: DDS.isTab
@@ -154,10 +147,10 @@ export const Login = ({ changeMode }) => {
: focused
? "100%"
: "99.9%",
padding: 12,
backgroundColor: colors.bg,
alignSelf: "center",
paddingHorizontal: 20,
flexGrow: 1
flexGrow: 1,
alignSelf: "center"
}}
>
<Input
@@ -224,22 +217,28 @@ export const Login = ({ changeMode }) => {
<View
style={{
marginTop: 25
marginTop: 25,
alignSelf: "center"
}}
>
<Button
style={{
width: 250,
borderRadius: 100
}}
loading={loading}
onPress={() => {
if (loading) return;
login();
}}
style={{
width: 250,
borderRadius: 100
}}
fontSize={SIZE.md}
type="accent"
title={!loading ? "Continue" : null}
title={
loading
? null
: step === LoginSteps.emailAuth
? "Login"
: "Continue"
}
/>
{step === LoginSteps.passwordAuth && (
@@ -262,34 +261,9 @@ export const Login = ({ changeMode }) => {
type="errorShade"
/>
)}
{!loading ? (
<TouchableOpacity
onPress={() => {
if (loading) return;
changeMode(1);
}}
activeOpacity={0.8}
style={{
alignSelf: "center",
marginTop: 12,
paddingVertical: 12
}}
>
<Paragraph size={SIZE.xs + 1} color={colors.icon}>
Don't have an account?{" "}
<Paragraph
size={SIZE.xs + 1}
style={{ color: colors.accent }}
>
Sign up
</Paragraph>
</Paragraph>
</TouchableOpacity>
) : null}
</View>
</View>
</View>
</Animated.View>
</>
);
};

View File

@@ -44,10 +44,8 @@ import Paragraph from "../ui/typography/paragraph";
import { LoginSteps, useLogin } from "./use-login";
function getObfuscatedEmail(email) {
if (!email) return "";
const [username, provider] = email.split("@");
if (username.length === 1) return `****@${provider}`;
return email.replace(/(.{1})(.*)(?=@)/, function (gp1, gp2, gp3) {
if (!email) return null;
return email.replace(/(.{2})(.*)(?=@)/, function (gp1, gp2, gp3) {
for (let i = 0; i < gp3.length; i++) {
gp2 += "*";
}

View File

@@ -18,7 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useRef, useState } from "react";
import { TouchableOpacity, View } from "react-native";
import { Dimensions, View } from "react-native";
import Animated, { FadeInDown, FadeOutUp } from "react-native-reanimated";
import { db } from "../../common/database";
import { DDS } from "../../services/device-detection";
import { ToastEvent } from "../../services/event-manager";
@@ -31,10 +32,11 @@ import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { Button } from "../ui/button";
import Input from "../ui/input";
import { SvgView } from "../ui/svg";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { SVG } from "./background";
import { hideAuth } from "./common";
import { useSettingStore } from "../../stores/use-setting-store";
export const Signup = ({ changeMode, trial }) => {
const colors = useThemeStore((state) => state.colors);
@@ -48,7 +50,6 @@ export const Signup = ({ changeMode, trial }) => {
const [loading, setLoading] = useState(false);
const setUser = useUserStore((state) => state.setUser);
const setLastSynced = useUserStore((state) => state.setLastSynced);
const deviceMode = useSettingStore((state) => state.deviceMode);
const validateInfo = () => {
if (!password.current || !email.current || !confirmPassword.current) {
@@ -95,77 +96,69 @@ export const Signup = ({ changeMode, trial }) => {
return (
<>
<View
<Animated.View
entering={FadeInDown}
exiting={FadeOutUp}
style={{
borderRadius: DDS.isTab ? 5 : 0,
backgroundColor: colors.bg,
zIndex: 10,
width: "100%",
alignSelf: "center",
height: "100%",
minHeight: "100%"
}}
>
<View
style={{
flexGrow: 1,
justifyContent: "flex-end",
paddingHorizontal: 20,
backgroundColor: colors.nav,
marginBottom: 20,
borderBottomWidth: 1,
borderBottomColor: colors.border,
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
borderWidth: deviceMode !== "mobile" ? 1 : null,
borderColor: deviceMode !== "mobile" ? colors.border : null,
borderRadius: deviceMode !== "mobile" ? 20 : null,
marginTop: deviceMode !== "mobile" ? 50 : null,
width: deviceMode === "mobile" ? null : "50%"
height: 250,
overflow: "hidden"
}}
>
<View
style={{
flexDirection: "row"
}}
>
<View
style={{
width: 100,
height: 5,
backgroundColor: colors.accent,
borderRadius: 2,
marginRight: 7
}}
/>
<View
style={{
width: 20,
height: 5,
backgroundColor: colors.nav,
borderRadius: 2
}}
/>
</View>
<Heading
extraBold
style={{
marginBottom: 25,
marginTop: 10
}}
size={SIZE.xxl}
>
Create your {"\n"}account
</Heading>
<SvgView
src={SVG(colors.night ? colors.icon : "black")}
height={700}
/>
</View>
<View
style={{
width: DDS.isTab ? "50%" : "100%",
paddingHorizontal: 20,
backgroundColor: colors.bg,
width: "100%",
justifyContent: "center",
alignSelf: "center",
flexGrow: 0.5
paddingHorizontal: 12,
marginBottom: 30,
marginTop: Dimensions.get("window").height < 700 ? -75 : 15
}}
>
<Heading
style={{
textAlign: "center"
}}
size={30}
color={colors.heading}
>
Create your account
</Heading>
<Paragraph
style={{
textDecorationLine: "underline",
textAlign: "center"
}}
onPress={() => {
if (loading) return;
changeMode(0);
}}
size={SIZE.md}
>
Already have an account? Log in
</Paragraph>
</View>
<View
style={{
width: DDS.isTab ? "50%" : "100%",
padding: 12,
backgroundColor: colors.bg,
flexGrow: 1,
alignSelf: "center"
}}
>
<Input
@@ -224,13 +217,37 @@ export const Signup = ({ changeMode, trial }) => {
validationType="confirmPassword"
customValidator={() => password.current}
placeholder="Confirm password"
marginBottom={12}
marginBottom={5}
onSubmit={signup}
/>
<View
style={{
marginTop: 25,
alignSelf: "center"
}}
>
<Button
style={{
width: 250,
borderRadius: 100
}}
loading={loading}
onPress={() => {
if (loading) return;
signup();
}}
type="accent"
title={loading ? null : "Agree and continue"}
/>
</View>
<Paragraph
style={{
marginBottom: 25
textAlign: "center",
position: "absolute",
bottom: 0,
alignSelf: "center",
marginBottom: 20
}}
size={SIZE.xs}
color={colors.icon}
@@ -246,7 +263,7 @@ export const Signup = ({ changeMode, trial }) => {
}}
color={colors.accent}
>
Terms of Service{" "}
terms of service{" "}
</Paragraph>
and{" "}
<Paragraph
@@ -259,46 +276,11 @@ export const Signup = ({ changeMode, trial }) => {
}}
color={colors.accent}
>
Privacy Policy.
</Paragraph>{" "}
You also agree to recieve marketing emails from us which you can
opt-out of from app settings.
</Paragraph>
<Button
title={!loading ? "Continue" : null}
type="accent"
loading={loading}
onPress={signup}
fontSize={SIZE.md}
style={{
marginRight: 12,
width: 250,
borderRadius: 100
}}
/>
<TouchableOpacity
onPress={() => {
if (loading) return;
changeMode(0);
}}
activeOpacity={0.8}
style={{
alignSelf: "center",
marginTop: 12,
paddingVertical: 12
}}
>
<Paragraph size={SIZE.xs + 1} color={colors.icon}>
Already have an account?{" "}
<Paragraph size={SIZE.xs + 1} style={{ color: colors.accent }}>
Login
</Paragraph>
privacy policy.
</Paragraph>
</TouchableOpacity>
</Paragraph>
</View>
</View>
</Animated.View>
</>
);
};

View File

@@ -21,6 +21,7 @@ import React from "react";
import { useNoteStore } from "../../stores/use-notes-store";
import { useThemeStore } from "../../stores/use-theme-store";
import { AnnouncementDialog } from "../announcements";
import { AttachmentDialog } from "../attachments";
import AuthModal from "../auth/auth-modal";
import { SessionExpired } from "../auth/session-expired";
import { Dialog } from "../dialog";
@@ -33,10 +34,13 @@ import MergeConflicts from "../merge-conflicts";
import PremiumDialog from "../premium";
import { Expiring } from "../premium/expiring";
import SheetProvider from "../sheet-provider";
import { AddNotebookSheet } from "../sheets/add-notebook";
import AddToNotebookSheet from "../sheets/add-to";
import ManageTagsSheet from "../sheets/manage-tags";
import PublishNoteSheet from "../sheets/publish-note";
import RateAppSheet from "../sheets/rate-app";
import RecoveryKeySheet from "../sheets/recovery-key";
import RestoreDataSheet from "../sheets/restore-data";
import PDFPreview from "../dialogs/pdf-preview";
const DialogProvider = () => {
const colors = useThemeStore((state) => state.colors);
@@ -47,6 +51,7 @@ const DialogProvider = () => {
<LoadingDialog />
<Dialog context="global" />
<AddTopicDialog colors={colors} />
<AddNotebookSheet colors={colors} />
<PremiumDialog colors={colors} />
<AuthModal colors={colors} />
<MergeConflicts />
@@ -56,12 +61,15 @@ const DialogProvider = () => {
<RestoreDataSheet />
<ResultDialog />
<VaultDialog colors={colors} />
<AddToNotebookSheet colors={colors} />
<RateAppSheet />
<ImagePreview />
<PublishNoteSheet />
<ManageTagsSheet />
<AttachmentDialog />
{loading ? null : <Expiring />}
<AnnouncementDialog />
<SessionExpired />
<PDFPreview />
</>
);
};

View File

@@ -46,8 +46,7 @@ const BaseDialog = ({
animated = true,
bounce = true,
closeOnTouch = true,
useSafeArea = true,
avoidKeyboardResize = false
useSafeArea = true
}) => {
const floating = useIsFloatingKeyboard();
@@ -95,7 +94,7 @@ const BaseDialog = ({
}}
>
<KeyboardAvoidingView
enabled={!floating && Platform.OS === "ios" && !avoidKeyboardResize}
enabled={!floating && Platform.OS === "ios"}
behavior="padding"
>
<BouncingView

View File

@@ -35,7 +35,6 @@ import BaseDialog from "./base-dialog";
import DialogButtons from "./dialog-buttons";
import DialogHeader from "./dialog-header";
import { useCallback } from "react";
import { Button } from "../ui/button";
export const Dialog = ({ context = "global" }) => {
const colors = useThemeStore((state) => state.colors);
@@ -55,11 +54,7 @@ export const Dialog = ({ context = "global" }) => {
input: false,
inputPlaceholder: "Enter some text",
defaultValue: "",
disableBackdropClosing: false,
check: {
info: "Check",
type: "transparent"
}
disableBackdropClosing: false
});
useEffect(() => {
@@ -92,7 +87,6 @@ export const Dialog = ({ context = "global" }) => {
if (data.context !== context) return;
setDialogInfo(data);
setVisible(true);
setInputValue(data.defaultValue);
},
[context]
);
@@ -143,9 +137,6 @@ export const Dialog = ({ context = "global" }) => {
paragraph={dialogInfo.paragraph}
paragraphColor={dialogInfo.paragraphColor}
padding={12}
style={{
minHeight: 0
}}
/>
<Seperator half />
@@ -172,28 +163,6 @@ export const Dialog = ({ context = "global" }) => {
</View>
) : null}
{dialogInfo.check ? (
<>
<Button
onPress={() => {
setInputValue(!inputValue);
}}
icon={
inputValue
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
style={{
justifyContent: "flex-start"
}}
height={35}
width="100%"
title={dialogInfo.check.info}
type={inputValue ? dialogInfo.check.type : "gray"}
/>
</>
) : null}
<DialogButtons
onPressNegative={onNegativePress}
onPressPositive={dialogInfo.positivePress && onPressPositive}

View File

@@ -22,7 +22,6 @@ import { View } from "react-native";
import { useMenuStore } from "../../../stores/use-menu-store";
import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent,
ToastEvent
@@ -31,7 +30,6 @@ import Navigation from "../../../services/navigation";
import { db } from "../../../common/database";
import {
eCloseAddTopicDialog,
eOnTopicSheetUpdate,
eOpenAddTopicDialog
} from "../../../utils/events";
import { sleep } from "../../../utils/time";
@@ -42,7 +40,6 @@ import DialogHeader from "../../dialog/dialog-header";
import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import { Toast } from "../../toast";
import { useRelationStore } from "../../../stores/use-relation-store";
export class AddTopicDialog extends React.Component {
constructor(props) {
@@ -62,12 +59,14 @@ export class AddTopicDialog extends React.Component {
addNewTopic = async () => {
try {
this.setState({ loading: true });
if (!this.title || this.title?.trim() === "") {
ToastEvent.show({
heading: "Topic title is required",
type: "error",
context: "local"
});
this.setState({ loading: false });
return;
}
@@ -79,13 +78,10 @@ export class AddTopicDialog extends React.Component {
await db.notebooks.notebook(topic.notebookId).topics.add(topic);
}
this.setState({ loading: false });
this.close();
setTimeout(() => {
Navigation.queueRoutesForUpdate();
useMenuStore.getState().setMenuPins();
});
eSendEvent(eOnTopicSheetUpdate);
useRelationStore.getState().update();
Navigation.queueRoutesForUpdate("Notebooks", "Notebook", "TopicNotes");
useMenuStore.getState().setMenuPins();
} catch (e) {
console.error(e);
}
@@ -181,6 +177,7 @@ export class AddTopicDialog extends React.Component {
positiveTitle={this.toEdit ? "Save" : "Add"}
onPressNegative={() => this.close()}
onPressPositive={() => this.addNewTopic()}
loading={this.state.loading}
/>
</DialogContainer>
<Toast context="local" />

View File

@@ -1,324 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useRef, useState } from "react";
import { Dimensions, TextInput, View } from "react-native";
import {
addOrientationListener,
removeOrientationListener
} from "react-native-orientation";
import Pdf from "react-native-pdf";
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
import downloadAttachment from "../../../common/filesystem/download-attachment";
import { cacheDir } from "../../../common/filesystem/utils";
import { useAttachmentProgress } from "../../../hooks/use-attachment-progress";
import useGlobalSafeAreaInsets from "../../../hooks/use-global-safe-area-insets";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../../services/event-manager";
import { useThemeStore } from "../../../stores/use-theme-store";
import { SIZE } from "../../../utils/size";
import { Dialog } from "../../dialog";
import BaseDialog from "../../dialog/base-dialog";
import { presentDialog } from "../../dialog/functions";
import SheetProvider from "../../sheet-provider";
import { IconButton } from "../../ui/icon-button";
import { ProgressBarComponent } from "../../ui/svg/lazy";
import Paragraph from "../../ui/typography/paragraph";
import { sleep } from "../../../utils/time";
import { MMKV } from "../../../common/database/mmkv";
const WIN_WIDTH = Dimensions.get("window").width;
const WIN_HEIGHT = Dimensions.get("window").height;
const attachmentSnapshotsKey = "___attachmentsnapshots";
const usePDFSnapshot = (attachment) => {
const snapshots = useRef(MMKV.getMap(attachmentSnapshotsKey) || {});
const snapshot = useRef(snapshots[attachment?.id]);
function saveSnapshot(ss) {
if (!attachment) return;
snapshots.current[attachment.id] = ss;
MMKV.setMap(attachmentSnapshotsKey, snapshots.current);
snapshot.current = snapshots.current[attachment.id];
}
return [snapshot, saveSnapshot];
};
const PDFPreview = () => {
const colors = useThemeStore((state) => state.colors);
const [visible, setVisible] = useState(false);
const [pdfSource, setPDFSource] = useState();
const [loading, setLoading] = useState(false);
const [width, setWidth] = useState(WIN_WIDTH);
const insets = useGlobalSafeAreaInsets();
const [numPages, setNumPages] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
const inputRef = useRef();
const pdfRef = useRef();
const [attachment, setAttachment] = useState(null);
const [password, setPassword] = useState("");
const [progress] = useAttachmentProgress(attachment);
const [snapshot, saveSnapshot] = usePDFSnapshot(attachment);
const snapshotValue = useRef(snapshot.current);
useEffect(() => {
eSubscribeEvent("PDFPreview", open);
return () => {
eUnSubscribeEvent("PDFPreview", open);
};
}, []);
const onOrientationChange = (o) => {
if (o.includes("LANDSCAPE")) {
setWidth(WIN_HEIGHT);
} else {
setWidth(WIN_WIDTH);
}
};
useEffect(() => {
addOrientationListener(onOrientationChange);
return () => {
removeOrientationListener(onOrientationChange);
};
}, []);
const open = async (attachment) => {
setVisible(true);
setLoading(true);
setTimeout(async () => {
setAttachment(attachment);
let hash = attachment.metadata.hash;
if (!hash) return;
const uri = await downloadAttachment(hash, false, {
silent: true,
cache: true
});
const path = `${cacheDir}/${uri}`;
snapshotValue.current = snapshot.current;
setPDFSource("file://" + path);
setLoading(false);
}, 100);
};
const close = () => {
setPDFSource(null);
setVisible(false);
setPassword("");
};
const onError = async (error) => {
if (error?.message === "Password required or incorrect password.") {
await sleep(300);
presentDialog({
context: attachment?.metadata?.hash,
input: true,
inputPlaceholder: "Enter password",
positiveText: "Unlock",
title: "Decrypt",
paragraph: "Please input password to view pdf.",
positivePress: (value) => {
setTimeout(() => {
setPassword(value);
});
},
onClose: () => {
close();
}
});
}
};
return (
visible && (
<BaseDialog animation="fade" visible={true} onRequestClose={close}>
<SheetProvider context={attachment?.metadata?.hash} />
<Dialog context={attachment?.metadata?.hash} />
<View
style={{
width: "100%",
height: "100%",
backgroundColor: "black"
}}
>
{loading ? (
<Animated.View
exiting={FadeOut}
style={{
flex: 1,
justifyContent: "center",
alignItems: "center"
}}
>
<ProgressBarComponent
indeterminate={!progress}
color={colors.accent}
borderColor="transparent"
progress={parseInt(progress?.value || "100") / 100}
/>
<Paragraph
style={{
marginTop: 10
}}
color={colors.light}
>
Loading {`${progress?.percent ? `(${progress?.percent})` : ""}`}
... Please wait
</Paragraph>
</Animated.View>
) : (
<>
<View
style={{
width: "100%",
height: 50,
marginTop: insets.top,
flexDirection: "row",
justifyContent: "space-between",
paddingHorizontal: 12,
paddingLeft: 6
}}
>
<View
style={{
flexDirection: "row"
}}
>
<IconButton
color={colors.light}
name="arrow-left"
onPress={close}
customStyle={{
marginRight: 12
}}
size={SIZE.xxl}
/>
</View>
<View
style={{
flexDirection: "row",
alignItems: "center",
marginRight: 12
}}
>
<TextInput
ref={inputRef}
defaultValue={currentPage + ""}
style={{
color: colors.pri,
padding: 0,
paddingTop: 0,
paddingBottom: 0,
marginTop: 0,
marginBottom: 0,
paddingVertical: 0,
height: 25,
backgroundColor: colors.nav,
width: 40,
textAlign: "center",
marginRight: 4,
borderRadius: 3,
fontFamily: "OpenSans-Regular"
}}
selectTextOnFocus
keyboardType="decimal-pad"
onSubmitEditing={(event) => {
setCurrentPage(event.nativeEvent.text);
pdfRef.current?.setPage(parseInt(event.nativeEvent.text));
}}
blurOnSubmit
/>
<Paragraph color={colors.light}>/{numPages}</Paragraph>
</View>
<View
style={{
flexDirection: "row"
}}
>
<IconButton
color={colors.light}
name="download"
onPress={() => {
downloadAttachment(attachment.metadata.hash, false);
}}
/>
</View>
</View>
{pdfSource ? (
<Animated.View
style={{
flex: 1
}}
entering={FadeIn}
>
<Pdf
source={{
uri: pdfSource
}}
ref={pdfRef}
onLoadComplete={(numberOfPages) => {
setNumPages(numberOfPages);
}}
onPageChanged={(page) => {
setCurrentPage(page);
inputRef.current?.setNativeProps({
text: page + ""
});
saveSnapshot({
currentPage: page,
scale: snapshot?.current?.scale
});
}}
// scale={snapshotValue.current?.scale}
// onScaleChanged={(scale) => {
// saveSnapshot({
// currentPage: snapshot?.current?.currentPage,
// scale: scale
// });
// }}
page={snapshotValue?.current?.currentPage}
password={password}
maxScale={6}
onError={onError}
onPressLink={(uri) => {
console.log(`Link pressed: ${uri}`);
}}
style={{
flex: 1,
width: width,
height: Dimensions.get("window").height
}}
/>
</Animated.View>
) : null}
</>
)}
</View>
</BaseDialog>
)
);
};
export default PDFPreview;

View File

@@ -135,6 +135,7 @@ const ResultDialog = () => {
paddingHorizontal: 12
}}
onPress={close}
height={50}
fontSize={SIZE.md + 2}
/>
</View>

View File

@@ -86,7 +86,7 @@ export const ProFeatures = ({ count = 6 }) => {
await sleep(300);
eSendEvent(eOpenPremiumDialog);
}}
size={SIZE.xs}
size={SIZE.xs + 1}
style={{
textDecorationLine: "underline",
color: colors.icon

View File

@@ -207,7 +207,13 @@ export class VaultDialog extends Component {
return;
}
Navigation.queueRoutesForUpdate();
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"TopicNotes",
"TaggedNotes",
"ColoredNotes"
);
this.password = null;
this.confirmPassword = null;

View File

@@ -30,6 +30,7 @@ import { useThemeStore } from "../../stores/use-theme-store";
import { eScrollEvent } from "../../utils/events";
import { SIZE } from "../../utils/size";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { useCallback } from "react";
import Tag from "../ui/tag";
@@ -63,15 +64,13 @@ export const Title = () => {
}
if (data.y > 150) {
if (!hide) return;
titleState[currentScreen.id] = false;
setHide(false);
} else {
if (hide) return;
titleState[currentScreen.id] = true;
setHide(true);
}
},
[currentScreen.id, currentScreen.name, hide]
[currentScreen.name, hide]
);
useEffect(() => {
@@ -86,6 +85,10 @@ export const Title = () => {
}
}, [currentScreen.id, currentScreen.name]);
useEffect(() => {
titleState[currentScreen.id] = hide;
}, [currentScreen.id, hide]);
useEffect(() => {
eSubscribeEvent(eScrollEvent, onScroll);
return () => {
@@ -109,16 +112,25 @@ export const Title = () => {
{!hide && !isHidden ? (
<Heading
onPress={navigateToNotebook}
numberOfLines={1}
size={SIZE.xl}
numberOfLines={isTopic ? 2 : 1}
size={isTopic ? SIZE.md + 2 : SIZE.xl}
style={{
flexWrap: "wrap",
marginTop: Platform.OS === "ios" ? -1 : 0
}}
color={currentScreen.color || colors.heading}
>
{isTopic ? (
<Paragraph numberOfLines={1} size={SIZE.xs + 1}>
{notebook?.title}
{"\n"}
</Paragraph>
) : null}
{isTag ? (
<Heading size={SIZE.xl} color={colors.accent}>
<Heading
size={isTopic ? SIZE.md + 2 : SIZE.xl}
color={colors.accent}
>
#
</Heading>
) : null}

View File

@@ -18,26 +18,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useState } from "react";
import { Platform, View } from "react-native";
import { View } from "react-native";
import ImageViewer from "react-native-image-zoom-viewer";
import downloadAttachment from "../../common/filesystem/download-attachment";
import { cacheDir } from "../../common/filesystem/utils";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../services/event-manager";
import { useThemeStore } from "../../stores/use-theme-store";
import BaseDialog from "../dialog/base-dialog";
import { IconButton } from "../ui/icon-button";
import { ProgressBarComponent } from "../ui/svg/lazy";
import Sodium from "@ammarahmed/react-native-sodium";
import dataurl from "@notesnook/core/utils/dataurl";
const ImagePreview = () => {
const colors = useThemeStore((state) => state.colors);
const [visible, setVisible] = useState(false);
const [image, setImage] = useState("");
const [loading, setLoading] = useState(false);
useEffect(() => {
eSubscribeEvent("ImagePreview", open);
@@ -47,29 +39,9 @@ const ImagePreview = () => {
};
}, []);
const open = async (image) => {
const open = (image) => {
setImage(image);
setVisible(true);
setLoading(true);
setTimeout(async () => {
let hash = image.hash;
if (!hash && dataurl.toObject(image.src)) {
const data = dataurl.toObject(image.src);
if (!data) return;
hash = await Sodium.hashFile({
data: data.data,
type: "base64",
uri: ""
});
}
if (!hash) return;
const uri = await downloadAttachment(hash, false, {
silent: true,
cache: true
});
const path = `${cacheDir}/${uri}`;
setImage("file://" + path);
setLoading(false);
}, 100);
};
const close = () => {
@@ -87,60 +59,44 @@ const ImagePreview = () => {
backgroundColor: "black"
}}
>
{loading ? (
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center"
}}
>
<ProgressBarComponent
indeterminate
color={colors.accent}
borderColor="transparent"
/>
</View>
) : (
<ImageViewer
enableImageZoom={true}
renderIndicator={() => <></>}
enableSwipeDown
useNativeDriver
onSwipeDown={close}
saveToLocalByLongPress={false}
renderHeader={() => (
<View
style={{
flexDirection: "row",
width: "100%",
justifyContent: "flex-end",
alignItems: "center",
height: 80,
marginTop: 0,
paddingHorizontal: 12,
position: "absolute",
zIndex: 999,
backgroundColor: "rgba(0,0,0,0.3)",
paddingTop: Platform.OS === "android" ? 30 : 0
<ImageViewer
enableImageZoom={true}
renderIndicator={() => <></>}
enableSwipeDown
useNativeDriver
onSwipeDown={close}
saveToLocalByLongPress={false}
renderHeader={() => (
<View
style={{
flexDirection: "row",
width: "100%",
justifyContent: "flex-end",
alignItems: "center",
height: 80,
marginTop: 0,
paddingHorizontal: 12,
position: "absolute",
zIndex: 999,
backgroundColor: "rgba(0,0,0,0.3)",
paddingTop: 30
}}
>
<IconButton
name="close"
color="white"
onPress={() => {
close();
}}
>
<IconButton
name="close"
color="white"
onPress={() => {
close();
}}
/>
</View>
)}
imageUrls={[
{
url: image
}
]}
/>
)}
/>
</View>
)}
imageUrls={[
{
url: image
}
]}
/>
</View>
</BaseDialog>
)

File diff suppressed because one or more lines are too long

View File

@@ -17,14 +17,12 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import notifee from "@notifee/react-native";
import React, { useCallback, useEffect, useRef } from "react";
import { Platform, View } from "react-native";
import RNBootSplash from "react-native-bootsplash";
import { checkVersion } from "react-native-check-version";
import Config from "react-native-config";
import { enabled } from "react-native-privacy-snapshot";
import { DatabaseLogger, db } from "../../common/database";
import { DatabaseLogger, db, loadDatabase } from "../../common/database";
import { useAppState } from "../../hooks/use-app-state";
import BiometricService from "../../services/biometrics";
import { eSendEvent, presentSheet } from "../../services/event-manager";
@@ -37,11 +35,10 @@ import { useNoteStore } from "../../stores/use-notes-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { useThemeStore } from "../../stores/use-theme-store";
import { useUserStore } from "../../stores/use-user-store";
import { AndroidModule } from "../../utils";
import { eOpenAnnouncementDialog } from "../../utils/events";
import { getGithubVersion } from "../../utils/github-version";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { SVG } from "../auth/background";
import Migrate from "../sheets/migrate";
import NewFeature from "../sheets/new-feature/index";
import { Update } from "../sheets/update";
@@ -49,9 +46,14 @@ import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Input from "../ui/input";
import Seperator from "../ui/seperator";
import { SvgView } from "../ui/svg";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { Walkthrough } from "../walkthroughs";
import Config from "react-native-config";
import { getGithubVersion } from "../../utils/github-version";
import notifee from "@notifee/react-native";
const Launcher = React.memo(
function Launcher() {
@@ -68,8 +70,7 @@ const Launcher = React.memo(
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
const verifying = useRef(false);
const dbInitCompleted = useRef(false);
const loadNotes = useCallback(async () => {
if (verifyUser) {
return;
@@ -80,25 +81,28 @@ const Launcher = React.memo(
initialize();
setImmediate(() => {
setLoading(false);
if (!dbInitCompleted.current) {
setImmediate(() => doAppLoadActions());
}
});
});
});
}, [setLoading, verifyUser]);
}, [doAppLoadActions, setLoading, verifyUser]);
const init = useCallback(async () => {
if (!db.isInitialized) {
RNBootSplash.hide({ fade: true });
if (!dbInitCompleted.current) {
await RNBootSplash.hide({ fade: true });
await loadDatabase();
DatabaseLogger.info("Initializing database");
await db.init();
dbInitCompleted.current = true;
}
if (db.migrations.required() && !verifyUser) {
presentSheet({
component: <Migrate />,
onClose: async () => {
if (!db.isInitialized) {
await db.init();
}
await db.init();
loadNotes();
},
disableClosing: true
@@ -117,6 +121,9 @@ const Launcher = React.memo(
if (!loading) {
doAppLoadActions();
}
return () => {
dbInitCompleted.current = false;
};
}, [doAppLoadActions, loading]);
const doAppLoadActions = useCallback(async () => {
@@ -130,6 +137,7 @@ const Launcher = React.memo(
if (NewFeature.present()) return;
if (await checkAppUpdateAvailable()) return;
if (await checkForRateAppRequest()) return;
if (await checkNeedsBackup()) return;
if (await PremiumService.getRemainingTrialDaysStatus()) return;
if (introCompleted) {
@@ -143,7 +151,7 @@ const Launcher = React.memo(
}, [introCompleted]);
const checkAppUpdateAvailable = async () => {
if (__DEV__ || Config.isTesting === "true") return;
if (__DEV__) return;
try {
const version =
Config.GITHUB_RELEASE === "true"
@@ -172,13 +180,25 @@ const Launcher = React.memo(
return false;
};
const checkNeedsBackup = async () => {
return false;
// let { nextBackupRequestTime, reminder } = SettingsService.get();
// if (reminder === 'off' || !reminder) {
// if (nextBackupRequestTime < Date.now()) {
// presentSheet({
// title: 'Backup & restore',
// paragraph: 'Please enable automatic backups to keep your data safe',
// component: <SettingsBackupAndRestore isSheet={true} />
// });
// return true;
// }
// }
// return false;
};
const onUnlockBiometrics = useCallback(async () => {
if (!(await BiometricService.isBiometryAvailable())) return;
if (Platform.OS === "android") {
const activityName = await AndroidModule.getActivityName();
if (activityName !== "MainActivity") return;
}
let verified = await BiometricService.validateUser(
"Unlock to access your notes",
""
@@ -187,9 +207,6 @@ const Launcher = React.memo(
setVerifyUser(false);
enabled(false);
password.current = null;
setTimeout(() => {
verifying.current = false;
}, 1);
}
}, [setVerifyUser]);
@@ -198,10 +215,7 @@ const Launcher = React.memo(
}, [init, verifyUser]);
useEffect(() => {
if (verifying.current || useUserStore.getState().shouldBlockVerifyUser)
return;
if (verifyUser && appState === "active") {
verifying.current = true;
onUnlockBiometrics();
}
}, [appState, onUnlockBiometrics, verifyUser]);
@@ -227,10 +241,18 @@ const Launcher = React.memo(
width: "100%",
height: "100%",
position: "absolute",
zIndex: 999,
justifyContent: "center"
zIndex: 999
}}
>
<View
style={{
height: 250,
overflow: "hidden"
}}
>
<SvgView src={SVG(colors.night ? "white" : "black")} height={700} />
</View>
<View
style={{
flex: 1,
@@ -266,13 +288,14 @@ const Launcher = React.memo(
textAlign: "center"
}}
>
Unlock your notes
Unlock to access your notes
</Heading>
<Paragraph
style={{
alignSelf: "center",
textAlign: "center",
fontSize: SIZE.md,
maxWidth: "90%"
}}
>
@@ -283,7 +306,8 @@ const Launcher = React.memo(
style={{
width: "100%",
padding: 12,
backgroundColor: colors.bg
backgroundColor: colors.bg,
flexGrow: 1
}}
>
{user ? (
@@ -323,9 +347,14 @@ const Launcher = React.memo(
<Button
title="Unlock with Biometrics"
width={250}
height={45}
style={{
borderRadius: 100
}}
onPress={onUnlockBiometrics}
icon={"fingerprint"}
type={user ? "grayAccent" : "accent"}
fontSize={SIZE.md}
/>
</View>
</View>

View File

@@ -24,10 +24,8 @@ import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import Notebook from "../../../screens/notebook";
import { TaggedNotes } from "../../../screens/notes/tagged";
import { TopicNotes } from "../../../screens/notes/topic-notes";
import useNavigationStore from "../../../stores/use-navigation-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { useSettingStore } from "../../../stores/use-setting-store";
import { useThemeStore } from "../../../stores/use-theme-store";
@@ -41,6 +39,10 @@ import { TimeSince } from "../../ui/time-since";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
const navigateToTopic = (topic) => {
TopicNotes.navigate(topic, true);
};
function navigateToTag(item) {
const tag = db.tags.tag(item.id);
if (!tag) return;
@@ -53,48 +55,30 @@ const showActionSheet = (item) => {
function getNotebook(item) {
const isTrash = item.type === "trash";
const currentId = useNavigationStore.getState().currentScreen.id;
if (isTrash) return [];
const items = [];
const notebooks = db.relations.to(item, "notebook") || [];
if (isTrash || !item.notebooks || item.notebooks.length < 1) return [];
for (let notebook of notebooks) {
if (items.length > 1) break;
if (notebook.id === currentId) continue;
items.push(notebook);
}
return item.notebooks.reduce(function (prev, curr) {
if (prev && prev.length > 0) return prev;
const topicId = curr.topics[0];
const notebook = db.notebooks?.notebook(curr.id)?.data;
if (!notebook) return [];
const topic = notebook.topics.find((t) => t.id === topicId);
if (!topic) return [];
if (item.notebooks) {
for (let nb of item.notebooks) {
if (items.length > 1) break;
const notebook = db.notebooks?.notebook(nb.id)?.data;
if (!notebook) continue;
for (let topicId of nb.topics) {
if (items.length > 1) break;
if (topicId === currentId) continue;
const topic = notebook.topics.find((t) => t.id === topicId);
if (!topic) continue;
items.push(topic);
return [
{
title: `${notebook?.title} ${topic?.title}`,
notebook: notebook,
topic: topic
}
}
}
return items;
}
function getTags(item) {
const noteTags = item.tags?.slice(0, 3) || [];
const tags = [];
for (const tagName of noteTags) {
const tag = db.tags.tag(tagName);
if (!tag) continue;
tags.push(tag);
}
return tags;
];
}, []);
}
const NoteItem = ({
item,
isTrash,
tags,
dateBy = "dateCreated",
noOpen = false
}) => {
@@ -104,13 +88,11 @@ const NoteItem = ({
);
const compactMode = notesListMode === "compact";
const attachmentCount = db.attachments?.ofNote(item.id, "all")?.length || 0;
const _update = useRelationStore((state) => state.updater);
// eslint-disable-next-line react-hooks/exhaustive-deps
const notebooks = React.useMemo(() => getNotebook(item), [item, _update]);
const notebooks = React.useMemo(() => getNotebook(item), [item]);
const reminders = db.relations.from(item, "reminder");
const reminder = getUpcomingReminder(reminders);
const _update = useRelationStore((state) => state.updater);
const noteColor = COLORS_NOTE[item.color?.toLowerCase()];
const tags = getTags(item);
return (
<>
<View
@@ -130,17 +112,12 @@ const NoteItem = ({
flexWrap: "wrap"
}}
>
{notebooks?.map((item) => (
{notebooks?.map((_item) => (
<Button
title={
item.title.length > 25
? item.title.slice(0, 25) + "..."
: item.title
}
tooltipText={item.title}
key={item.id}
title={_item.title}
key={_item}
height={25}
icon={item.type === "topic" ? "bookmark" : "book-outline"}
icon="book-outline"
type="grayBg"
fontSize={SIZE.xs}
iconSize={SIZE.sm}
@@ -155,13 +132,7 @@ const NoteItem = ({
paddingHorizontal: 6,
marginBottom: 5
}}
onPress={() => {
if (item.type === "topic") {
TopicNotes.navigate(item, true);
} else {
Notebook.navigate(item);
}
}}
onPress={() => navigateToTopic(_item.topic)}
/>
))}
@@ -178,29 +149,16 @@ const NoteItem = ({
</View>
) : null}
{compactMode ? (
<Paragraph
numberOfLines={1}
color={COLORS_NOTE[item.color?.toLowerCase()] || colors.heading}
style={{
flexWrap: "wrap"
}}
size={SIZE.sm}
>
{item.title}
</Paragraph>
) : (
<Heading
numberOfLines={1}
color={COLORS_NOTE[item.color?.toLowerCase()] || colors.heading}
style={{
flexWrap: "wrap"
}}
size={SIZE.md}
>
{item.title}
</Heading>
)}
<Heading
numberOfLines={1}
color={COLORS_NOTE[item.color?.toLowerCase()] || colors.heading}
style={{
flexWrap: "wrap"
}}
size={SIZE.md}
>
{item.title}
</Heading>
{item.headline && !compactMode ? (
<Paragraph
@@ -215,223 +173,161 @@ const NoteItem = ({
</Paragraph>
) : null}
{compactMode ? null : (
<View
style={{
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center",
width: "100%",
marginTop: 5,
height: SIZE.md + 2
}}
>
{!isTrash ? (
<>
{item.conflicted ? (
<Icon
name="alert-circle"
style={{
marginRight: 6
}}
size={SIZE.sm}
color={colors.red}
/>
) : null}
<TimeSince
<View
style={{
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center",
width: "100%",
marginTop: 5,
height: SIZE.md + 2
}}
>
{!isTrash ? (
<>
{item.conflicted ? (
<Icon
name="alert-circle"
style={{
fontSize: SIZE.xs,
color: colors.icon,
marginRight: 6
}}
time={item[dateBy]}
updateFrequency={
Date.now() - item[dateBy] < 60000 ? 2000 : 60000
size={SIZE.sm}
color={colors.red}
/>
) : null}
<TimeSince
style={{
fontSize: SIZE.xs,
color: colors.icon,
marginRight: 6
}}
time={item[dateBy]}
updateFrequency={
Date.now() - item[dateBy] < 60000 ? 2000 : 60000
}
/>
{attachmentCount > 0 ? (
<View
style={{
flexDirection: "row",
alignItems: "center",
marginRight: 6
}}
>
<Icon name="attachment" size={SIZE.md} color={colors.icon} />
<Paragraph color={colors.icon} size={SIZE.xs}>
{attachmentCount}
</Paragraph>
</View>
) : null}
{item.pinned ? (
<Icon
testID="icon-pinned"
name="pin-outline"
size={SIZE.sm}
style={{
marginRight: 6
}}
color={
COLORS_NOTE[item.color?.toLowerCase()] || colors.accent
}
/>
) : null}
{attachmentCount > 0 ? (
<View
style={{
flexDirection: "row",
alignItems: "center",
marginRight: 6
}}
>
<Icon
name="attachment"
size={SIZE.md}
color={colors.icon}
/>
<Paragraph color={colors.icon} size={SIZE.xs}>
{attachmentCount}
</Paragraph>
</View>
) : null}
{item.pinned ? (
<Icon
testID="icon-pinned"
name="pin-outline"
size={SIZE.sm}
style={{
marginRight: 6
}}
color={
COLORS_NOTE[item.color?.toLowerCase()] || colors.accent
}
/>
) : null}
{item.locked ? (
<Icon
name="lock"
testID="note-locked-icon"
size={SIZE.sm}
style={{
marginRight: 6
}}
color={colors.icon}
/>
) : null}
{item.favorite ? (
<Icon
testID="icon-star"
name="star-outline"
size={SIZE.md}
style={{
marginRight: 6
}}
color="orange"
/>
) : null}
{!isTrash && !compactMode && tags
? tags.map((item) =>
item.id ? (
<Button
title={"#" + item.alias}
key={item.id}
height={23}
type="gray"
textStyle={{
textDecorationLine: "underline"
}}
hitSlop={{ top: 8, bottom: 12, left: 0, right: 0 }}
fontSize={SIZE.xs}
style={{
borderRadius: 5,
paddingHorizontal: 6,
marginRight: 4,
zIndex: 10,
maxWidth: tags.length > 1 ? 130 : null
}}
onPress={() => navigateToTag(item)}
/>
) : null
)
: null}
</>
) : (
<>
<Paragraph
{item.locked ? (
<Icon
name="lock"
testID="note-locked-icon"
size={SIZE.sm}
style={{
marginRight: 6
}}
color={colors.icon}
size={SIZE.xs}
style={{
marginRight: 6
}}
>
Deleted on{" "}
{item && item.dateDeleted
? new Date(item.dateDeleted).toISOString().slice(0, 10)
: null}
</Paragraph>
/>
) : null}
<Paragraph
color={colors.accent}
size={SIZE.xs}
{item.favorite ? (
<Icon
testID="icon-star"
name="star"
size={SIZE.md}
style={{
marginRight: 6
}}
>
{item.itemType[0].toUpperCase() + item.itemType.slice(1)}
</Paragraph>
</>
)}
</View>
)}
color="orange"
/>
) : null}
{!isTrash && !compactMode && tags
? tags.map((item) =>
item.id ? (
<Button
title={"#" + item.alias}
key={item.id}
height={23}
type="gray"
textStyle={{
textDecorationLine: "underline"
}}
hitSlop={{ top: 8, bottom: 12, left: 0, right: 0 }}
fontSize={SIZE.xs}
style={{
borderRadius: 5,
paddingHorizontal: 6,
marginRight: 4,
zIndex: 10,
maxWidth: tags.length > 1 ? 130 : null
}}
onPress={() => navigateToTag(item)}
/>
) : null
)
: null}
</>
) : (
<>
<Paragraph
color={colors.icon}
size={SIZE.xs}
style={{
marginRight: 6
}}
>
Deleted on{" "}
{item && item.dateDeleted
? new Date(item.dateDeleted).toISOString().slice(0, 10)
: null}
</Paragraph>
<Paragraph
color={colors.accent}
size={SIZE.xs}
style={{
marginRight: 6
}}
>
{item.itemType[0].toUpperCase() + item.itemType.slice(1)}
</Paragraph>
</>
)}
</View>
</View>
<View
style={{
flexDirection: "row",
<IconButton
testID={notesnook.listitem.menu}
color={colors.pri}
name="dots-horizontal"
size={SIZE.xl}
onPress={() => !noOpen && showActionSheet(item, isTrash)}
customStyle={{
justifyContent: "center",
height: 35,
width: 35,
borderRadius: 100,
alignItems: "center"
}}
>
{compactMode ? (
<>
{item.conflicted ? (
<Icon
name="alert-circle"
style={{
marginRight: 6
}}
size={SIZE.sm}
color={colors.red}
/>
) : null}
{item.locked ? (
<Icon
name="lock"
testID="note-locked-icon"
size={SIZE.sm}
style={{
marginRight: 6
}}
color={colors.icon}
/>
) : null}
{item.favorite ? (
<Icon
testID="icon-star"
name="star-outline"
size={SIZE.md}
style={{
marginRight: 6
}}
color="orange"
/>
) : null}
<TimeSince
style={{
fontSize: SIZE.xs,
color: colors.icon,
marginRight: 6
}}
time={item[dateBy]}
updateFrequency={Date.now() - item[dateBy] < 60000 ? 2000 : 60000}
/>
</>
) : null}
<IconButton
testID={notesnook.listitem.menu}
color={colors.pri}
name="dots-horizontal"
size={SIZE.xl}
onPress={() => !noOpen && showActionSheet(item, isTrash)}
customStyle={{
justifyContent: "center",
height: 35,
width: 35,
borderRadius: 100,
alignItems: "center"
}}
/>
</View>
/>
</>
);
};

View File

@@ -100,7 +100,7 @@ export const openNote = async (item, isTrash, setSelectedItem, isSheet) => {
};
export const NoteWrapper = React.memo(
function NoteWrapper({ item, index, dateBy, isSheet }) {
function NoteWrapper({ item, index, tags, dateBy, isSheet }) {
const isTrash = item.type === "trash";
const setSelectedItem = useSelectionStore((state) => state.setSelectedItem);
@@ -113,7 +113,7 @@ export const NoteWrapper = React.memo(
isSheet={isSheet}
item={item}
>
<NoteItem item={item} dateBy={dateBy} isTrash={isTrash} />
<NoteItem item={item} dateBy={dateBy} tags={tags} isTrash={isTrash} />
</SelectionWrapper>
);
},
@@ -125,6 +125,10 @@ export const NoteWrapper = React.memo(
return false;
}
if (JSON.stringify(prev.tags) !== JSON.stringify(next.tags)) {
return false;
}
if (prev.item !== next.item) {
return false;
}

View File

@@ -31,7 +31,6 @@ import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { getFormattedDate } from "../../../utils/time";
const showActionSheet = (item) => {
Properties.present(item);
@@ -64,28 +63,15 @@ export const NotebookItem = ({
flexShrink: 1
}}
>
{compactMode ? (
<Paragraph
size={SIZE.sm}
numberOfLines={1}
style={{
flexWrap: "wrap"
}}
>
{item.title}
</Paragraph>
) : (
<Heading
size={SIZE.md}
numberOfLines={1}
style={{
flexWrap: "wrap"
}}
>
{item.title}
</Heading>
)}
<Heading
size={SIZE.md}
numberOfLines={1}
style={{
flexWrap: "wrap"
}}
>
{item.title}
</Heading>
{isTopic || !item.description || compactMode ? null : (
<Paragraph
size={SIZE.sm}
@@ -136,51 +122,40 @@ export const NotebookItem = ({
</View>
)}
{!compactMode ? (
<View
style={{
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center",
marginTop: 5,
height: SIZE.md + 2
}}
>
{isTrash ? (
<>
<Paragraph
color={colors.icon}
size={SIZE.xs}
style={{
textAlignVertical: "center",
marginRight: 6
}}
>
{"Deleted on " +
new Date(item.dateDeleted).toISOString().slice(0, 10)}
</Paragraph>
<Paragraph
color={colors.accent}
size={SIZE.xs}
style={{
textAlignVertical: "center",
marginRight: 6
}}
>
{item.itemType[0].toUpperCase() + item.itemType.slice(1)}
</Paragraph>
</>
) : (
<View
style={{
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center",
marginTop: 5,
height: SIZE.md + 2
}}
>
{isTrash ? (
<>
<Paragraph
color={colors.icon}
size={SIZE.xs}
style={{
textAlignVertical: "center",
marginRight: 6
}}
>
{getFormattedDate(item[dateBy], "date")}
{"Deleted on " +
new Date(item.dateDeleted).toISOString().slice(0, 10)}
</Paragraph>
)}
<Paragraph
color={colors.accent}
size={SIZE.xs}
style={{
textAlignVertical: "center",
marginRight: 6
}}
>
{item.itemType[0].toUpperCase() + item.itemType.slice(1)}
</Paragraph>
</>
) : (
<Paragraph
color={colors.icon}
size={SIZE.xs}
@@ -188,66 +163,50 @@ export const NotebookItem = ({
marginRight: 6
}}
>
{item && totalNotes > 1
? totalNotes + " notes"
: totalNotes === 1
? totalNotes + " note"
: "0 notes"}
{new Date(item[dateBy]).toDateString().substring(4)}
</Paragraph>
)}
<Paragraph
color={colors.icon}
size={SIZE.xs}
style={{
marginRight: 6
}}
>
{item && totalNotes > 1
? totalNotes + " notes"
: totalNotes === 1
? totalNotes + " note"
: "0 notes"}
</Paragraph>
{item.pinned ? (
<Icon
name="pin-outline"
size={SIZE.sm}
style={{
marginRight: 10,
marginTop: 2
}}
color={colors.accent}
/>
) : null}
</View>
) : null}
{item.pinned ? (
<Icon
name="pin-outline"
size={SIZE.sm}
style={{
marginRight: 10,
marginTop: 2
}}
color={colors.accent}
/>
) : null}
</View>
</View>
<View
style={{
flexDirection: "row",
<IconButton
color={colors.heading}
name="dots-horizontal"
testID={notesnook.ids.notebook.menu}
size={SIZE.xl}
onPress={() => showActionSheet(item)}
customStyle={{
justifyContent: "center",
height: 35,
width: 35,
borderRadius: 100,
alignItems: "center"
}}
>
{compactMode ? (
<>
<Paragraph
color={colors.icon}
size={SIZE.xs}
style={{
marginRight: 6
}}
>
{item && totalNotes > 1
? totalNotes + " notes"
: totalNotes === 1
? totalNotes + " note"
: "0 notes"}
</Paragraph>
</>
) : null}
<IconButton
color={colors.heading}
name="dots-horizontal"
testID={notesnook.ids.notebook.menu}
size={SIZE.xl}
onPress={() => showActionSheet(item)}
customStyle={{
justifyContent: "center",
height: 35,
width: 35,
borderRadius: 100,
alignItems: "center"
}}
/>
</View>
/>
</>
);
};

View File

@@ -64,7 +64,16 @@ export const openNotebookTopic = (item) => {
negativeText: "Delete",
positivePress: async () => {
await db.trash.restore(item.id);
Navigation.queueRoutesForUpdate();
Navigation.queueRoutesForUpdate(
"Tags",
"Notes",
"Notebooks",
"Favorites",
"Trash",
"TaggedNotes",
"ColoredNotes",
"TopicNotes"
);
useSelectionStore.getState().setSelectionMode(false);
ToastEvent.show({
heading: "Restore successful",

View File

@@ -108,7 +108,7 @@ const ReminderItem = React.memo(
color={colors.errorText}
/>
<Paragraph
size={SIZE.xs}
size={SIZE.xs + 1}
color={colors.icon}
style={{ marginLeft: 5 }}
>
@@ -133,7 +133,7 @@ const ReminderItem = React.memo(
>
<Icon name="reload" size={SIZE.md} color={colors.accent} />
<Paragraph
size={SIZE.xs}
size={SIZE.xs + 1}
color={colors.icon}
style={{ marginLeft: 5 }}
>
@@ -147,7 +147,7 @@ const ReminderItem = React.memo(
<ReminderTime
reminder={item}
checkIsActive={false}
fontSize={SIZE.xs}
fontSize={SIZE.xs + 1}
style={{
justifyContent: "flex-start",
borderWidth: 0,

View File

@@ -42,16 +42,11 @@ const SelectionWrapper = ({
const notesListMode = useSettingStore(
(state) => state.settings.notesListMode
);
const listMode =
item.type === "notebook" || item.itemType === "notebook"
? notebooksListMode
: notesListMode;
const listMode = item.type === "notebook" ? notebooksListMode : notesListMode;
const compactMode =
(item.type === "notebook" ||
item.itemType === "notebook" ||
item.itemType === "note" ||
item.type === "note") &&
(item.type === "notebook" || item.type === "note") &&
listMode === "compact";
if (item.id !== itemId.current) {
itemId.current = item.id;
}
@@ -82,7 +77,7 @@ const SelectionWrapper = ({
width: "100%",
overflow: "hidden",
paddingHorizontal: 12,
paddingVertical: compactMode ? 4 : 12,
paddingVertical: compactMode ? 8 : 12,
borderRadius: isSheet ? 10 : 0,
marginBottom: isSheet ? 12 : undefined
}}

View File

@@ -23,7 +23,6 @@ import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { useThemeStore } from "../../../stores/use-theme-store";
import { SIZE } from "../../../utils/size";
import { useSettingStore } from "../../../stores/use-setting-store";
export const SelectionIcon = ({ item }) => {
const colors = useThemeStore((state) => state.colors);
@@ -33,17 +32,6 @@ export const SelectionIcon = ({ item }) => {
(state) => state.selectedItemsList
);
const [selected, setSelected] = useState(false);
const notebooksListMode = useSettingStore(
(state) => state.settings.notebooksListMode
);
const notesListMode = useSettingStore(
(state) => state.settings.notesListMode
);
const listMode = item.type === "notebook" ? notebooksListMode : notesListMode;
const compactMode =
(item.type === "notebook" || item.type === "note") &&
listMode === "compact";
useEffect(() => {
if (selectionMode) {
@@ -66,8 +54,8 @@ export const SelectionIcon = ({ item }) => {
return selectionMode ? (
<View
style={{
width: compactMode ? 30 : 40,
height: compactMode ? 30 : 40,
width: 40,
height: 40,
justifyContent: "center",
alignItems: "center",
marginRight: 10,
@@ -79,7 +67,7 @@ export const SelectionIcon = ({ item }) => {
>
{selected ? (
<Icon
size={compactMode ? SIZE.xl - 2 : SIZE.xl}
size={SIZE.xl}
color={selected ? colors.accent : colors.icon}
name={"check"}
/>

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useRef } from "react";
import React, { useRef } from "react";
import { RefreshControl, View } from "react-native";
import { FlashList } from "@shopify/flash-list";
import Animated, { FadeInDown } from "react-native-reanimated";
@@ -57,10 +57,24 @@ const RenderItem = ({ item, index, type, ...restArgs }) => {
const dateBy =
groupOptions.sortBy !== "title" ? groupOptions.sortBy : "dateEdited";
const totalNotes = getTotalNotes(item);
const tags =
item.tags
?.slice(0, 3)
?.map((item) => {
let tag = db.tags.tag(item);
if (!tag) return null;
return {
title: tag.title,
id: tag.id,
alias: tag.alias
};
})
.filter((t) => t !== null) || [];
return (
<Item
item={item}
tags={tags}
dateBy={dateBy}
index={index}
type={type}
@@ -89,9 +103,7 @@ const List = ({
ListHeader,
warning,
isSheet = false,
onMomentumScrollEnd,
handlers,
ScrollComponent
onMomentumScrollEnd
}) => {
const colors = useThemeStore((state) => state.colors);
const scrollRef = useRef();
@@ -139,21 +151,14 @@ const List = ({
[screen]
);
useEffect(() => {
eSendEvent(eScrollEvent, {
y: 0,
screen
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
let styles = {
width: "100%",
minHeight: 1,
minWidth: 1
};
const ListView = ScrollComponent ? ScrollComponent : FlashList;
const _keyExtractor = (item) => item.id || item.title;
return (
<>
<Animated.View
@@ -162,8 +167,7 @@ const List = ({
}}
entering={type === "search" ? undefined : FadeInDown}
>
<ListView
{...handlers}
<FlashList
style={styles}
ref={scrollRef}
testID={notesnook.list.id}
@@ -218,14 +222,12 @@ const List = ({
}
/>
</Animated.View>
{listData ? (
<JumpToSectionDialog
screen={screen}
data={listData}
type={screen === "Notes" ? "home" : type}
scrollRef={scrollRef}
/>
) : null}
<JumpToSectionDialog
screen={screen}
data={listData}
type={screen === "Notes" ? "home" : type}
scrollRef={scrollRef}
/>
</>
);
};

View File

@@ -37,7 +37,7 @@ import { useThemeStore } from "../../stores/use-theme-store";
import { dHeight } from "../../utils";
import { eOnLoadNote, eShowMergeDialog } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { getFormattedDate, sleep } from "../../utils/time";
import { sleep, timeConverter } from "../../utils/time";
import BaseDialog from "../dialog/base-dialog";
import DialogButtons from "../dialog/dialog-buttons";
import DialogContainer from "../dialog/dialog-container";
@@ -85,7 +85,13 @@ const MergeConflicts = () => {
}
});
}
Navigation.queueRoutesForUpdate();
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes"
);
if (editorController.current?.note?.id === note.id) {
// reload the note in editor
eSendEvent(eOnLoadNote, {
@@ -166,7 +172,7 @@ const MergeConflicts = () => {
{isCurrent ? "(This Device)" : "(Incoming)"}
</Text>
{"\n"}
{getFormattedDate(contentToKeep?.dateEdited)}
{timeConverter(contentToKeep?.dateEdited)}
</Paragraph>
</View>

View File

@@ -26,7 +26,7 @@ import { presentSheet } from "../../services/event-manager";
import { useThemeStore } from "../../stores/use-theme-store";
import { openLinkInBrowser } from "../../utils/functions";
import { SIZE } from "../../utils/size";
import { getFormattedDate, timeSince } from "../../utils/time";
import { timeConverter, timeSince } from "../../utils/time";
import DialogHeader from "../dialog/dialog-header";
import SheetProvider from "../sheet-provider";
import { PressableButton } from "../ui/pressable";
@@ -63,15 +63,15 @@ export default function NoteHistory({ note, fwdRef }) {
}, []);
const getDate = (start, end) => {
let _start_date = getFormattedDate(start, "date");
let _end_date = getFormattedDate(end + 60000, "date");
let _start_time = getFormattedDate(start, "time");
let _end_time = getFormattedDate(end + 60000, "time");
return `${_start_date} ${_start_time} - ${
_end_date === _start_date ? " " : _end_date + " "
}${_end_time}`;
let _start = timeConverter(start);
let _end = timeConverter(end + 60000);
if (_start === _end) return _start;
let final = _end.lastIndexOf(",");
let part = _end.slice(0, final + 1);
if (_start.includes(part)) {
return _start + " —" + _end.replace(part, "");
}
return _start + " — " + _end;
};
const renderItem = useCallback(

View File

@@ -44,7 +44,16 @@ export default function NotePreview({ session, content, note }) {
async function restore() {
if (note && note.type === "trash") {
await db.trash.restore(note.id);
Navigation.queueRoutesForUpdate();
Navigation.queueRoutesForUpdate(
"Tags",
"Notes",
"Notebooks",
"Favorites",
"Trash",
"TaggedNotes",
"ColoredNotes",
"TopicNotes"
);
useSelectionStore.getState().setSelectionMode(false);
ToastEvent.show({
heading: "Restore successful",
@@ -64,7 +73,13 @@ export default function NotePreview({ session, content, note }) {
}
eSendEvent(eCloseSheet, "note_history");
eSendEvent(eCloseSheet);
Navigation.queueRoutesForUpdate();
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes"
);
ToastEvent.show({
heading: "Note restored successfully",

View File

@@ -18,13 +18,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { ScrollView } from "react-native";
import { FeatureBlock } from "./feature";
import { ScrollView } from "react-native-actions-sheet";
export const CompactFeatures = ({
vertical,
features = [],
maxHeight = 600,
maxHeight = 500,
scrollRef
}) => {
let data = vertical
@@ -80,11 +80,14 @@ export const CompactFeatures = ({
return (
<ScrollView
horizontal={!vertical}
nestedScrollEnabled
onMomentumScrollEnd={() => {
scrollRef?.current?.handleChildScrollEnd();
}}
showsHorizontalScrollIndicator={false}
style={{
width: "100%",
maxHeight: maxHeight,
paddingHorizontal: 12
maxHeight: maxHeight
}}
>
{data.map((item) => (

View File

@@ -143,7 +143,7 @@ export const Expiring = () => {
await sleep(300);
eSendEvent(eOpenPremiumDialog, promo);
}}
size={SIZE.xs}
size={SIZE.xs + 1}
style={{
textDecorationLine: "underline",
color: colors.icon,

View File

@@ -41,19 +41,18 @@ export const FeatureBlock = ({
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 12,
marginBottom: 10,
backgroundColor: colors.nav,
borderRadius: 10,
paddingVertical: 12
marginBottom: 10
}}
>
<Icon color={colors.accent} name="check" size={SIZE.lg} />
<Paragraph
style={{
flexWrap: "wrap",
marginLeft: 5,
flexShrink: 1
}}
size={SIZE.sm}
size={SIZE.md}
>
{content}
</Paragraph>

View File

@@ -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 React, { useCallback, useEffect, useRef, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { View } from "react-native";
import Animated, { FadeInUp, FadeOutUp } from "react-native-reanimated";
import useKeyboard from "../../hooks/use-keyboard";
import { editorState } from "../../screens/editor/tiptap/utils";
import { DDS } from "../../services/device-detection";
import {
eSendEvent,
@@ -31,7 +31,6 @@ import { useThemeStore } from "../../stores/use-theme-store";
import { getElevation } from "../../utils";
import {
eCloseActionSheet,
eCloseSheet,
eOpenPremiumDialog,
eShowGetPremium
} from "../../utils/events";
@@ -40,12 +39,12 @@ import { sleep } from "../../utils/time";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { useCallback } from "react";
export const PremiumToast = ({ context = "global", offset = 0 }) => {
const colors = useThemeStore((state) => state.colors);
const [msg, setMsg] = useState(null);
const timer = useRef();
const keyboard = useKeyboard();
const open = useCallback(
(event) => {
@@ -73,6 +72,7 @@ export const PremiumToast = ({ context = "global", offset = 0 }) => {
useEffect(() => {
eSubscribeEvent(eShowGetPremium, open);
return () => {
clearTimeout(timer.current);
eUnSubscribeEvent(eShowGetPremium, open);
};
}, [open]);
@@ -80,7 +80,9 @@ export const PremiumToast = ({ context = "global", offset = 0 }) => {
const onPress = async () => {
open(null);
eSendEvent(eCloseActionSheet);
eSendEvent(eCloseSheet);
if (editorState().isFocused) {
//tiny.call(EditorWebView, tiny.blur);
}
await sleep(300);
eSendEvent(eOpenPremiumDialog);
};
@@ -100,13 +102,9 @@ export const PremiumToast = ({ context = "global", offset = 0 }) => {
flexDirection: "row",
alignSelf: "center",
justifyContent: "space-between",
top: offset + keyboard.keyboardHeight,
top: offset,
maxWidth: DDS.isLargeTablet() ? 400 : "98%"
}}
onTouchEnd={() => {
setMsg(null);
clearTimeout(timer.current);
}}
>
<View
style={{

View File

@@ -232,7 +232,8 @@ export const PricingPlans = ({
width={250}
style={{
paddingHorizontal: 12,
marginBottom: 15
marginBottom: 15,
borderRadius: 100
}}
/>
</>
@@ -397,7 +398,8 @@ export const PricingPlans = ({
style={{
paddingHorizontal: 12,
marginTop: 30,
marginBottom: 10
marginBottom: 10,
borderRadius: 100
}}
/>
{Platform.OS !== "ios" &&

View File

@@ -43,7 +43,13 @@ export const ColorTags = ({ item }) => {
let _note = db.notes.note(note.id).data;
setNote({ ..._note });
setColorNotes();
Navigation.queueRoutesForUpdate();
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes"
);
eSendEvent(refreshNotesPage);
};
@@ -83,7 +89,6 @@ export const ColorTags = ({ item }) => {
flexWrap: "wrap",
flexGrow: isTablet ? undefined : 1,
paddingHorizontal: 12,
paddingRight: 0,
alignItems: "center",
justifyContent: isTablet ? "center" : "space-between"
}}

View File

@@ -21,7 +21,7 @@ import React from "react";
import { View } from "react-native";
import { useThemeStore } from "../../stores/use-theme-store";
import { SIZE } from "../../utils/size";
import { getFormattedDate } from "../../utils/time";
import { timeConverter } from "../../utils/time";
import Paragraph from "../ui/typography/paragraph";
export const DateMeta = ({ item }) => {
const colors = useThemeStore((state) => state.colors);
@@ -62,11 +62,11 @@ export const DateMeta = ({ item }) => {
paddingVertical: 3
}}
>
<Paragraph size={SIZE.xs} color={colors.icon}>
<Paragraph size={SIZE.xs + 1} color={colors.icon}>
{getNameFromKey(key)}
</Paragraph>
<Paragraph size={SIZE.xs} color={colors.icon}>
{getFormattedDate(item[key], "date-time")}
<Paragraph size={SIZE.xs + 1} color={colors.icon}>
{timeConverter(item[key])}
</Paragraph>
</View>
);

View File

@@ -16,9 +16,10 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { Platform, View } from "react-native";
import { FlatList } from "react-native-actions-sheet";
import { ScrollView } from "react-native-gesture-handler";
import { db } from "../../common/database";
import { DDS } from "../../services/device-detection";
import { presentSheet } from "../../services/event-manager";
@@ -36,6 +37,7 @@ import { Items } from "./items";
import Notebooks from "./notebooks";
import { Synced } from "./synced";
import { Tags, TagStrip } from "./tags";
const Line = ({ top = 6, bottom = 6 }) => {
const colors = useThemeStore((state) => state.colors);
return (
@@ -51,11 +53,20 @@ const Line = ({ top = 6, bottom = 6 }) => {
);
};
export const Properties = ({ close = () => {}, item, buttons = [] }) => {
export const Properties = ({
close = () => {},
item,
buttons = [],
getRef
}) => {
const colors = useThemeStore((state) => state.colors);
const alias = item.alias || item.title;
const isColor = !!COLORS_NOTE[item.title];
const onScrollEnd = () => {
getRef().current?.handleChildScrollEnd();
};
if (!item || !item.id) {
return (
<Paragraph style={{ marginVertical: 10, alignSelf: "center" }}>
@@ -64,92 +75,89 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
);
}
return (
<FlatList
<ScrollView
nestedScrollEnabled
onMomentumScrollEnd={onScrollEnd}
keyboardShouldPersistTaps="always"
keyboardDismissMode="none"
style={{
backgroundColor: colors.bg,
paddingHorizontal: 0,
borderBottomRightRadius: DDS.isLargeTablet() ? 10 : 1,
borderBottomLeftRadius: DDS.isLargeTablet() ? 10 : 1,
maxHeight: "100%"
}}
data={[0]}
keyExtractor={() => "properties-scroll-item"}
renderItem={() => (
<View>
<View
style={{
paddingHorizontal: 12,
marginTop: 5,
zIndex: 10
}}
>
<Heading size={SIZE.lg}>
{item.type === "tag" && !isColor ? (
<Heading size={SIZE.xl} color={colors.accent}>
#
</Heading>
) : null}
{alias}
>
<View
style={{
paddingHorizontal: 12,
marginTop: 5,
zIndex: 10
}}
>
<Heading size={SIZE.lg}>
{item.type === "tag" && !isColor ? (
<Heading size={SIZE.xl} color={colors.accent}>
#
</Heading>
{item.type === "note" ? (
<TagStrip close={close} item={item} />
) : null}
{item.type === "reminder" ? (
<ReminderTime
reminder={item}
style={{
justifyContent: "flex-start",
borderWidth: 0,
height: 30,
alignSelf: "flex-start",
backgroundColor: "transparent",
paddingHorizontal: 0
}}
fontSize={SIZE.xs}
/>
) : null}
</View>
<Line top={12} />
<DateMeta item={item} />
<Line bottom={0} />
{item.type === "note" ? <Tags close={close} item={item} /> : null}
<View
style={{
paddingHorizontal: 12
}}
>
<Notebooks note={item} close={close} />
</View>
<Items
item={item}
buttons={buttons}
close={() => {
close();
setTimeout(() => {
SearchService.updateAndSearch();
}, 1000);
}}
/>
<Synced item={item} close={close} />
<DevMode item={item} />
{DDS.isTab ? (
<View
style={{
height: 20
}}
/>
) : null}
<SheetProvider context="properties" />
</View>
)}
/>
{alias}
</Heading>
{item.type === "note" ? <TagStrip close={close} item={item} /> : null}
{item.type === "reminder" ? (
<ReminderTime
reminder={item}
style={{
justifyContent: "flex-start",
borderWidth: 0,
height: 30,
alignSelf: "flex-start",
backgroundColor: "transparent",
paddingHorizontal: 0
}}
fontSize={SIZE.xs + 1}
/>
) : null}
</View>
<Line top={12} />
<DateMeta item={item} />
<Line bottom={0} />
{item.type === "note" ? <Tags close={close} item={item} /> : null}
<View
style={{
paddingHorizontal: 12
}}
>
{item.notebooks ? <Notebooks note={item} close={close} /> : null}
</View>
<Items
item={item}
buttons={buttons}
close={() => {
close();
setTimeout(() => {
SearchService.updateAndSearch();
}, 1000);
}}
/>
<Synced item={item} close={close} />
<DevMode item={item} />
{DDS.isTab ? (
<View
style={{
height: 20
}}
/>
) : null}
<SheetProvider context="properties" />
</ScrollView>
);
};
@@ -179,7 +187,6 @@ Properties.present = (item, buttons = [], isSheet) => {
"lock-unlock",
"trash",
"remove-from-topic",
"remove-from-notebook",
"history",
"read-only",
"reminders",
@@ -191,25 +198,13 @@ Properties.present = (item, buttons = [], isSheet) => {
break;
case "notebook":
props[0] = db.notebooks.notebook(item.id).data;
props.push([
"edit-notebook",
"pin",
"add-shortcut",
"trash",
"default-notebook"
]);
props.push(["edit-notebook", "pin", "add-shortcut", "trash"]);
break;
case "topic":
props[0] = db.notebooks
.notebook(item.notebookId)
.topics.topic(item.id)._topic;
props.push([
"move-notes",
"edit-topic",
"add-shortcut",
"trash",
"default-topic"
]);
props.push(["move-notes", "edit-topic", "add-shortcut", "trash"]);
break;
case "tag":
props[0] = db.tags.tag(item.id);
@@ -229,7 +224,7 @@ Properties.present = (item, buttons = [], isSheet) => {
close={() => {
close();
}}
actionSheetRef={ref}
getRef={() => ref}
item={props[0]}
buttons={props[1]}
/>

View File

@@ -18,7 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { FlatList, ScrollView, View } from "react-native";
import { View } from "react-native";
import { FlatList } from "react-native-gesture-handler";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { useActions } from "../../hooks/use-actions";
import { DDS } from "../../services/device-detection";
@@ -84,9 +85,8 @@ export const Items = ({ item, buttons, close }) => {
</View>
);
const renderColumnItem = (item) => (
const renderColumnItem = ({ item }) => (
<Button
key={item.name + item.title}
buttonType={{
text: item.on
? colors.accent
@@ -108,19 +108,18 @@ export const Items = ({ item, buttons, close }) => {
/>
);
const renderTopBarItem = (item, index) => {
const isLast = index === topBarItems.length;
const renderTopBarItem = ({ item }) => {
return (
<PressableButton
onPress={item.func}
key={item.id}
testID={"icon-" + item.id}
activeOpacity={1}
customStyle={{
alignSelf: "flex-start",
marginRight: isLast ? 0 : 10,
paddingHorizontal: 0,
width: topBarItemWidth
alignItems: "center",
width: topBarItemWidth,
marginBottom: 10,
marginRight: 10,
backgroundColor: "transparent"
}}
>
<PressableButton
@@ -150,7 +149,11 @@ export const Items = ({ item, buttons, close }) => {
/>
</PressableButton>
<Paragraph size={SIZE.xxs + 1} style={{ textAlign: "center" }}>
<Paragraph
size={SIZE.xxs + 1}
style={{ textAlign: "center" }}
textBreakStrategy="simple"
>
{item.title}
</Paragraph>
</PressableButton>
@@ -166,7 +169,7 @@ export const Items = ({ item, buttons, close }) => {
"lock-unlock",
"publish"
];
const topBarItems = data.filter(
const bottomBarItems = data.filter(
(item) => topBarItemsList.indexOf(item.id) > -1
);
@@ -175,19 +178,21 @@ export const Items = ({ item, buttons, close }) => {
);
const topBarItemWidth =
(width - (topBarItems.length * 10 + 14)) / topBarItems.length;
(width - (bottomBarItems.length * 10 + 14)) / bottomBarItems.length;
return item.type === "note" ? (
<>
<ScrollView
<FlatList
data={bottomBarItems}
keyExtractor={(item) => item.title}
horizontal
disableVirtualization={true}
style={{
paddingHorizontal: 12,
paddingVertical: 12
paddingTop: 12
}}
>
{topBarItems.map(renderTopBarItem)}
</ScrollView>
renderItem={renderTopBarItem}
/>
<FlatList
data={bottomGridItems}
@@ -197,8 +202,10 @@ export const Items = ({ item, buttons, close }) => {
disableVirtualization={true}
style={{
marginTop: item.type !== "note" ? 10 : 0,
paddingTop: 10,
marginLeft: 6
paddingTop: 10
}}
columnWrapperStyle={{
justifyContent: "flex-start"
}}
contentContainerStyle={{
alignSelf: "center",
@@ -209,6 +216,11 @@ export const Items = ({ item, buttons, close }) => {
/>
</>
) : (
<View data={data}>{data.map(renderColumnItem)}</View>
<FlatList
data={data}
keyExtractor={(item) => item.title}
renderItem={renderColumnItem}
disableVirtualization={true}
/>
);
};

View File

@@ -23,33 +23,20 @@ import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import Notebook from "../../screens/notebook";
import { TopicNotes } from "../../screens/notes/topic-notes";
import {
eSendEvent,
presentSheet,
ToastEvent
} from "../../services/event-manager";
import { presentSheet, ToastEvent } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { useNotebookStore } from "../../stores/use-notebook-store";
import { useThemeStore } from "../../stores/use-theme-store";
import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import { eClearEditor } from "../../utils/events";
export default function Notebooks({ note, close, full }) {
const colors = useThemeStore((state) => state.colors);
const notebooks = useNotebookStore((state) => state.notebooks);
function getNotebooks(item) {
if (!item.notebooks || item.notebooks.length < 1) return [];
let filteredNotebooks = [];
const relations = db.relations.to(note, "notebook");
filteredNotebooks.push(
...relations.map((notebook) => ({
...notebook,
topics: []
}))
);
if (!item.notebooks || item.notebooks.length < 1) return filteredNotebooks;
for (let notebookReference of item.notebooks) {
let notebook = {
...(notebooks.find((item) => item.id === notebookReference.id) || {})
@@ -58,14 +45,7 @@ export default function Notebooks({ note, close, full }) {
notebook.topics = notebook.topics.filter((topic) => {
return notebookReference.topics.findIndex((t) => t === topic.id) > -1;
});
const index = filteredNotebooks.findIndex(
(item) => item.id === notebook.id
);
if (index > -1) {
filteredNotebooks[index].topics = notebook.topics;
} else {
filteredNotebooks.push(notebook);
}
filteredNotebooks.push(notebook);
}
}
return filteredNotebooks;
@@ -83,6 +63,7 @@ export default function Notebooks({ note, close, full }) {
if (!item) return;
TopicNotes.navigate(item, true);
};
const renderItem = (item) => (
<View
key={item.id}
@@ -97,8 +78,7 @@ export default function Notebooks({ note, close, full }) {
borderWidth: full ? 0 : 1,
borderColor: colors.nav,
borderRadius: 10,
backgroundColor: full ? "transparent" : colors.nav,
minHeight: 42
backgroundColor: full ? "transparent" : colors.nav
}}
>
<Icon
@@ -117,7 +97,6 @@ export default function Notebooks({ note, close, full }) {
size={SIZE.sm}
onPress={() => {
navigateNotebook(item.id);
eSendEvent(eClearEditor);
close();
}}
>
@@ -140,7 +119,6 @@ export default function Notebooks({ note, close, full }) {
key={topic.id}
onPress={() => {
navigateTopic(topic.id, item.id);
eSendEvent(eClearEditor);
close();
}}
onLongPress={async () => {
@@ -152,7 +130,13 @@ export default function Notebooks({ note, close, full }) {
note
);
useNotebookStore.getState().setNotebooks();
Navigation.queueRoutesForUpdate();
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes"
);
ToastEvent.show({
heading: "Note removed from topic",
context: "local",
@@ -162,7 +146,7 @@ export default function Notebooks({ note, close, full }) {
title={topic.title}
type="gray"
height={30}
fontSize={SIZE.xs}
fontSize={SIZE.xs + 1}
icon="bookmark-outline"
style={{
marginRight: 5,
@@ -176,7 +160,7 @@ export default function Notebooks({ note, close, full }) {
</View>
);
return noteNotebooks.length === 0 ? null : (
return !note.notebooks || note.notebooks.length === 0 ? null : (
<View
style={{
width: "100%",
@@ -191,7 +175,7 @@ export default function Notebooks({ note, close, full }) {
{noteNotebooks.length > 1 && !full ? (
<Button
title={`See all linked notebooks`}
fontSize={SIZE.xs}
fontSize={SIZE.xs + 1}
style={{
alignSelf: "flex-end",
marginRight: 12,

View File

@@ -91,10 +91,10 @@ export const Synced = ({ item, close }) => {
console.error(e);
}
}}
fontSize={SIZE.xs}
fontSize={SIZE.xs + 1}
title="Learn more"
height={30}
type="grayAccent"
type="transparent"
/>
</View>
) : null;

View File

@@ -21,10 +21,11 @@ import React from "react";
import { View } from "react-native";
import { db } from "../../common/database";
import { TaggedNotes } from "../../screens/notes/tagged";
import { eSendEvent } from "../../services/event-manager";
import { useThemeStore } from "../../stores/use-theme-store";
import { eOpenTagsDialog } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import ManageTagsSheet from "../sheets/manage-tags";
import { Button } from "../ui/button";
import { ColorTags } from "./color-tags";
export const Tags = ({ item, close }) => {
@@ -40,14 +41,14 @@ export const Tags = ({ item, close }) => {
flexWrap: "wrap",
alignItems: "center",
paddingHorizontal: 12,
alignSelf: "center",
justifyContent: "space-between",
width: "100%"
alignSelf: "center"
}}
>
<Button
onPress={async () => {
ManageTagsSheet.present([item]);
close();
await sleep(300);
eSendEvent(eOpenTagsDialog, item);
}}
buttonType={{
text: colors.accent
@@ -57,7 +58,7 @@ export const Tags = ({ item, close }) => {
icon="plus"
iconPosition="right"
height={30}
fontSize={SIZE.xs}
fontSize={SIZE.xs + 1}
style={{
marginRight: 5,
borderRadius: 100,
@@ -78,8 +79,8 @@ export const TagStrip = ({ item, close }) => {
alignItems: "center"
}}
>
{item.tags.map((tag) =>
tag ? <TagItem key={tag} tag={tag} close={close} /> : null
{item.tags.map((item) =>
item ? <TagItem key={item} tag={item} close={close} /> : null
)}
</View>
) : null;
@@ -101,13 +102,14 @@ const TagItem = ({ tag, close }) => {
marginTop: 0,
backgroundColor: "transparent"
};
return (
<Button
onPress={onPress}
title={"#" + tag}
type="grayBg"
height={20}
fontSize={SIZE.xs}
fontSize={SIZE.xs + 1}
style={style}
textStyle={{
textDecorationLine: "underline"

View File

@@ -39,7 +39,7 @@ export const Topics = ({ item, close }) => {
height={30}
onPress={() => open(topic)}
icon="bookmark-outline"
fontSize={SIZE.xs}
fontSize={SIZE.xs + 1}
style={{
marginRight: 5,
paddingHorizontal: 8,

View File

@@ -21,22 +21,21 @@ import React, { useCallback, useEffect } from "react";
import { BackHandler, Platform, View } from "react-native";
import { db } from "../../common/database";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { ToastEvent } from "../../services/event-manager";
import { eSendEvent, ToastEvent } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import SearchService from "../../services/search";
import useNavigationStore from "../../stores/use-navigation-store";
import { useSelectionStore } from "../../stores/use-selection-store";
import { useThemeStore } from "../../stores/use-theme-store";
import { eOpenMoveNoteDialog } from "../../utils/events";
import { deleteItems } from "../../utils/functions";
import { tabBarRef } from "../../utils/global-refs";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { presentDialog } from "../dialog/functions";
import MoveNoteSheet from "../sheets/add-to";
import ExportNotesSheet from "../sheets/export-notes";
import { IconButton } from "../ui/icon-button";
import Heading from "../ui/typography/heading";
import ManageTagsSheet from "../sheets/manage-tags";
export const SelectionHeader = React.memo(() => {
const colors = useThemeStore((state) => state.colors);
@@ -65,7 +64,13 @@ export const SelectionHeader = React.memo(() => {
selectedItemsList.forEach((item) => {
db.notes.note(item.id).favorite();
});
Navigation.queueRoutesForUpdate();
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes"
);
clearSelection();
}
};
@@ -77,7 +82,16 @@ export const SelectionHeader = React.memo(() => {
noteIds.push(item.id);
});
await db.trash.restore(...noteIds);
Navigation.queueRoutesForUpdate();
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes",
"Trash",
"Notebooks",
"Tags"
);
clearSelection();
ToastEvent.show({
@@ -104,7 +118,16 @@ export const SelectionHeader = React.memo(() => {
noteIds.push(item.id);
});
await db.trash.delete(...noteIds);
Navigation.queueRoutesForUpdate();
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes",
"Trash",
"Notebooks",
"Tags"
);
clearSelection();
}
},
@@ -178,7 +201,7 @@ export const SelectionHeader = React.memo(() => {
}}
>
<Heading size={SIZE.md} color={colors.accent}>
{selectedItemsList.length}
{selectedItemsList.length + " Selected"}
</Heading>
</View>
</View>
@@ -210,25 +233,11 @@ export const SelectionHeader = React.memo(() => {
screen === "Notebook" ||
screen === "Reminders" ? null : (
<>
<IconButton
onPress={async () => {
await sleep(100);
ManageTagsSheet.present(selectedItemsList);
}}
customStyle={{
marginLeft: 10
}}
color={colors.pri}
tooltipText="Manage tags"
tooltipPosition={4}
name="pound"
size={SIZE.xl}
/>
<IconButton
onPress={async () => {
//setSelectionMode(false);
await sleep(100);
MoveNoteSheet.present();
eSendEvent(eOpenMoveNoteDialog);
}}
customStyle={{
marginLeft: 10
@@ -239,7 +248,6 @@ export const SelectionHeader = React.memo(() => {
name="plus"
size={SIZE.xl}
/>
<IconButton
onPress={async () => {
ExportNotesSheet.present(selectedItemsList);
@@ -256,40 +264,36 @@ export const SelectionHeader = React.memo(() => {
</>
)}
{screen === "TopicNotes" || screen === "Notebook" ? (
{screen === "TopicNotes" ? (
<IconButton
onPress={async () => {
if (selectedItemsList.length > 0) {
const currentScreen =
const currentTopic =
useNavigationStore.getState().currentScreen;
await db.notes.removeFromNotebook(
{
id: currentTopic.notebookId,
topic: currentTopic.id
},
...selectedItemsList.map((item) => item.id)
);
if (screen === "Notebook") {
for (const item of selectedItemsList) {
await db.relations.unlink(
{ type: "notebook", id: currentScreen.id },
item
);
}
} else {
await db.notes.removeFromNotebook(
{
id: currentScreen.notebookId,
topic: currentScreen.id
},
...selectedItemsList.map((item) => item.id)
);
}
Navigation.queueRoutesForUpdate();
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes",
"Notebooks",
"Notebook"
);
clearSelection();
}
}}
customStyle={{
marginLeft: 10
}}
tooltipText={`Remove from ${
screen === "Notebook" ? "notebook" : "topic"
}`}
tooltipText="Remove from topic"
tooltipPosition={4}
testID="select-minus"
color={colors.pri}

View File

@@ -36,7 +36,7 @@ import Paragraph from "../ui/typography/paragraph";
const SheetProvider = ({ context = "global" }) => {
const colors = useThemeStore((state) => state.colors);
const [visible, setVisible] = useState(false);
const [data, setData] = useState(null);
const [dialogData, setDialogData] = useState(null);
const actionSheetRef = useRef();
const editor = useRef({
refocus: false
@@ -52,35 +52,42 @@ const SheetProvider = ({ context = "global" }) => {
}, [close, open, visible]);
const open = useCallback(
async (payload) => {
if (!payload.context) payload.context = "global";
if (payload.context !== context) return;
setData(payload);
async (data) => {
if (!data.context) data.context = "global";
if (data.context !== context) return;
if (visible || dialogData) {
setDialogData(null);
setVisible(false);
await sleep(0);
}
setDialogData(data);
setVisible(true);
if (payload.editor) {
if (data.editor) {
editor.current.refocus = false;
if (editorState().keyboardState) {
// tiny.call(EditorWebView, tiny.cacheRange + tiny.blur);
editor.current.refocus = true;
}
}
},
[context]
[context, dialogData, visible]
);
useEffect(() => {
(async () => {
if (visible && data) {
if (data.editor) await sleep(100);
if (visible && dialogData) {
if (dialogData.editor) await sleep(100);
actionSheetRef.current?.setModalVisible(true);
return;
} else {
if (editor.current?.refocus) {
editorState().isFocused = true;
// tiny.call(EditorWebView, tiny.restoreRange + tiny.clearRange);
editor.current.refocus = false;
}
}
})();
}, [visible, data]);
}, [visible, dialogData]);
const close = useCallback(
(ctx) => {
@@ -91,31 +98,34 @@ const SheetProvider = ({ context = "global" }) => {
[context]
);
return !visible || !data ? null : (
return !visible || !dialogData ? null : (
<SheetWrapper
fwdRef={actionSheetRef}
gestureEnabled={!data?.progress && !data?.disableClosing}
closeOnTouchBackdrop={!data?.progress && !data?.disableClosing}
gestureEnabled={!dialogData?.progress && !dialogData?.disableClosing}
closeOnTouchBackdrop={
!dialogData?.progress && !dialogData?.disableClosing
}
onClose={() => {
data.onClose && data.onClose();
dialogData.onClose && dialogData.onClose();
setVisible(false);
setData(null);
setDialogData(null);
}}
bottomPadding={!data.noBottomPadding}
enableGesturesInScrollView={data.enableGesturesInScrollView}
>
<View
style={{
justifyContent: "center",
alignItems: "center",
marginBottom:
!data.progress && !data.icon && !data.title && !data.paragraph
!dialogData.progress &&
!dialogData.icon &&
!dialogData.title &&
!dialogData.paragraph
? 0
: 10,
paddingHorizontal: 12
}}
>
{data?.progress ? (
{dialogData?.progress ? (
<ActivityIndicator
style={{
marginTop: 15
@@ -125,48 +135,47 @@ const SheetProvider = ({ context = "global" }) => {
/>
) : null}
{data?.icon ? (
{dialogData?.icon ? (
<Icon
color={colors[data.iconColor] || colors.accent}
name={data.icon}
color={colors[dialogData.iconColor] || colors.accent}
name={dialogData.icon}
size={50}
/>
) : null}
{data?.title ? <Heading> {data?.title}</Heading> : null}
{dialogData?.title ? <Heading> {dialogData?.title}</Heading> : null}
{data?.paragraph ? (
{dialogData?.paragraph ? (
<Paragraph style={{ textAlign: "center" }}>
{data?.paragraph}
{dialogData?.paragraph}
</Paragraph>
) : null}
</View>
{typeof data.component === "function"
? data.component(
{typeof dialogData.component === "function"
? dialogData.component(
actionSheetRef,
() => close(context),
(data) => {
if (!data) return;
setData((prevData) => {
setDialogData((prevData) => {
return {
...prevData,
...data
};
});
},
colors
}
)
: data.component}
: dialogData.component}
<View
style={{
paddingHorizontal: 12,
marginBottom: data.valueArray ? 12 : 0
marginBottom: dialogData.valueArray ? 12 : 0
}}
>
{data.valueArray &&
data.valueArray.map((v) => (
{dialogData.valueArray &&
dialogData.valueArray.map((v) => (
<Button
title={v}
type="gray"
@@ -188,30 +197,32 @@ const SheetProvider = ({ context = "global" }) => {
paddingHorizontal: 12
}}
>
{data?.action ? (
{dialogData?.action ? (
<Button
onPress={data.action}
key={data.actionText}
title={data.actionText}
accentColor={data.iconColor || "accent"}
onPress={dialogData.action}
key={dialogData.actionText}
title={dialogData.actionText}
accentColor={dialogData.iconColor || "accent"}
accentText="light"
type="accent"
height={40}
height={45}
width={250}
style={{
marginBottom: 25
borderRadius: 100
}}
fontSize={SIZE.md}
/>
) : null}
{data?.actionsArray &&
data?.actionsArray.map((item) => (
{dialogData?.actionsArray &&
dialogData?.actionsArray.map((item) => (
<Button
onPress={item.action}
key={item.accentText}
title={item.actionText}
icon={item.icon && item.icon}
type={item.type || "accent"}
height={50}
style={{
marginBottom: 10
}}
@@ -220,7 +231,7 @@ const SheetProvider = ({ context = "global" }) => {
/>
))}
{data?.learnMore ? (
{dialogData?.learnMore ? (
<Paragraph
style={{
alignSelf: "center",
@@ -228,7 +239,7 @@ const SheetProvider = ({ context = "global" }) => {
textDecorationLine: "underline"
}}
size={SIZE.xs}
onPress={data.learnMorePress}
onPress={dialogData.learnMorePress}
color={colors.icon}
>
<Icon
@@ -236,7 +247,7 @@ const SheetProvider = ({ context = "global" }) => {
name="information-outline"
size={SIZE.xs}
/>{" "}
{data.learnMore}
{dialogData.learnMore}
</Paragraph>
) : null}
</View>

View File

@@ -25,34 +25,41 @@ import {
TouchableOpacity,
View
} from "react-native";
import { FlatList } from "react-native-actions-sheet";
import { FlatList } from "react-native-gesture-handler";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import { DDS } from "../../../services/device-detection";
import { ToastEvent, presentSheet } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { useMenuStore } from "../../../stores/use-menu-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { SIZE, ph, pv } from "../../../utils/size";
import { DDS } from "../../../services/device-detection";
import {
eSubscribeEvent,
eUnSubscribeEvent,
ToastEvent
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { db } from "../../../common/database";
import {
eCloseAddNotebookDialog,
eOpenAddNotebookDialog
} from "../../../utils/events";
import { ph, pv, SIZE } from "../../../utils/size";
import { sleep } from "../../../utils/time";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import { Button } from "../../ui/button";
import DialogHeader from "../../dialog/dialog-header";
import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import Heading from "../../ui/typography/heading";
import { MoveNotes } from "../move-notes/movenote";
import Seperator from "../../ui/seperator";
import SheetWrapper from "../../ui/sheet";
import { Toast } from "../../toast";
let refs = [];
export class AddNotebookSheet extends React.Component {
constructor(props) {
super(props);
refs = [];
this.state = {
notebook: props.notebook,
topics:
props.notebook?.topics?.map((item) => {
return item.title;
}) || [],
notebook: null,
visible: false,
topics: [],
description: null,
titleFocused: false,
descFocused: false,
@@ -61,14 +68,13 @@ export class AddNotebookSheet extends React.Component {
editTopic: false,
loading: false
};
this.title = props.notebook?.title;
this.description = props.notebook?.description;
this.title = null;
this.description = null;
this.listRef;
this.prevItem = null;
this.prevIndex = null;
this.currentSelectedInput = null;
this.id = props.notebook?.id;
this.id = null;
this.backPressCount = 0;
this.currentInputValue = null;
this.titleRef;
@@ -77,22 +83,57 @@ export class AddNotebookSheet extends React.Component {
this.hiddenInput = createRef();
this.topicInputRef = createRef();
this.addingTopic = false;
this.actionSheetRef = props.actionSheetRef;
}
componentWillUnmount() {
refs = [];
this.actionSheetRef = createRef();
}
componentDidMount() {
sleep(300).then(() => {
!this.state.notebook && this.titleRef?.focus();
});
eSubscribeEvent(eOpenAddNotebookDialog, this.open);
eSubscribeEvent(eCloseAddNotebookDialog, this.close);
}
close = () => {
componentWillUnmount() {
eUnSubscribeEvent(eOpenAddNotebookDialog, this.open);
eUnSubscribeEvent(eCloseAddNotebookDialog, this.close);
}
open = (notebook) => {
refs = [];
this.props.close(true);
if (notebook) {
let topicsList = [];
notebook.topics.forEach((item) => {
topicsList.push(item.title);
});
this.id = notebook.id;
this.title = notebook.title;
this.description = notebook.description;
this.setState({
topics: [...topicsList],
visible: true,
notebook: notebook
});
} else {
this.setState({
visible: true,
notebook: null
});
}
sleep(100).then(() => {
this.actionSheetRef.current?.show();
});
};
close = () => {
this.actionSheetRef.current?.hide();
refs = [];
this.prevIndex = null;
this.prevItem = null;
this.currentSelectedInput = null;
this.title = null;
this.description = null;
this.currentInputValue = null;
this.id = null;
};
onDelete = (index) => {
@@ -182,7 +223,6 @@ export class AddNotebookSheet extends React.Component {
});
await db.notebooks.notebook(toEdit.id).topics.add(...nextTopics);
this.close();
} else {
newNotebookId = await db.notebooks.add({
title: this.title,
@@ -192,9 +232,23 @@ export class AddNotebookSheet extends React.Component {
});
}
useMenuStore.getState().setMenuPins();
Navigation.queueRoutesForUpdate();
useRelationStore.getState().update();
MoveNotes.present(db.notebooks.notebook(newNotebookId).data);
Navigation.queueRoutesForUpdate(
"Notes",
"ColoredNotes",
"TaggedNotes",
"TopicNotes",
"Notebooks",
"Notebook"
);
this.setState({
loading: false
});
this.close();
await sleep(300);
if (!notebook) {
MoveNotes.present(db.notebooks.notebook(newNotebookId).data);
}
};
onSubmit = (forward = true) => {
@@ -211,7 +265,7 @@ export class AddNotebookSheet extends React.Component {
topics: prevTopics
});
setTimeout(() => {
this.listRef.current?.scrollToEnd?.({ animated: true });
this.listRef.scrollToEnd({ animated: true });
}, 30);
this.currentInputValue = null;
} else {
@@ -235,7 +289,7 @@ export class AddNotebookSheet extends React.Component {
if (forward) {
setTimeout(() => {
this.listRef.current?.scrollToEnd?.({ animated: true });
this.listRef.scrollToEnd({ animated: true });
}, 30);
}
}
@@ -245,165 +299,183 @@ export class AddNotebookSheet extends React.Component {
willFocus && this.topicInputRef.current?.focus();
};
renderTopicItem = ({ item, index }) => (
<TopicItem
item={item}
onPress={(item, index) => {
this.prevIndex = index;
this.prevItem = item;
this.topicInputRef.current?.setNativeProps({
text: item
});
this.topicInputRef.current?.focus();
this.currentInputValue = item;
this.setState({
editTopic: true
});
}}
onDelete={this.onDelete}
index={index}
colors={this.props.colors}
/>
);
render() {
const { colors } = this.props;
const { topics, topicInputFocused, notebook } = this.state;
const { topics, visible, topicInputFocused, notebook } = this.state;
if (!visible) return null;
return (
<View
style={{
maxHeight: DDS.isTab ? "90%" : "97%",
borderRadius: DDS.isTab ? 5 : 0,
paddingHorizontal: 12
<SheetWrapper
onOpen={async () => {
this.topicsToDelete = [];
await sleep(300);
!this.state.notebook && this.titleRef?.focus();
}}
fwdRef={this.actionSheetRef}
onClose={() => {
this.close();
this.setState({
visible: false,
topics: [],
descFocused: false,
titleFocused: false,
editTopic: false,
notebook: null
});
}}
statusBarTranslucent={false}
onRequestClose={this.close}
>
<TextInput
ref={this.hiddenInput}
style={{
width: 1,
height: 1,
opacity: 0,
position: "absolute"
}}
blurOnSubmit={false}
/>
<View
style={{
flexDirection: "row",
width: "100%",
justifyContent: "space-between",
alignItems: "center"
maxHeight: DDS.isTab ? "90%" : "96%",
borderRadius: DDS.isTab ? 5 : 0,
paddingHorizontal: 12
}}
>
<Heading size={SIZE.lg}>
{notebook && notebook.dateCreated
? "Edit Notebook"
: "New Notebook"}
</Heading>
<Button
title="Save"
type="accent"
height={40}
<TextInput
ref={this.hiddenInput}
style={{
borderRadius: 100,
paddingHorizontal: 24
width: 1,
height: 1,
opacity: 0,
position: "absolute"
}}
blurOnSubmit={false}
/>
<DialogHeader
title={
notebook && notebook.dateCreated
? "Edit Notebook"
: "New Notebook"
}
paragraph={
notebook && notebook.dateCreated
? "You are editing " + this.title + " notebook."
: "Notebooks are the best way to organize your notes."
}
/>
<Seperator half />
<Input
fwdRef={(ref) => (this.titleRef = ref)}
testID={notesnook.ids.dialogs.notebook.inputs.title}
onChangeText={(value) => {
this.title = value;
}}
placeholder="Enter a title"
onSubmit={() => {
this.descriptionRef.focus();
}}
returnKeyLabel="Next"
returnKeyType="next"
defaultValue={notebook ? notebook.title : null}
/>
<Input
fwdRef={(ref) => (this.descriptionRef = ref)}
testID={notesnook.ids.dialogs.notebook.inputs.description}
onChangeText={(value) => {
this.description = value;
}}
placeholder="Describe your notebook."
onSubmit={() => {
this.topicInputRef.current?.focus();
}}
returnKeyLabel="Next"
returnKeyType="next"
defaultValue={notebook ? notebook.description : null}
/>
<Input
fwdRef={this.topicInputRef}
testID={notesnook.ids.dialogs.notebook.inputs.topic}
onChangeText={(value) => {
this.currentInputValue = value;
if (this.prevItem !== null) {
refs[this.prevIndex].setNativeProps({
text: value,
style: {
borderBottomColor: colors.accent
}
});
}
}}
returnKeyLabel="Done"
returnKeyType="done"
onSubmit={() => {
this.onSubmit();
}}
blurOnSubmit={false}
button={{
testID: "topic-add-button",
icon: this.state.editTopic ? "check" : "plus",
onPress: this.onSubmit,
color: topicInputFocused ? colors.accent : colors.icon
}}
placeholder="Add a topic"
/>
<FlatList
data={topics}
ref={(ref) => (this.listRef = ref)}
nestedScrollEnabled
keyExtractor={(item, index) => item + index.toString()}
onMomentumScrollEnd={() => {
this.actionSheetRef.current?.handleChildScrollEnd();
}}
keyboardShouldPersistTaps="always"
keyboardDismissMode="interactive"
ListFooterComponent={<View style={{ height: 50 }} />}
renderItem={({ item, index }) => (
<TopicItem
item={item}
onPress={(item, index) => {
this.prevIndex = index;
this.prevItem = item;
this.topicInputRef.current?.setNativeProps({
text: item
});
this.topicInputRef.current?.focus();
this.currentInputValue = item;
this.setState({
editTopic: true
});
}}
onDelete={this.onDelete}
index={index}
colors={colors}
/>
)}
/>
<Seperator />
<Button
width="100%"
height={50}
fontSize={SIZE.md}
title={
notebook && notebook.dateCreated
? "Save changes"
: "Create notebook"
}
type="accent"
onPress={this.addNewNotebook}
/>
{/*
{Platform.OS === 'ios' && (
<View
style={{
height: 40
}}
/>
)} */}
</View>
<Seperator />
<Input
fwdRef={(ref) => (this.titleRef = ref)}
testID={notesnook.ids.dialogs.notebook.inputs.title}
onChangeText={(value) => {
this.title = value;
}}
placeholder="Enter a title"
onSubmit={() => {
this.descriptionRef.focus();
}}
returnKeyLabel="Next"
returnKeyType="next"
defaultValue={notebook ? notebook.title : null}
/>
<Input
fwdRef={(ref) => (this.descriptionRef = ref)}
testID={notesnook.ids.dialogs.notebook.inputs.description}
onChangeText={(value) => {
this.description = value;
}}
placeholder="Describe your notebook."
onSubmit={() => {
this.topicInputRef.current?.focus();
}}
returnKeyLabel="Next"
returnKeyType="next"
defaultValue={notebook ? notebook.description : null}
/>
<Input
fwdRef={this.topicInputRef}
testID={notesnook.ids.dialogs.notebook.inputs.topic}
onChangeText={(value) => {
this.currentInputValue = value;
if (this.prevItem !== null) {
refs[this.prevIndex].setNativeProps({
text: value,
style: {
borderBottomColor: colors.accent
}
});
}
}}
returnKeyLabel="Done"
returnKeyType="done"
onSubmit={() => {
this.onSubmit();
}}
blurOnSubmit={false}
button={{
testID: "topic-add-button",
icon: this.state.editTopic ? "check" : "plus",
onPress: this.onSubmit,
color: topicInputFocused ? colors.accent : colors.icon
}}
placeholder="Add a topic"
/>
<FlatList
data={topics}
ref={(ref) => (this.listRef = ref)}
nestedScrollEnabled
keyExtractor={(item, index) => item + index.toString()}
keyboardShouldPersistTaps="always"
keyboardDismissMode="interactive"
ListFooterComponent={
topics.length === 0 ? null : <View style={{ height: 50 }} />
}
renderItem={this.renderTopicItem}
/>
</View>
<Toast context="local" />
</SheetWrapper>
);
}
}
AddNotebookSheet.present = (notebook) => {
presentSheet({
component: (ref, close, _update, colors) => (
<AddNotebookSheet
actionSheetRef={ref}
notebook={notebook}
close={close}
colors={colors}
/>
)
});
};
const TopicItem = ({ item, index, colors, onPress, onDelete }) => {
const topicRef = (ref) => (refs[index] = ref);

View File

@@ -20,9 +20,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { createContext, useContext } from "react";
export const SelectionContext = createContext({
enabled: false,
selected: [],
toggleSelection: (item) => null,
deselect: (item) => null,
select: (item) => null,
isSelected: (item) => null,
setMultiSelect: () => null,
deselectAll: () => null
});
export const SelectionProvider = SelectionContext.Provider;

View File

@@ -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 { FlatList } from "react-native-actions-sheet";
import { FlatList } from "react-native-gesture-handler";
import { db } from "../../../common/database";
import { ListHeaderInputItem } from "./list-header-item.js";
@@ -27,7 +27,6 @@ export const FilteredList = ({
itemType,
onAddItem,
hasHeaderSearch,
listRef,
...restProps
}) => {
const [filtered, setFiltered] = useState(data);
@@ -57,7 +56,6 @@ export const FilteredList = ({
<FlatList
{...restProps}
data={filtered}
ref={listRef}
ListHeaderComponent={
hasHeaderSearch ? (
<ListHeaderInputItem
@@ -71,6 +69,7 @@ export const FilteredList = ({
}
keyboardShouldPersistTaps="always"
keyboardDismissMode="none"
nestedScrollEnabled
/>
);
};

View File

@@ -17,45 +17,90 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useCallback, useEffect, useMemo } from "react";
import React, {
createRef,
useCallback,
useEffect,
useMemo,
useState
} from "react";
import { Keyboard, TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../../common/database";
import {
eSendEvent,
presentSheet,
eSubscribeEvent,
eUnSubscribeEvent,
ToastEvent
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import SearchService from "../../../services/search";
import { useNotebookStore } from "../../../stores/use-notebook-store";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { useSettingStore } from "../../../stores/use-setting-store";
import { useThemeStore } from "../../../stores/use-theme-store";
import { eOnTopicSheetUpdate } from "../../../utils/events";
import { eOpenMoveNoteDialog } from "../../../utils/events";
import { Dialog } from "../../dialog";
import DialogHeader from "../../dialog/dialog-header";
import { presentDialog } from "../../dialog/functions";
import { Button } from "../../ui/button";
import SheetWrapper from "../../ui/sheet";
import Paragraph from "../../ui/typography/paragraph";
import { SelectionProvider } from "./context";
import { FilteredList } from "./filtered-list";
import { ListItem } from "./list-item";
import { useItemSelectionStore } from "./store";
import { useRelationStore } from "../../../stores/use-relation-store";
const MoveNoteSheet = ({ note, actionSheetRef }) => {
const actionSheetRef = createRef();
const AddToNotebookSheet = () => {
const [visible, setVisible] = useState(false);
const [note, setNote] = useState(null);
function open(note) {
setNote(note);
setVisible(true);
actionSheetRef.current?.setModalVisible(true);
}
useEffect(() => {
eSubscribeEvent(eOpenMoveNoteDialog, open);
return () => {
eUnSubscribeEvent(eOpenMoveNoteDialog, open);
};
}, []);
const _onClose = () => {
setVisible(false);
setNote(null);
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes",
"Notebooks",
"Notebook"
);
};
return !visible ? null : (
<SheetWrapper fwdRef={actionSheetRef} onClose={_onClose}>
<MoveNoteComponent note={note} />
</SheetWrapper>
);
};
export default AddToNotebookSheet;
const MoveNoteComponent = ({ note }) => {
const colors = useThemeStore((state) => state.colors);
const [multiSelect, setMultiSelect] = useState(false);
const notebooks = useNotebookStore((state) =>
state.notebooks.filter((n) => n?.type === "notebook")
);
const dimensions = useSettingStore((state) => state.dimensions);
const [edited, setEdited] = useState(false);
const selectedItemsList = useSelectionStore(
(state) => state.selectedItemsList
);
const setNotebooks = useNotebookStore((state) => state.setNotebooks);
const multiSelect = useItemSelectionStore((state) => state.multiSelect);
const [itemState, setItemState] = useState({});
const onAddNotebook = async (title) => {
if (!title || title.trim().length === 0) {
@@ -114,13 +159,13 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
(item) => {
switch (item.type) {
case "notebook": {
const notes = db.relations.from(item, "note");
if (notes.length === 0) return 0;
const noteIds = [];
for (let topic of item.topics) {
noteIds.push(...(db.notes?.topicReferences.get(topic.id) || []));
}
let count = 0;
selectedItemsList.forEach((item) =>
notes.findIndex((note) => note.id === item.id) > -1
? count++
: undefined
noteIds.indexOf(item.id) > -1 ? count++ : undefined
);
return count;
}
@@ -139,55 +184,42 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
useEffect(() => {
resetItemState();
return () => {
useItemSelectionStore.getState().setMultiSelect(false);
useItemSelectionStore.getState().setItemState({});
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const resetItemState = useCallback(
(state) => {
const itemState = {};
const notebooks = db.notebooks.all;
let count = 0;
for (let notebook of notebooks) {
itemState[notebook.id] = state
? state
: areAllSelectedItemsInNotebook(notebook, selectedItemsList)
? "selected"
: getSelectedNotesCountInItem(notebook, selectedItemsList) > 0
? "intermediate"
: "deselected";
if (itemState[notebook.id] === "selected") {
count++;
contextValue.select(notebook);
} else {
contextValue.deselect(notebook);
}
for (let topic of notebook.topics) {
itemState[topic.id] = state
setItemState(() => {
const itemState = {};
const notebooks = db.notebooks.all;
for (let notebook of notebooks) {
itemState[notebook.id] = state
? state
: areAllSelectedItemsInTopic(topic, selectedItemsList) &&
getSelectedNotesCountInItem(topic, selectedItemsList)
: areAllSelectedItemsInAllTopics(notebook, selectedItemsList) &&
getSelectedNotesCountInItem(notebook, selectedItemsList) > 0
? "selected"
: getSelectedNotesCountInItem(topic, selectedItemsList) > 0
: getSelectedNotesCountInItem(notebook, selectedItemsList) > 0
? "intermediate"
: "deselected";
if (itemState[topic.id] === "selected") {
count++;
contextValue.select(topic);
} else {
contextValue.deselect(topic);
if (itemState[notebook.id] === "selected") {
contextValue.select(notebook);
}
for (let topic of notebook.topics) {
itemState[topic.id] = state
? state
: areAllSelectedItemsInTopic(topic, selectedItemsList) &&
getSelectedNotesCountInItem(topic, selectedItemsList)
? "selected"
: getSelectedNotesCountInItem(topic, selectedItemsList) > 0
? "intermediate"
: "deselected";
if (itemState[topic.id] === "selected") {
contextValue.select(topic);
}
}
}
}
if (count > 1) {
useItemSelectionStore.getState().setMultiSelect(true);
} else {
useItemSelectionStore.getState().setMultiSelect(false);
}
useItemSelectionStore.getState().setItemState(itemState);
return itemState;
});
},
[contextValue, getSelectedNotesCountInItem, selectedItemsList]
);
@@ -199,11 +231,11 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
}
};
function areAllSelectedItemsInNotebook(notebook, items) {
const notes = db.relations.from(notebook, "note");
if (notes.length === 0) return false;
function areAllSelectedItemsInAllTopics(notebook, items) {
return items.every((item) => {
return notes.find((note) => note.id === item.id);
return notebook.topics.every((topic) => {
return db.notes.topicReferences.get(topic.id).indexOf(item.id) > -1;
});
});
}
@@ -214,26 +246,51 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
}
const updateItemState = useCallback(function (item, state) {
const itemState = useItemSelectionStore.getState().itemState;
const mergeState = {
[item.id]: state
};
useItemSelectionStore.getState().setItemState({
...itemState,
...mergeState
setItemState((itemState) => {
const mergeState = {
[item.id]: state
};
const notebooks = db.notebooks.all;
const notebook =
item.type === "notebook"
? item
: notebooks.find((n) => n.id === item.notebookId);
const intermediate = notebook.topics.some((topic) => {
return topic.id === item.id
? state === "selected"
: itemState[topic.id] === "selected";
});
if (intermediate) mergeState[notebook.id] = "intermediate";
const selected = notebook.topics.every((topic) => {
return topic.id === item.id
? state === "selected"
: itemState[topic.id] === "selected";
});
if (selected) mergeState[notebook.id] = "selected";
if (!selected && !intermediate) mergeState[notebook.id] = "deselected";
return {
...itemState,
...mergeState
};
});
}, []);
const contextValue = useMemo(
() => ({
enabled: multiSelect,
toggleSelection: (item) => {
const itemState = useItemSelectionStore.getState().itemState;
if (itemState[item.id] === "selected") {
updateItemState(item, "deselected");
} else {
updateItemState(item, "selected");
}
setItemState((itemState) => {
if (itemState[item.id] === "selected") {
updateItemState(item, "deselected");
} else {
updateItemState(item, "selected");
}
return itemState;
});
},
setMultiSelect: setMultiSelect,
deselect: (item) => {
updateItemState(item, "deselected");
},
@@ -244,7 +301,7 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
resetItemState(state);
}
}),
[resetItemState, updateItemState]
[multiSelect, resetItemState, updateItemState]
);
const getItemFromId = (id) => {
@@ -257,47 +314,39 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
};
const onSave = async () => {
const noteIds = note ? [note.id] : selectedItemsList.map((n) => n.id);
const itemState = useItemSelectionStore.getState().itemState;
for (const id in itemState) {
const item = getItemFromId(id);
if (item.type === "notebook") continue;
const noteIds = selectedItemsList.map((n) => n.id);
if (itemState[id] === "selected") {
if (item.type === "notebook") {
for (let noteId of noteIds) {
await db.relations.add(item, { id: noteId, type: "note" });
}
} else {
await db.notes.addToNotebook(
{
topic: item.id,
id: item.notebookId,
rebuildCache: true
},
...noteIds
);
}
await db.notes.addToNotebook(
{
topic: item.id,
id: item.notebookId,
rebuildCache: true
},
...noteIds
);
} else if (itemState[id] === "deselected") {
if (item.type === "notebook") {
for (let noteId of noteIds) {
await db.relations.unlink(item, { id: noteId, type: "note" });
}
} else {
await db.notes.removeFromNotebook(
{
id: item.notebookId,
topic: item.id,
rebuildCache: true
},
...noteIds
);
}
await db.notes.removeFromNotebook(
{
id: item.notebookId,
topic: item.id,
rebuildCache: true
},
...noteIds
);
}
}
Navigation.queueRoutesForUpdate();
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes"
);
setNotebooks();
eSendEvent(eOnTopicSheetUpdate);
SearchService.updateAndSearch();
useRelationStore.getState().update();
actionSheetRef.current?.hide();
};
@@ -348,32 +397,34 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
/>
</View>
<View
style={{
paddingHorizontal: 12
}}
>
<Button
title="Reset selection"
height={30}
{multiSelect ? (
<View
style={{
alignSelf: "flex-start",
paddingHorizontal: 0,
width: "100%",
marginTop: 6
paddingHorizontal: 12
}}
type="grayAccent"
onPress={() => {
resetItemState();
}}
/>
</View>
>
<Button
title="Reset selection"
height={30}
style={{
alignSelf: "flex-start",
paddingHorizontal: 0
}}
onPress={() => {
resetItemState();
setMultiSelect(false);
}}
/>
</View>
) : null}
<SelectionProvider value={contextValue}>
<FilteredList
onMomentumScrollEnd={() => {
actionSheetRef.current?.handleChildScrollEnd();
}}
style={{
paddingHorizontal: 12,
maxHeight: dimensions.height * 0.85
paddingHorizontal: 12
}}
ListEmptyComponent={
notebooks.length > 0 ? null : (
@@ -399,8 +450,12 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
item={item}
key={item.id}
index={index}
hasNotes={getSelectedNotesCountInItem(item) > 0}
sheetRef={actionSheetRef}
intermediate={itemState[item.id] === "intermediate"}
removed={
itemState[item.id] === "deselected" &&
getSelectedNotesCountInItem(item) > 0
}
isSelected={itemState[item.id] === "selected"}
infoText={
<>
{item.topics.length === 1
@@ -410,18 +465,25 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
}
getListItems={getItemsForItem}
getSublistItemProps={(topic) => ({
hasNotes: getSelectedNotesCountInItem(topic) > 0,
selected: itemState[topic.id] === "selected",
intermediate: itemState[topic.id] === "intermediate",
isSelected: itemState[topic.id] === "selected",
removed:
itemState[topic.id] === "deselected" &&
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);
updateItemState(
notebooks.find((n) => n.id === item.notebookId),
"intermediate"
);
} else {
contextValue.deselect(item);
}
@@ -446,35 +508,22 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
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 }} />}
ListFooterComponent={
<View
style={{
height: 200
}}
/>
}
/>
</SelectionProvider>
</View>
</>
);
};
MoveNoteSheet.present = (note) => {
presentSheet({
component: (ref) => <MoveNoteSheet actionSheetRef={ref} note={note} />,
enableGesturesInScrollView: false,
noBottomPadding: true
});
};
export default MoveNoteSheet;

View File

@@ -17,10 +17,8 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useState } from "react";
import React, { useState } from "react";
import { View } from "react-native";
import { db } from "../../../common/database";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { useThemeStore } from "../../../stores/use-theme-store";
import { SIZE } from "../../../utils/size";
import { IconButton } from "../../ui/icon-button";
@@ -29,78 +27,13 @@ import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { useSelectionContext } from "./context";
import { FilteredList } from "./filtered-list";
import { useItemSelectionStore } from "./store";
const SelectionIndicator = ({
item,
hasNotes,
selectItem,
onPress,
onChange
}) => {
const itemState = useItemSelectionStore((state) => state.itemState[item.id]);
const multiSelect = useItemSelectionStore((state) => state.multiSelect);
const isSelected = itemState === "selected";
const isIntermediate = itemState === "intermediate";
const isRemoved = !isSelected && hasNotes;
const colors = useThemeStore((state) => state.colors);
useEffect(() => {
onChange?.();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [itemState]);
return (
<IconButton
size={22}
customStyle={{
marginRight: 5,
width: 23,
height: 23
}}
color={
isRemoved
? colors.red
: isIntermediate || isSelected
? colors.accent
: colors.icon
}
onPress={() => {
if (multiSelect) return selectItem();
onPress?.(item);
}}
onLongPress={() => {
useItemSelectionStore.getState().setMultiSelect(true);
selectItem();
}}
testID={
isRemoved
? "close-circle-outline"
: isSelected
? "check-circle-outline"
: isIntermediate
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
name={
isRemoved
? "close-circle-outline"
: isSelected
? "check-circle-outline"
: isIntermediate
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
);
};
export const ListItem = ({
const _ListItem = ({
item,
index,
icon,
infoText,
intermediate,
hasSubList,
onPress,
onScrollEnd,
@@ -110,49 +43,29 @@ export const ListItem = ({
sublistItemType,
onAddItem,
getSublistItemProps,
removed,
isSelected,
hasHeaderSearch,
onAddSublistItem,
hasNotes,
onChange,
sheetRef
onAddSublistItem
}) => {
const { toggleSelection } = useSelectionContext();
const multiSelect = useItemSelectionStore((state) => state.multiSelect);
const [showSelectedIndicator, setShowSelectedIndicator] = useState(false);
const { enabled, toggleSelection, setMultiSelect, select, deselect } =
useSelectionContext();
const colors = useThemeStore((state) => state.colors);
const [expanded, setExpanded] = useState(false);
function selectItem() {
const currentState = isSelected;
toggleSelection(item);
}
const getSelectedNotesCountInNotebookTopics = (item) => {
if (item.type === "topic") return;
let count = 0;
const noteIds = [];
for (let topic of item.topics) {
noteIds.push(...(db.notes?.topicReferences.get(topic.id) || []));
if (useItemSelectionStore.getState().itemState[topic.id] === "selected") {
count++;
}
if (item.type === "notebook") {
item.topics.forEach((item) => {
if (currentState) {
deselect(item);
} else {
select(item);
}
});
}
useSelectionStore.getState().selectedItemsList.forEach((item) => {
if (noteIds.indexOf(item.id) > -1) {
count++;
}
});
return count;
};
useEffect(() => {
setShowSelectedIndicator(getSelectedNotesCountInNotebookTopics(item) > 0);
}, [item]);
const onChangeSubItem = () => {
setShowSelectedIndicator(getSelectedNotesCountInNotebookTopics(item) > 0);
};
}
return (
<View
style={{
@@ -164,12 +77,12 @@ export const ListItem = ({
<PressableButton
onPress={() => {
if (hasSubList) return setExpanded(!expanded);
if (multiSelect) return selectItem();
if (enabled) return selectItem();
onPress?.(item);
}}
type={type}
onLongPress={() => {
useItemSelectionStore.getState().setMultiSelect(true);
setMultiSelect(true);
selectItem();
}}
customStyle={{
@@ -194,12 +107,46 @@ export const ListItem = ({
alignItems: "center"
}}
>
<SelectionIndicator
hasNotes={hasNotes}
onPress={onPress}
item={item}
onChange={onChange}
selectItem={selectItem}
<IconButton
size={22}
customStyle={{
marginRight: 5,
width: 23,
height: 23
}}
color={
removed
? colors.red
: intermediate || isSelected
? colors.accent
: colors.icon
}
onPress={() => {
if (item.type === "notebook") {
setMultiSelect(true);
}
selectItem();
if (enabled) return;
onPress?.(item);
}}
testID={
removed
? "close-circle-outline"
: isSelected
? "check-circle-outline"
: intermediate
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
name={
removed
? "close-circle-outline"
: isSelected
? "check-circle-outline"
: intermediate
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
<View>
{hasSubList && expanded ? (
@@ -218,27 +165,14 @@ export const ListItem = ({
<View
style={{
flexDirection: "row",
alignItems: "center"
flexDirection: "row"
}}
>
{showSelectedIndicator ? (
<View
style={{
backgroundColor: colors.accent,
width: 7,
height: 7,
borderRadius: 100,
marginRight: 12
}}
/>
) : null}
{onAddSublistItem ? (
<IconButton
name={"plus"}
testID="add-item-icon"
color={colors.pri}
color={colors}
size={SIZE.xl}
onPress={() => {
onAddSublistItem(item);
@@ -271,7 +205,7 @@ export const ListItem = ({
style={{
width: "95%",
alignSelf: "flex-end",
maxHeight: 250
maxHeight: 500
}}
itemType={sublistItemType}
hasHeaderSearch={hasHeaderSearch}
@@ -280,7 +214,6 @@ export const ListItem = ({
item={item}
{...getSublistItemProps(item)}
index={index}
onChange={onChangeSubItem}
onScrollEnd={onScrollEnd}
/>
)}
@@ -290,3 +223,11 @@ export const ListItem = ({
</View>
);
};
export const ListItem = React.memo(_ListItem, (prev, next) => {
if (prev.selected === undefined) return false;
if (prev.isSelected !== next.isSelected) return false;
if (prev.selected !== next.selected) return false;
if (prev.intermediate !== next.intermediate) return false;
return true;
});

View File

@@ -1,42 +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 create, { State } from "zustand";
type SelectionItemState = Record<
string,
"intermediate" | "selected" | "deselected"
>;
export interface SelectionStore extends State {
itemState: SelectionItemState;
setItemState: (state: SelectionItemState) => void;
multiSelect: boolean;
setMultiSelect: (multiSelect: boolean) => void;
}
export const useItemSelectionStore = create<SelectionStore>((set) => ({
itemState: {},
setItemState: (itemState) => {
set({
itemState
});
},
multiSelect: false,
setMultiSelect: (multiSelect) => set({ multiSelect })
}));

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { RefObject, useRef, useState } from "react";
import { TextInput, View } from "react-native";
import { ActionSheetRef } from "react-native-actions-sheet";
import ActionSheet from "react-native-actions-sheet";
import { db } from "../../../common/database";
import {
eSendEvent,
@@ -31,7 +31,7 @@ import { Button } from "../../ui/button";
import Input from "../../ui/input";
type ChangeEmailProps = {
actionSheetRef: RefObject<ActionSheetRef>;
actionSheetRef: RefObject<ActionSheet>;
close?: () => void;
update?: (options: PresentSheetOptions) => void;
};

View File

@@ -42,7 +42,6 @@ import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { eSendEvent } from "../../../services/event-manager";
import { eCloseSheet } from "../../../utils/events";
import { requestInAppReview } from "../../../services/app-review";
const ExportNotesSheet = ({ notes, update }) => {
const colors = useThemeStore((state) => state.colors);
@@ -73,7 +72,6 @@ const ExportNotesSheet = ({ notes, update }) => {
update({ disableClosing: false });
setComplete(true);
setExporting(false);
requestInAppReview();
};
const actions = [
@@ -279,6 +277,7 @@ const ExportNotesSheet = ({ notes, update }) => {
});
});
}}
height={50}
/>
<Button
title="Share"
@@ -302,6 +301,7 @@ const ExportNotesSheet = ({ notes, update }) => {
}).catch(console.log);
}
}}
height={50}
/>
<Button
title="Export in another format"
@@ -317,6 +317,7 @@ const ExportNotesSheet = ({ notes, update }) => {
setResult(null);
setExporting(false);
}}
height={50}
/>
</>
)}

View File

@@ -48,6 +48,7 @@ export const ShareComponent = ({ uri, name, padding }) => {
});
});
}}
height={50}
/>
<Button
title="Share"
@@ -64,6 +65,7 @@ export const ShareComponent = ({ uri, name, padding }) => {
shareFile: true
}).catch(console.log);
}}
height={50}
/>
</View>
);

View File

@@ -196,6 +196,7 @@ For example:
onPress={onPress}
title={loading ? null : "Submit"}
loading={loading}
height={50}
width="100%"
type="accent"
/>

View File

@@ -17,33 +17,51 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useCallback, useEffect, useRef, useState } from "react";
import { View } from "react-native";
import { ScrollView } from "react-native-actions-sheet";
import React, { useEffect, useRef, useState } from "react";
import { ScrollView, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../../common/database";
import { ToastEvent, presentSheet } from "../../../services/event-manager";
import {
eSubscribeEvent,
eUnSubscribeEvent,
ToastEvent
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { useTagStore } from "../../../stores/use-tag-store";
import { useThemeStore } from "../../../stores/use-theme-store";
import { eCloseTagsDialog, eOpenTagsDialog } from "../../../utils/events";
import { SIZE } from "../../../utils/size";
import { sleep } from "../../../utils/time";
import Input from "../../ui/input";
import { PressableButton } from "../../ui/pressable";
import SheetWrapper from "../../ui/sheet";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { IconButton } from "../../ui/icon-button";
const ManageTagsSheet = (props) => {
import { useCallback } from "react";
const ManageTagsSheet = () => {
const colors = useThemeStore((state) => state.colors);
const [notes, setNotes] = useState(props.notes || []);
const [visible, setVisible] = useState(false);
const [note, setNote] = useState(null);
const allTags = useTagStore((state) => state.tags);
const [tags, setTags] = useState([]);
const [query, setQuery] = useState(null);
const inputRef = useRef();
const [focus, setFocus] = useState(false);
const actionSheetRef = useRef();
useEffect(() => {
sortTags();
}, [allTags, notes, query, sortTags]);
eSubscribeEvent(eOpenTagsDialog, open);
eSubscribeEvent(eCloseTagsDialog, close);
return () => {
eUnSubscribeEvent(eOpenTagsDialog, open);
eUnSubscribeEvent(eCloseTagsDialog, close);
};
}, [open]);
useEffect(() => {
if (visible) {
sortTags();
}
}, [allTags, note, query, sortTags, visible]);
const sortTags = useCallback(() => {
let _tags = [...allTags];
@@ -52,14 +70,13 @@ const ManageTagsSheet = (props) => {
if (query) {
_tags = _tags.filter((t) => t.title.startsWith(query));
}
const tagsMerged = [...notes.map((note) => note.tags || []).flat()];
if (!tagsMerged || !tagsMerged.length) {
if (!note || !note.tags) {
setTags(_tags);
return;
}
let noteTags = [];
for (let tag of tagsMerged) {
for (let tag of note.tags) {
let index = _tags.findIndex((t) => t.title === tag);
if (index !== -1) {
noteTags.push(_tags[index]);
@@ -69,11 +86,28 @@ const ManageTagsSheet = (props) => {
noteTags = noteTags.sort((a, b) => a.title.localeCompare(b.title));
let combinedTags = [...noteTags, ..._tags];
setTags(combinedTags);
}, [allTags, notes, query]);
}, [allTags, note, query]);
const open = useCallback(
(item) => {
setNote(item);
useTagStore.getState().setTags();
sortTags();
setVisible(true);
},
[sortTags]
);
useEffect(() => {
useTagStore.getState().setTags();
}, []);
if (visible) {
actionSheetRef.current?.show();
}
}, [visible]);
const close = () => {
setQuery(null);
actionSheetRef.current?.hide();
};
const onSubmit = async () => {
let _query = query;
@@ -87,23 +121,15 @@ const ManageTagsSheet = (props) => {
}
let tag = _query;
setNotes(
notes.map((note) => ({
...note,
tags: note.tags ? [...note.tags, tag] : [tag]
}))
);
setNote({ ...note, tags: note.tags ? [...note.tags, tag] : [tag] });
setQuery(null);
inputRef.current?.setNativeProps({
text: ""
});
try {
for (let note of notes) {
await db.notes.note(note.id).tag(tag);
}
await db.notes.note(note.id).tag(tag);
useTagStore.getState().setTags();
setNotes(notes.map((note) => db.notes.note(note.id).data));
setNote(db.notes.note(note.id).data);
} catch (e) {
ToastEvent.show({
heading: "Cannot add tag",
@@ -113,168 +139,184 @@ const ManageTagsSheet = (props) => {
});
}
Navigation.queueRoutesForUpdate();
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes",
"Notebooks",
"Notebook"
);
};
return (
<View
style={{
width: "100%",
alignSelf: "center",
paddingHorizontal: 12,
minHeight: focus ? "100%" : "60%"
return !visible ? null : (
<SheetWrapper
centered={false}
fwdRef={actionSheetRef}
onOpen={async () => {
await sleep(300);
inputRef.current?.focus();
}}
onClose={async () => {
setQuery(null);
setVisible(false);
}}
>
<Input
button={{
icon: "magnify",
color: colors.accent,
size: SIZE.lg
<View
style={{
width: "100%",
alignSelf: "center",
paddingHorizontal: 12,
minHeight: "60%"
}}
testID="tag-input"
fwdRef={inputRef}
autoCapitalize="none"
onChangeText={(v) => {
setQuery(db.tags.sanitize(v));
}}
onFocusInput={() => {
setFocus(true);
}}
onBlurInput={() => {
setFocus(false);
}}
onSubmit={onSubmit}
placeholder="Search or add a tag"
/>
<ScrollView
overScrollMode="never"
scrollToOverflowEnabled={false}
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
>
{query && query !== tags[0]?.title ? (
<PressableButton
key={"query_item"}
customStyle={{
flexDirection: "row",
marginVertical: 5,
justifyContent: "space-between",
padding: 12
}}
onPress={onSubmit}
type="accent"
>
<Heading size={SIZE.sm} color={colors.light}>
Add {'"' + "#" + query + '"'}
</Heading>
<Icon name="plus" color={colors.light} size={SIZE.lg} />
</PressableButton>
) : null}
{!allTags || allTags.length === 0 ? (
<View
style={{
width: "100%",
height: 200,
justifyContent: "center",
alignItems: "center"
}}
>
<Heading size={50} color={colors.icon}>
#
</Heading>
<Paragraph textBreakStrategy="balanced" color={colors.icon}>
You do not have any tags.
</Paragraph>
</View>
) : null}
<Input
button={{
icon: "magnify",
color: colors.accent,
size: SIZE.lg
}}
testID="tag-input"
fwdRef={inputRef}
autoCapitalize="none"
onChangeText={(v) => {
setQuery(db.tags.sanitize(v));
}}
onSubmit={onSubmit}
height={50}
placeholder="Search or add a tag"
/>
{tags.map((item) => (
<TagItem
key={item.title}
tag={item}
notes={notes}
setNotes={setNotes}
/>
))}
</ScrollView>
</View>
<ScrollView
nestedScrollEnabled
overScrollMode="never"
scrollToOverflowEnabled={false}
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
onMomentumScrollEnd={() => {
actionSheetRef.current?.handleChildScrollEnd();
}}
>
{query && query !== tags[0]?.title ? (
<PressableButton
key={"query_item"}
customStyle={{
flexDirection: "row",
marginVertical: 5,
justifyContent: "space-between",
padding: 12
}}
onPress={onSubmit}
type="accent"
>
<Heading size={SIZE.sm} color={colors.light}>
Add {'"' + "#" + query + '"'}
</Heading>
<Icon name="plus" color={colors.light} size={SIZE.lg} />
</PressableButton>
) : null}
{!allTags || allTags.length === 0 ? (
<View
style={{
width: "100%",
height: 200,
justifyContent: "center",
alignItems: "center"
}}
>
<Heading size={50} color={colors.icon}>
#
</Heading>
<Paragraph textBreakStrategy="balanced" color={colors.icon}>
You do not have any tags.
</Paragraph>
</View>
) : null}
{tags.map((item) => (
<TagItem
key={item.title}
tag={item}
note={note}
setNote={setNote}
/>
))}
</ScrollView>
</View>
</SheetWrapper>
);
};
ManageTagsSheet.present = (notes) => {
presentSheet({
component: (ref) => {
return <ManageTagsSheet actionSheetRef={ref} notes={notes} />;
}
});
};
export default ManageTagsSheet;
const TagItem = ({ tag, notes, setNotes }) => {
const TagItem = ({ tag, note, setNote }) => {
const colors = useThemeStore((state) => state.colors);
const someNotesTagged = notes.some(
(note) => note.tags?.indexOf(tag.title) !== -1
);
const allNotesTagged = notes.every(
(note) => note.tags?.indexOf(tag.title) !== -1
);
const onPress = async () => {
for (let note of notes) {
try {
if (someNotesTagged) {
await db.notes
.note(note.id)
.untag(note.tags[note.tags.indexOf(tag.title)]);
} else {
await db.notes.note(note.id).tag(tag.title);
}
} catch (e) {
console.error(e);
let prevNote = { ...note };
try {
if (prevNote.tags.indexOf(tag.title) !== -1) {
await db.notes
.note(note.id)
.untag(prevNote.tags[prevNote.tags.indexOf(tag.title)]);
} else {
await db.notes.note(note.id).tag(tag.title);
}
useTagStore.getState().setTags();
setNote(db.notes.note(note.id).data);
} catch (e) {
console.error(e);
}
useTagStore.getState().setTags();
setNotes(notes.map((note) => db.notes.note(note.id).data));
setTimeout(() => {
Navigation.queueRoutesForUpdate();
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes"
);
}, 1);
};
return (
<PressableButton
customStyle={{
flexDirection: "row",
marginVertical: 5,
justifyContent: "flex-start",
height: 40
justifyContent: "space-between",
padding: 12
}}
onPress={onPress}
type="gray"
type={
note && note.tags.findIndex((t) => t === tag.title) !== -1
? "shade"
: "grayBg"
}
>
<IconButton
size={22}
customStyle={{
marginRight: 5,
width: 23,
height: 23
}}
color={someNotesTagged || allNotesTagged ? colors.accent : colors.icon}
testID={
allNotesTagged
? "check-circle-outline"
: someNotesTagged
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
<Heading
size={SIZE.sm}
color={
note && note?.tags.findIndex((t) => t === tag.title) !== -1
? colors.accent
: colors.pri
}
>
{"#" + tag.title}
</Heading>
<Icon
name={
allNotesTagged
? "check-circle-outline"
: someNotesTagged
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
note && note?.tags.findIndex((t) => t === tag.title) !== -1
? "minus"
: "plus"
}
color={
note && note?.tags.findIndex((t) => t === tag.title) !== -1
? colors.accent
: colors.accent
}
size={SIZE.lg}
/>
<Paragraph size={SIZE.sm}>{"#" + tag.alias}</Paragraph>
</PressableButton>
);
};

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