Compare commits

..

1 Commits

Author SHA1 Message Date
ammarahm-ed
735ab21ee9 mobile: fix incorrect note loaded when
note is very large
2023-01-17 17:05:22 +05:00
540 changed files with 42587 additions and 55564 deletions

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

4
.github/dco.yml vendored
View File

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

View File

@@ -1,64 +0,0 @@
name: Publish @notesnook/android-internal
on: workflow_dispatch
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 40
steps:
- name: Checkout
uses: actions/checkout@v2
- 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
with:
java-version: '11'
architecture: 'x64'
- name: Install node modules
run: |
npm ci
- name: Make Gradlew Executable
run: cd apps/mobile/native/android && chmod +x ./gradlew
- name: Build unsigned app bundle
run: yarn release:android:bundle
- name: Sign app bundle for Playstore release
id: sign_app
uses: r0adkll/sign-android-release@v1
with:
releaseDirectory: apps/mobile/native/android/app/build/outputs/bundle/release
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.ALIAS }}
keyStorePassword: ${{ secrets.KEY_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
- name: Publish to Playstore
id: deploy
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName: com.streetwriters.notesnook
releaseFile: ${{steps.sign_app.outputs.signedReleaseFile}}
track: internal
status: completed
whatsNewDirectory: apps/mobile/native/android/releasenotes/

View File

@@ -50,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
@@ -102,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
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

@@ -127,9 +127,7 @@ jobs:
path: ./apps/web/desktop/build
- name: Install packages
run: |
npm i
npm i dmg-license
run: npm i
working-directory: ./apps/web/desktop
- name: Install provisioning profile
@@ -215,17 +213,17 @@ jobs:
- name: Build snap
if: inputs.publish-snap
run: |
npx electron-builder --linux snap:x64 -p never
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
npx electron-builder --linux AppImage:x64 AppImage:arm64 -p always
npx electron-builder --linux AppImage deb rpm -p always
else
npx electron-builder --linux AppImage:x64 AppImage:arm64 -p never
npx electron-builder --linux AppImage deb rpm -p never
fi
working-directory: ./apps/web/desktop

View File

@@ -1,57 +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: 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: |
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

3
.gitignore vendored
View File

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

View File

@@ -1,11 +0,0 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"ms-playwright.playwright",
"firefox-devtools.vscode-firefox-debug",
"msjsdiag.vscode-react-native"
]
}

View File

@@ -1,72 +0,0 @@
{
"folders": [
{
"name": "✨ notesnook",
"path": ".."
},
{
"name": "🚀 @notesnook/mobile",
"path": "../apps/mobile"
},
{
"name": "🚀 @notesnook/web",
"path": "../apps/web"
},
{
"name": "🚀 @notesnook/desktop",
"path": "../apps/web/desktop"
},
{
"name": "🚀 @notesnook/web-clipper",
"path": "../extensions/web-clipper"
},
{
"name": "📦 @notesnook/clipper",
"path": "../packages/clipper"
},
{
"name": "📦 @notesnook/core",
"path": "../packages/core"
},
{
"name": "📦 @notesnook/crypto",
"path": "../packages/crypto"
},
{
"name": "📦 @notesnook/crypto-worker",
"path": "../packages/crypto-worker"
},
{
"name": "📦 @notesnook/editor",
"path": "../packages/editor"
},
{
"name": "📦 @notesnook/editor-mobile",
"path": "../packages/editor-mobile"
},
{
"name": "📦 @notesnook/logger",
"path": "../packages/logger"
},
{
"name": "📦 @notesnook/streamable-fs",
"path": "../packages/streamable-fs"
},
{
"name": "📦 @notesnook/theme",
"path": "../packages/theme"
}
],
"settings": {
"typescript.tsdk": "node_modules/typescript/lib",
"javascript.format.enable": true,
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "modificationsIfAvailable",
"eslint.packageManager": "npm",
"eslint.run": "onSave",
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"eslint.execArgv": ["--cache"]
}
}

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}

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).
@@ -28,9 +28,15 @@ If you've thought of a way that Notesnook could be better, we want to hear about
Before you open a new feature request, please make sure it's not a duplicate. **Duplicate feature requests & bug reports are closed immediately.**
### Improving documentation
All app related documentation can be found in the `docs/` directory. All the files in this directory are simple markdown files which you can edit either directly on GitHub, or in your favorite text editor.
If you find a mistake in the documentation or would like to contribute something new to the documentation, feel free to open a pull request; creating an issue for this is not required.
### 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 +68,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.
@@ -85,11 +91,7 @@ The DCO is a declaration attached to every contribution made by every developer
Signed-off-by: Jane Smith <jane.smith@email.com>
```
On VSCode, it looks like this:
![](resources/vscode-signed-off-commit.png)
You may type this line on your own when writing your commit messages. However, if your [user.name](http://user.name) and [user.email](http://user.email) are set in your git configs, you can use `-s` or `--signoff` to add the `Signed-off-by` line to the end of the commit message automatically.
You may type this line on your own when writing your commit messages. However, if your [user.name](http://user.name) and [user.email](http://user.email) are set in your git configs, you can use `-s` or `--signoff` to add the `Signed-off-by` line to the end of the commit message.
#### Include `<scope>:` in your commit message
@@ -131,11 +133,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

View File

@@ -25,4 +25,4 @@
"request": "attach"
}
]
}
}

1
apps/mobile/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1 @@
{}

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

@@ -16,7 +16,7 @@ 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-native-gesture-handler";
import React, { useEffect } from "react";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
@@ -32,6 +32,7 @@ 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) => {
@@ -50,7 +51,6 @@ const App = () => {
if (appLockMode && appLockMode !== "none") {
useUserStore.getState().setVerifyUser(true);
}
//BackgroundSync.start();
setInit(true);
setTimeout(async () => {
SettingsService.onFirstLaunch();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

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

@@ -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 "rn-fetch-blob";
/**
* 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

@@ -28,16 +28,8 @@ import { useAttachmentStore } from "../../stores/use-attachment-store";
import { db } from "../database";
import Storage from "../database/storage";
import { cacheDir } from "./utils";
import { getFileNameWithExtension } from "@notesnook/core/utils/filename";
export default async function downloadAttachment(
hash,
global = true,
options = {
silent: false,
cache: false
}
) {
export default async function downloadAttachment(hash, global = true) {
let attachment = db.attachments.attachment(hash);
if (!attachment) {
console.log("attachment not found");
@@ -45,13 +37,11 @@ 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 {
@@ -64,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,
@@ -78,50 +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 &&
!attachment.metadata?.type?.startsWith("image")
) {
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);

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

@@ -21,8 +21,6 @@ import { Platform } from "react-native";
import Sodium from "@ammarahmed/react-native-sodium";
import RNFetchBlob from "rn-fetch-blob";
import { cacheDir, getRandomId } from "./utils";
import { db } from "../database";
import { compressToBase64 } from "./compress";
export async function readEncrypted(filename, key, cipherData) {
let path = `${cacheDir}/${filename}`;
@@ -31,9 +29,6 @@ export async function readEncrypted(filename, key, cipherData) {
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,
@@ -41,20 +36,8 @@ export async function readEncrypted(filename, key, cipherData) {
...cipherData,
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);
@@ -63,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

@@ -36,11 +36,7 @@ function getFileExtension(filename) {
var ext = /^.+\.([^.]+)$/.exec(filename);
return ext == null ? "" : ext[1];
}
/**
*
* @param {any} param0
* @returns
*/
export const AttachmentItem = ({ attachment, encryption, setAttachments }) => {
const colors = useThemeStore((state) => state.colors);
const [currentProgress, setCurrentProgress] = useAttachmentProgress(
@@ -50,6 +46,7 @@ export const AttachmentItem = ({ attachment, encryption, setAttachments }) => {
const encryptionProgress = useAttachmentStore(
(state) => state.encryptionProgress
);
const onPress = () => {
Actions.present(attachment, setAttachments, attachment.metadata.hash);
};
@@ -125,9 +122,7 @@ export const AttachmentItem = ({ attachment, encryption, setAttachments }) => {
</View>
</View>
{currentProgress ||
(encryptionProgress && encryptionProgress !== "0.00") ||
encryption ? (
{currentProgress || encryptionProgress || encryption ? (
<TouchableOpacity
activeOpacity={0.9}
onPress={() => {

View File

@@ -17,33 +17,70 @@ 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 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 DialogHeader from "../dialog/dialog-header";
import { Toast } from "../toast";
import Input from "../ui/input";
import Seperator from "../ui/seperator";
import SheetWrapper from "../ui/sheet";
import Paragraph from "../ui/typography/paragraph";
import { AttachmentItem } from "./attachment-item";
import { FlatList } from "react-native-actions-sheet";
export const AttachmentDialog = ({ data }) => {
export const AttachmentDialog = () => {
const colors = useThemeStore((state) => state.colors);
const [note, setNote] = useState(data);
const [attachments, setAttachments] = useState(
data
? db.attachments.ofNote(data.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);
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) => {
attachmentSearchValue.current = text;
if (
@@ -67,101 +104,107 @@ export const AttachmentDialog = ({ data }) => {
<AttachmentItem setAttachments={setAttachments} attachment={item} />
);
return (
<View
style={{
width: "100%",
alignSelf: "center",
paddingHorizontal: 12
return !visible ? null : (
<SheetWrapper
centered={false}
fwdRef={actionSheetRef}
onClose={async () => {
setVisible(false);
}}
>
<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}
<FlatList
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
maxToRenderPerBatch={10}
initialNumToRender={10}
windowSize={5}
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}
<Toast context="local" />
<View
style={{
textAlign: "center",
marginTop: 10
width: "100%",
alignSelf: "center",
paddingHorizontal: 12
}}
>
<Icon name="shield-key-outline" size={SIZE.xs} color={colors.icon} />
{" "}All attachments are end-to-end encrypted.
</Paragraph>
</View>
<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}
<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={{
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 data={note} />
});
};

View File

@@ -26,6 +26,7 @@ import { eSendEvent } from "../../services/event-manager";
import { useThemeStore } from "../../stores/use-theme-store";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import BaseDialog from "../dialog/base-dialog";
import SheetProvider from "../sheet-provider";
import { Progress } from "../sheets/progress";
import { Button } from "../ui/button";
@@ -82,6 +83,9 @@ export const Login = ({ changeMode }) => {
<>
<ForgotPassword />
<SheetProvider context="two_factor_verify" />
{loading ? (
<BaseDialog transparent={true} visible={true} animation="fade" />
) : null}
<Animated.View
entering={FadeInDown}
exiting={FadeOutUp}
@@ -130,7 +134,6 @@ export const Login = ({ changeMode }) => {
marginTop: 5
}}
onPress={() => {
if (loading) return;
changeMode(1);
}}
size={SIZE.md}
@@ -169,7 +172,7 @@ export const Login = ({ changeMode }) => {
errorMessage="Email is invalid"
placeholder="Enter your email"
defaultValue={email.current}
editable={step === LoginSteps.emailAuth && !loading}
editable={step === LoginSteps.emailAuth}
onSubmit={() => {
passwordInputRef.current?.focus();
}}
@@ -191,7 +194,6 @@ export const Login = ({ changeMode }) => {
autoCorrect={false}
placeholder="Password"
marginBottom={0}
editable={!loading}
defaultValue={password.current}
onSubmit={() => login()}
/>
@@ -203,7 +205,6 @@ export const Login = ({ changeMode }) => {
paddingHorizontal: 0
}}
onPress={() => {
if (loading) return;
SheetManager.show("forgotpassword_sheet", email.current);
}}
textStyle={{
@@ -227,10 +228,7 @@ export const Login = ({ changeMode }) => {
borderRadius: 100
}}
loading={loading}
onPress={() => {
if (loading) return;
login();
}}
onPress={login}
type="accent"
title={
loading
@@ -250,7 +248,6 @@ export const Login = ({ changeMode }) => {
marginTop: 10
}}
onPress={() => {
if (loading) return;
setStep(LoginSteps.emailAuth);
setLoading(false);
}}

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

@@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useRef, useState } from "react";
import { Dimensions, View } from "react-native";
import Animated, { FadeInDown, FadeOutUp } from "react-native-reanimated";
import umami from "../../common/analytics";
import { db } from "../../common/database";
import { DDS } from "../../services/device-detection";
import { ToastEvent } from "../../services/event-manager";
@@ -30,6 +31,7 @@ import { useUserStore } from "../../stores/use-user-store";
import { openLinkInBrowser } from "../../utils/functions";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import BaseDialog from "../dialog/base-dialog";
import { Button } from "../ui/button";
import Input from "../ui/input";
import { SvgView } from "../ui/svg";
@@ -77,6 +79,7 @@ export const Signup = ({ changeMode, trial }) => {
clearMessage();
setEmailVerifyMessage();
hideAuth();
umami.pageView("/account-created", "/welcome/signup");
await sleep(300);
if (trial) {
PremiumService.sheet(null, null, true);
@@ -96,6 +99,9 @@ export const Signup = ({ changeMode, trial }) => {
return (
<>
{loading ? (
<BaseDialog transparent={true} visible={true} animation="fade" />
) : null}
<Animated.View
entering={FadeInDown}
exiting={FadeOutUp}
@@ -144,7 +150,6 @@ export const Signup = ({ changeMode, trial }) => {
textAlign: "center"
}}
onPress={() => {
if (loading) return;
changeMode(0);
}}
size={SIZE.md}
@@ -232,10 +237,7 @@ export const Signup = ({ changeMode, trial }) => {
borderRadius: 100
}}
loading={loading}
onPress={() => {
if (loading) return;
signup();
}}
onPress={signup}
type="accent"
title={loading ? null : "Agree and continue"}
/>

View File

@@ -54,7 +54,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
app: "Enter the 6 digit code from your authenticator app to continue logging in",
sms: "Enter the 6 digit code sent to your phone number to continue logging in",
email: "Enter the 6 digit code sent to your email to continue logging in",
recoveryCode: "Enter the recovery code to continue logging in"
recoveryCode: "Enter the 8 digit recovery code to continue logging in"
};
const secondaryMethodsText = {
@@ -65,7 +65,9 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
};
const onNext = async () => {
if (!code.current || code.current.length < 6) return;
const length = currentMethod.method === "recoveryCode" ? 8 : 6;
if (!code.current || code.current.length !== length) return;
setLoading(true);
inputRef.current?.blur();
await onMfaLogin(
@@ -200,26 +202,22 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
<>
<Input
placeholder={
currentMethod.method === "recoveryCode"
? "xxxxx-xxxxx"
: "xxxxxx"
currentMethod.method === "recoveryCode" ? "xxxxxxxx" : "xxxxxx"
}
testID={"input.totp"}
maxLength={
currentMethod.method === "recoveryCode" ? undefined : 6
}
maxLength={currentMethod.method === "recoveryCode" ? 8 : 6}
fwdRef={inputRef}
textAlign="center"
onChangeText={(value) => {
code.current = value;
//onNext();
onNext();
}}
inputStyle={{
fontSize: SIZE.lg,
height: 60,
textAlign: "center",
letterSpacing: 10,
width: 250
width: null
}}
keyboardType={
currentMethod.method === "recoveryCode" ? "default" : "numeric"
@@ -302,9 +300,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
TwoFactorVerification.present = (onMfaLogin, data, context) => {
presentSheet({
component: () => (
<TwoFactorVerification onMfaLogin={onMfaLogin} mfaInfo={data} />
),
component: <TwoFactorVerification onMfaLogin={onMfaLogin} mfaInfo={data} />,
context: context || "two_factor_verify",
onClose: () => {
onMfaLogin();

View File

@@ -75,7 +75,6 @@ export const useLogin = (onFinishLogin) => {
mfa.code,
mfa.method
);
if (success) {
setStep(LoginSteps.passwordAuth);
setLoading(false);
@@ -94,8 +93,6 @@ export const useLogin = (onFinishLogin) => {
}
}
}, mfaInfo);
} else {
finishWithError(new Error("Unable to send 2FA code"));
}
break;
}

View File

@@ -18,8 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { KeyboardAvoidingView, Platform, View } from "react-native";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { KeyboardAvoidingView, Platform, SafeAreaView } from "react-native";
import useIsFloatingKeyboard from "../../hooks/use-is-floating-keyboard";
import { useSettingStore } from "../../stores/use-setting-store";
import { Header } from "../header";
@@ -29,7 +28,6 @@ export const Container = ({ children }) => {
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
const insets = useGlobalSafeAreaInsets();
return (
<KeyboardAvoidingView
behavior="padding"
@@ -38,12 +36,10 @@ export const Container = ({ children }) => {
flex: 1
}}
>
<View
<SafeAreaView
style={{
flex: 1,
overflow: "hidden",
paddingTop: Platform.OS === "android" ? 0 : insets.top,
paddingBottom: Platform.OS === "android" ? 0 : insets.bottom
overflow: "hidden"
}}
>
{!introCompleted ? null : (
@@ -54,7 +50,7 @@ export const Container = ({ children }) => {
)}
{children}
</View>
</SafeAreaView>
</KeyboardAvoidingView>
);
};

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,6 +34,10 @@ 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";
@@ -46,6 +51,7 @@ const DialogProvider = () => {
<LoadingDialog />
<Dialog context="global" />
<AddTopicDialog colors={colors} />
<AddNotebookSheet colors={colors} />
<PremiumDialog colors={colors} />
<AuthModal colors={colors} />
<MergeConflicts />
@@ -55,8 +61,12 @@ const DialogProvider = () => {
<RestoreDataSheet />
<ResultDialog />
<VaultDialog colors={colors} />
<AddToNotebookSheet colors={colors} />
<RateAppSheet />
<ImagePreview />
<PublishNoteSheet />
<ManageTagsSheet />
<AttachmentDialog />
{loading ? null : <Expiring />}
<AnnouncementDialog />
<SessionExpired />

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { Text, View, ViewStyle } from "react-native";
import { Text, View } from "react-native";
import { useThemeStore } from "../../stores/use-theme-store";
import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
@@ -41,7 +41,6 @@ type DialogHeaderProps = {
padding?: number;
centered?: boolean;
titlePart?: string;
style?: ViewStyle;
};
const DialogHeader = ({
@@ -51,8 +50,7 @@ const DialogHeader = ({
paragraphColor,
padding,
centered,
titlePart,
style
titlePart
}: DialogHeaderProps) => {
const colors = useThemeStore((state) => state.colors);
@@ -64,8 +62,7 @@ const DialogHeader = ({
alignItems: "center",
justifyContent: "space-between",
minHeight: 50,
paddingHorizontal: padding,
...style
paddingHorizontal: padding
}}
>
<View

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

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

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>
)

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 from "react";
import React, { useEffect } from "react";
import {
Dimensions,
Image,
@@ -26,6 +26,7 @@ import {
View
} from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import umami from "../../common/analytics";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import { DDS } from "../../services/device-detection";
@@ -49,6 +50,9 @@ export const SVG_Z =
const Intro = ({ navigation }) => {
const colors = useThemeStore((state) => state.colors);
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
const isTelemetryEnabled = useSettingStore(
(state) => state.settings.telemetry
);
@@ -60,6 +64,12 @@ const Intro = ({ navigation }) => {
}
});
useEffect(() => {
if (!introCompleted) {
umami.pageView("/welcome", "", []);
}
}, [introCompleted]);
return (
<View
testID="notesnook.splashscreen"
@@ -161,6 +171,8 @@ const Intro = ({ navigation }) => {
navigation.navigate("AppLock", {
welcome: true
});
umami.pageView("/home", "/welcome");
}}
style={{
paddingHorizontal: 24,

File diff suppressed because one or more lines are too long

View File

@@ -17,17 +17,18 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 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";
import {
eSendEvent,
presentSheet
} from "../../services/event-manager";
import { setRateAppMessage } from "../../services/message";
import PremiumService from "../../services/premium";
import SettingsService from "../../services/settings";
@@ -37,9 +38,7 @@ 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";
@@ -54,6 +53,8 @@ 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";
const Launcher = React.memo(
function Launcher() {
@@ -70,7 +71,7 @@ const Launcher = React.memo(
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
const dbInitCompleted = useRef(false);
const loadNotes = useCallback(async () => {
if (verifyUser) {
return;
@@ -81,25 +82,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) {
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
@@ -118,6 +122,9 @@ const Launcher = React.memo(
if (!loading) {
doAppLoadActions();
}
return () => {
dbInitCompleted.current = false;
};
}, [doAppLoadActions, loading]);
const doAppLoadActions = useCallback(async () => {
@@ -126,7 +133,6 @@ const Launcher = React.memo(
eSendEvent("session_expired");
return;
}
notifee.setBadgeCount(0);
await useMessageStore.getState().setAnnouncement();
if (NewFeature.present()) return;
if (await checkAppUpdateAvailable()) return;
@@ -145,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"
@@ -193,11 +199,6 @@ const Launcher = React.memo(
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",
""

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)}
/>
))}

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

@@ -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

@@ -56,6 +56,7 @@ const ReminderItem = React.memo(
>
<View
style={{
flexWrap: "wrap",
flexShrink: 1,
opacity: item.disabled ? 0.5 : 1
}}
@@ -93,13 +94,13 @@ const ReminderItem = React.memo(
backgroundColor: colors.nav,
borderRadius: 5,
flexDirection: "row",
paddingHorizontal: 6,
paddingHorizontal: 5,
paddingVertical: 3,
alignItems: "center",
marginTop: 5,
justifyContent: "flex-start",
alignSelf: "flex-start",
marginRight: 10,
height: 30
marginRight: 10
}}
>
<Icon
@@ -122,13 +123,13 @@ const ReminderItem = React.memo(
backgroundColor: colors.nav,
borderRadius: 5,
flexDirection: "row",
paddingHorizontal: 6,
paddingHorizontal: 5,
paddingVertical: 3,
alignItems: "center",
marginTop: 5,
justifyContent: "flex-start",
alignSelf: "flex-start",
marginRight: 10,
height: 30
marginRight: 10
}}
>
<Icon name="reload" size={SIZE.md} color={colors.accent} />

View File

@@ -0,0 +1,326 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Clipboard from "@react-native-clipboard/clipboard";
import React, { useEffect, useState } from "react";
import { View } from "react-native";
import Animated, { SlideInUp, SlideOutDown } from "react-native-reanimated";
import { db } from "../../../common/database";
import { openVault, ToastEvent } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { useMenuStore } from "../../../stores/use-menu-store";
import { useNotebookStore } from "../../../stores/use-notebook-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { useThemeStore } from "../../../stores/use-theme-store";
import { useTrashStore } from "../../../stores/use-trash-store";
import { dWidth, getElevation, toTXT } from "../../../utils";
import { deleteItems } from "../../../utils/functions";
import { presentDialog } from "../../dialog/functions";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
export const ActionStrip = ({ note, setActionStrip }) => {
const colors = useThemeStore((state) => state.colors);
const selectionMode = useSelectionStore((state) => state.selectionMode);
const setNotebooks = useNotebookStore((state) => state.setNotebooks);
const setMenuPins = useMenuStore((state) => state.setMenuPins);
const setSelectedItem = useSelectionStore((state) => state.setSelectedItem);
const setSelectionMode = useSelectionStore((state) => state.setSelectionMode);
const [isPinnedToMenu, setIsPinnedToMenu] = useState(false);
const [width, setWidth] = useState(dWidth - 16);
useEffect(() => {
if (note.type === "note") return;
setIsPinnedToMenu(db.shortcuts.exists(note.id));
}, [note.id, note.type]);
const updateNotes = () => {
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes"
);
};
const actions = [
{
title: "Pin " + note.type,
icon: note.pinned ? "pin-off" : "pin",
visible: note.type === "note" || note.type === "notebook",
onPress: async () => {
if (!note.id) return;
if (note.type === "note") {
await db.notes.note(note.id).pin();
} else {
await db.notebooks.notebook(note.id).pin();
setNotebooks();
}
updateNotes();
setActionStrip(false);
}
},
{
title: "Add to favorites",
icon: note.favorite ? "star-off" : "star",
onPress: async () => {
if (!note.id) return;
if (note.type === "note") {
await db.notes.note(note.id).favorite();
} else {
await db.notebooks.notebook(note.id).favorite();
}
updateNotes();
setActionStrip(false);
},
visible: note.type === "note",
color: !note.favorite ? "orange" : null
},
{
title: isPinnedToMenu
? "Remove Shortcut from Menu"
: "Add Shortcut to Menu",
icon: isPinnedToMenu ? "link-variant-remove" : "link-variant",
onPress: async () => {
try {
if (isPinnedToMenu) {
await db.shortcuts.remove(note.id);
ToastEvent.show({
heading: "Shortcut removed from menu",
type: "success"
});
} else {
if (note.type === "topic") {
await db.shortcuts.add({
item: {
type: "topic",
id: note.id,
notebookId: note.notebookId
}
});
} else {
await db.shortcuts.add({
item: {
type: "notebook",
id: note.id
}
});
}
ToastEvent.show({
heading: "Shortcut added to menu",
type: "success"
});
}
setIsPinnedToMenu(db.shortcuts.exists(note.id));
setMenuPins();
setActionStrip(false);
} catch (e) {
console.error(e);
}
},
visible: note.type !== "note" && note.type !== "reminder"
},
{
title: "Copy Note",
icon: "content-copy",
visible: note.type === "note",
onPress: async () => {
if (note.locked) {
openVault({
copyNote: true,
novault: true,
locked: true,
item: note,
title: "Copy note",
description: "Unlock note to copy to clipboard."
});
} else {
let text = await toTXT(note);
text = `${note.title}\n \n ${text}`;
Clipboard.setString(text);
ToastEvent.show({
heading: "Note copied to clipboard",
type: "success"
});
}
setActionStrip(false);
}
},
{
title: "Restore " + note.itemType,
icon: "delete-restore",
onPress: async () => {
await db.trash.restore(note.id);
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes",
"Trash",
"Notebooks"
);
ToastEvent.show({
heading:
note.type === "note"
? "Note restored from trash"
: "Notebook restored from trash",
type: "success"
});
setActionStrip(false);
},
visible: note.type === "trash"
},
{
title: "Delete" + note.itemType,
icon: "delete",
visible: note.type === "trash",
onPress: () => {
presentDialog({
title: "Permanent delete",
paragraph: `Are you sure you want to delete this ${note.itemType} permanantly from trash?`,
positiveText: "Delete",
negativeText: "Cancel",
positivePress: async () => {
await db.trash.delete(note.id);
useTrashStore.getState().setTrash();
useSelectionStore.getState().setSelectionMode(false);
ToastEvent.show({
heading: "Permanantly deleted items",
type: "success",
context: "local"
});
},
positiveType: "errorShade"
});
setActionStrip(false);
}
},
{
title: "Delete" + note.type,
icon: "delete",
visible: note.type !== "trash",
onPress: async () => {
if (note.type === "reminder") {
presentDialog({
title: `Delete ${note.type}`,
paragraph: "This reminder will be removed",
positivePress: async () => {
const routes = [];
await db.reminders.remove(note.id);
routes.push("Reminders");
Navigation.queueRoutesForUpdate(...routes);
useRelationStore.getState().update();
},
positiveText: "Delete",
positiveType: "errorShade"
});
return;
}
try {
await deleteItems(note);
} catch (e) {
console.error(e);
}
setActionStrip(false);
}
},
{
title: "Close",
icon: "close",
onPress: () => setActionStrip(false),
color: colors.light,
bg: colors.red,
visible: true
}
];
return (
<Animated.View
onLayout={(event) => {
setWidth(event.nativeEvent.layout.width);
}}
entering={SlideInUp.springify().mass(0.4)}
exiting={SlideOutDown}
style={{
position: "absolute",
zIndex: 999,
width: "102%",
height: "100%",
flexDirection: "row",
justifyContent: "flex-end",
alignItems: "center"
}}
>
<Button
type="accent"
title="Select"
icon="check"
tooltipText="Select Item"
onPress={() => {
if (!selectionMode) {
setSelectionMode(true);
}
setSelectedItem(note);
setActionStrip(false);
}}
style={{
borderRadius: 100,
paddingHorizontal: 12,
...getElevation(5)
}}
height={30}
/>
{actions.map((item) =>
item.visible ? (
<View
key={item.icon}
style={{
width: width / 1.4 / actions.length,
height: width / 1.4 / actions.length,
backgroundColor: item.bg || colors.nav,
borderRadius: 100,
justifyContent: "center",
alignItems: "center",
...getElevation(5),
marginLeft: 15
}}
>
<IconButton
color={item.color || colors.heading}
onPress={item.onPress}
tooltipText={item.title}
top={60}
bottom={60}
name={item.icon}
size={width / 2.8 / actions.length}
/>
</View>
) : null
)}
</Animated.View>
);
};

View File

@@ -17,13 +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 React, { useRef } from "react";
import { useSelectionStore } from "../../../stores/use-selection-store";
import React, { useEffect, useState } from "react";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../../services/event-manager";
import { useSettingStore } from "../../../stores/use-setting-store";
import { useThemeStore } from "../../../stores/use-theme-store";
import { history } from "../../../utils";
import { PressableButton } from "../../ui/pressable";
import { ActionStrip } from "./action-strip";
import { Filler } from "./back-fill";
import { SelectionIcon } from "./selection";
import { useRef } from "react";
const SelectionWrapper = ({
children,
@@ -36,6 +42,7 @@ const SelectionWrapper = ({
}) => {
const itemId = useRef(item.id);
const colors = useThemeStore((state) => state.colors);
const [actionStrip, setActionStrip] = useState(false);
const notebooksListMode = useSettingStore(
(state) => state.settings.notebooksListMode
);
@@ -49,18 +56,35 @@ const SelectionWrapper = ({
if (item.id !== itemId.current) {
itemId.current = item.id;
if (actionStrip) {
setActionStrip(false);
}
}
const _onLongPress = () => {
if (!useSelectionStore.getState().selectionMode) {
useSelectionStore.getState().setSelectionMode(true);
}
useSelectionStore.getState().setSelectedItem(item);
if (history.selectedItemsList.length > 0) return;
setActionStrip(!actionStrip);
};
const _onPress = async () => {
if (actionStrip) {
setActionStrip(false);
return;
}
await onPress();
};
const closeStrip = () => {
setActionStrip(false);
};
useEffect(() => {
eSubscribeEvent("navigate", closeStrip);
return () => {
eUnSubscribeEvent("navigate", closeStrip);
};
}, []);
return (
<PressableButton
customColor={isSheet ? colors.transGray : "transparent"}
@@ -87,10 +111,15 @@ const SelectionWrapper = ({
) : null}
<SelectionIcon
compactMode={compactMode}
setActionStrip={setActionStrip}
item={item}
onLongPress={onLongPress}
/>
{children}
{actionStrip ? (
<ActionStrip note={item} setActionStrip={setActionStrip} />
) : null}
</PressableButton>
);
};

View File

@@ -24,7 +24,7 @@ import { useSelectionStore } from "../../../stores/use-selection-store";
import { useThemeStore } from "../../../stores/use-theme-store";
import { SIZE } from "../../../utils/size";
export const SelectionIcon = ({ item }) => {
export const SelectionIcon = ({ setActionStrip, item }) => {
const colors = useThemeStore((state) => state.colors);
const selectionMode = useSelectionStore((state) => state.selectionMode);
@@ -35,6 +35,7 @@ export const SelectionIcon = ({ item }) => {
useEffect(() => {
if (selectionMode) {
setActionStrip(false);
let exists = selectedItemsList.filter(
(o) => o.dateCreated === item.dateCreated
);
@@ -49,9 +50,16 @@ export const SelectionIcon = ({ item }) => {
}
}
}
}, [selectedItemsList, item.id, selectionMode, item.dateCreated, selected]);
}, [
selectedItemsList,
item.id,
selectionMode,
setActionStrip,
item.dateCreated,
selected
]);
return selectionMode ? (
return selectionMode && selected ? (
<View
style={{
width: 40,
@@ -65,13 +73,11 @@ export const SelectionIcon = ({ item }) => {
}}
pointerEvents="none"
>
{selected ? (
<Icon
size={SIZE.xl}
color={selected ? colors.accent : colors.icon}
name={"check"}
/>
) : null}
<Icon
size={SIZE.xl}
color={selected ? colors.accent : colors.icon}
name="check"
/>
</View>
) : null;
};

View File

@@ -53,52 +53,45 @@ export const Card = ({ color, warning }) => {
>
<View
style={{
flexDirection: "row",
alignItems: "center"
width: 40,
backgroundColor:
messageBoardState.type === "error"
? hexToRGBA(colors.red, 0.15)
: hexToRGBA(color, 0.15),
height: 40,
borderRadius: 100,
alignItems: "center",
justifyContent: "center"
}}
>
<View
style={{
width: 40,
backgroundColor:
messageBoardState.type === "error"
? hexToRGBA(colors.red, 0.15)
: hexToRGBA(color, 0.15),
height: 40,
borderRadius: 100,
alignItems: "center",
justifyContent: "center"
}}
>
<Icon
size={SIZE.lg}
color={
messageBoardState.type === "error" ? colors.errorText : color
}
name={messageBoardState.icon}
/>
</View>
<Icon
size={SIZE.lg}
color={
messageBoardState.type === "error" ? colors.errorText : color
}
name={messageBoardState.icon}
/>
</View>
<View
<View
style={{
marginLeft: 10,
flexShrink: 1,
marginRight: 10
}}
>
<Paragraph color={colors.icon} size={SIZE.xs}>
{messageBoardState.message}
</Paragraph>
<Paragraph
style={{
marginLeft: 10,
flexShrink: 1,
marginRight: 10
flexWrap: "wrap",
flexShrink: 1
}}
color={colors.heading}
>
<Paragraph color={colors.icon} size={SIZE.xs}>
{messageBoardState.message}
</Paragraph>
<Paragraph
style={{
flexWrap: "wrap",
flexShrink: 1
}}
color={colors.heading}
>
{messageBoardState.actionText}
</Paragraph>
</View>
{messageBoardState.actionText}
</Paragraph>
</View>
<View

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

@@ -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, {

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,8 +18,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useState } from "react";
import { ActivityIndicator, Platform, ScrollView, View } from "react-native";
import { ActivityIndicator, ScrollView, View } from "react-native";
import { LAUNCH_ROCKET } from "../../assets/images/assets";
import umami from "../../common/analytics";
import { db } from "../../common/database";
import { usePricing } from "../../hooks/use-pricing";
import { DDS } from "../../services/device-detection";
@@ -47,6 +48,7 @@ import { Walkthrough } from "../walkthroughs";
import { features } from "./features";
import { Group } from "./group";
import { PricingPlans } from "./pricing-plans";
import { Platform } from "react-native";
export const Component = ({ close, promo }) => {
const colors = useThemeStore((state) => state.colors);
@@ -58,6 +60,7 @@ export const Component = ({ close, promo }) => {
const onPress = async () => {
if (user) {
umami.pageView("/pro-plans", "/pro-screen");
presentSheet({
context: "pricing_plans",
component: (
@@ -66,6 +69,7 @@ export const Component = ({ close, promo }) => {
});
} else {
close();
umami.pageView("/signup", "/pro-screen");
setTimeout(() => {
eSendEvent(eOpenLoginDialog, AuthMode.trialSignup);
}, 400);

View File

@@ -17,9 +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, useState } from "react";
import React, { useEffect, useState } from "react";
import { ActivityIndicator, Platform, Text, View } from "react-native";
import * as RNIap from "react-native-iap";
import umami from "../../common/analytics";
import { db } from "../../common/database";
import { usePricing } from "../../hooks/use-pricing";
import {
@@ -47,6 +48,7 @@ import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { Walkthrough } from "../walkthroughs";
import { PricingItem } from "./pricing-item";
import { useCallback } from "react";
const promoCyclesMonthly = {
1: "first month",
@@ -139,6 +141,10 @@ export const PricingPlans = ({
setBuying(false);
return;
}
umami.pageView(
"/iap-native",
`${compact ? "pro-sheet" : "pro-screen"}/pro-plans`
);
await RNIap.requestSubscription(
product?.productId,
false,

View File

@@ -22,10 +22,12 @@ 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 { DDS } from "../../services/device-detection";
import { eSendEvent } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { useMenuStore } from "../../stores/use-menu-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { dWidth } from "../../utils";
import { COLORS_NOTE } from "../../utils/color-scheme";
import { refreshNotesPage } from "../../utils/events";
import { SIZE } from "../../utils/size";
@@ -33,7 +35,9 @@ import { PressableButton } from "../ui/pressable";
export const ColorTags = ({ item }) => {
const [note, setNote] = useState(item);
const setColorNotes = useMenuStore((state) => state.setColorNotes);
const isTablet = useSettingStore((state) => state.deviceMode) !== "mobile";
const dimensions = useSettingStore((state) => state.dimensions);
let width = dimensions.width > 600 ? 600 : 500;
const changeColor = async (color) => {
if (note.color === color.name) {
await db.notes.note(note.id).uncolor();
@@ -43,7 +47,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);
};
@@ -61,12 +71,11 @@ export const ColorTags = ({ item }) => {
key={color.value}
onPress={() => changeColor(color)}
customStyle={{
width: 30,
height: 30,
width: DDS.isTab ? width / 10 : dWidth / 9,
height: DDS.isTab ? width / 10 : dWidth / 9,
borderRadius: 100,
justifyContent: "center",
alignItems: "center",
marginRight: isTablet ? 10 : undefined
alignItems: "center"
}}
>
{note.color?.toLowerCase() === color.name ? (
@@ -81,11 +90,12 @@ export const ColorTags = ({ item }) => {
style={{
flexDirection: "row",
flexWrap: "wrap",
flexGrow: isTablet ? undefined : 1,
paddingHorizontal: 12,
paddingRight: 0,
width: "100%",
marginVertical: 10,
marginTop: 20,
alignItems: "center",
justifyContent: isTablet ? "center" : "space-between"
justifyContent: "space-between"
}}
>
{Object.keys(COLORS_NOTE).map(_renderColor)}

View File

@@ -43,41 +43,36 @@ export const DateMeta = ({ item }) => {
}
};
function getDateMeta() {
let keys = Object.keys(item);
if (keys.includes("dateEdited"))
keys.splice(
keys.findIndex((k) => k === "dateModified"),
1
);
return keys.filter((key) => key.startsWith("date") && key !== "date");
}
const renderItem = (key) => (
<View
key={key}
style={{
flexDirection: "row",
justifyContent: "space-between",
paddingVertical: 3
}}
>
<Paragraph size={SIZE.xs + 1} color={colors.icon}>
{getNameFromKey(key)}
</Paragraph>
<Paragraph size={SIZE.xs + 1} color={colors.icon}>
{timeConverter(item[key])}
</Paragraph>
</View>
);
const renderItem = (key) =>
key.startsWith("date") && key !== "date" ? (
<View
key={key}
style={{
flexDirection: "row",
justifyContent: "space-between",
paddingVertical: 3
}}
>
<Paragraph size={SIZE.xs} color={colors.icon}>
{getNameFromKey(key)}
</Paragraph>
<Paragraph size={SIZE.xs} color={colors.icon}>
{timeConverter(item[key])}
</Paragraph>
</View>
) : null;
return (
<View
style={{
paddingVertical: 5,
marginTop: 5,
borderTopWidth: 1,
borderTopColor: colors.nav,
paddingHorizontal: 12
}}
>
{getDateMeta().map(renderItem)}
{Object.keys(item).map(renderItem)}
</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";
@@ -26,62 +27,64 @@ import SearchService from "../../services/search";
import { useThemeStore } from "../../stores/use-theme-store";
import { COLORS_NOTE } from "../../utils/color-scheme";
import { SIZE } from "../../utils/size";
import SheetProvider from "../sheet-provider";
import { ReminderTime } from "../ui/reminder-time";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { ColorTags } from "./color-tags";
import { DateMeta } from "./date-meta";
import { DevMode } from "./dev-mode";
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 (
<View
style={{
height: 1,
backgroundColor: colors.nav,
width: "100%",
marginTop: top,
marginBottom: bottom
}}
/>
);
};
export const Properties = ({ close = () => {}, item, buttons = [] }) => {
import { Tags } from "./tags";
import { Topics } from "./topics";
import {
getUpcomingReminder
} from "@notesnook/core/collections/reminders";
import { ReminderTime } from "../ui/reminder-time";
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 reminders = db.relations.from(item, "reminder");
const current = getUpcomingReminder(reminders);
const onScrollEnd = () => {
getRef().current?.handleChildScrollEnd();
};
if (!item || !item.id) {
return (
<Paragraph style={{ marginVertical: 10, alignSelf: "center" }}>
Start writing to save your note.
</Paragraph>
);
}
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>
>
{!item || !item.id ? (
<Paragraph style={{ marginVertical: 10, alignSelf: "center" }}>
Start writing to save your note.
</Paragraph>
) : (
<View
style={{
marginTop: 5,
zIndex: 10
}}
>
<View
style={{
paddingHorizontal: 12,
marginTop: 5,
zIndex: 10
paddingHorizontal: 12
}}
>
<Heading size={SIZE.lg}>
@@ -93,8 +96,25 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
{alias}
</Heading>
{item.type === "note" ? (
<TagStrip close={close} item={item} />
{item.headline || item.description ? (
<Paragraph
style={{
marginBottom: 5
}}
numberOfLines={2}
color={colors.icon}
>
{(item.type === "notebook" ||
item.itemType === "notebook" ||
item.type === "reminder") &&
item?.description
? item.description
: null}
{(item.type === "note" || item.itemType === "note") &&
item?.headline
? item.headline
: null}
</Paragraph>
) : null}
{item.type === "reminder" ? (
@@ -104,52 +124,55 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
justifyContent: "flex-start",
borderWidth: 0,
height: 30,
alignSelf: "flex-start",
backgroundColor: "transparent",
paddingHorizontal: 0
alignSelf: "flex-start"
}}
fontSize={SIZE.xs + 1}
/>
) : null}
{item.type === "note" ? <Tags close={close} item={item} /> : null}
<Topics item={item} close={close} />
</View>
<Line top={12} />
{item.type === "note" ? (
<Notebooks note={item} close={close} />
) : null}
<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>
)}
/>
<View
style={{
borderTopWidth: 1,
borderColor: colors.nav
}}
/>
{item.type === "note" ? <ColorTags close={close} item={item} /> : null}
<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}
</ScrollView>
);
};
@@ -161,51 +184,50 @@ Properties.present = (item, buttons = [], isSheet) => {
switch (type) {
case "trash":
props[0] = item;
props.push(["delete", "restore"]);
props.push(["PermDelete", "Restore"]);
break;
case "note":
android = Platform.OS === "android" ? ["pin-to-notifications"] : [];
android = Platform.OS === "android" ? ["PinToNotif"] : [];
props[0] = db.notes.note(item.id).data;
props.push([
"notebooks",
"add-reminder",
"share",
"export",
"copy",
"publish",
"pin",
"favorite",
"attachments",
"lock-unlock",
"trash",
"remove-from-topic",
"remove-from-notebook",
"history",
"read-only",
"reminders",
"local-only",
"duplicate",
"Add to notebook",
"Add-Reminder",
"Share",
"Export",
"Copy",
"Publish",
"Pin",
"Favorite",
"Attachments",
"Vault",
"Delete",
"RemoveTopic",
"History",
"ReadOnly",
"Reminders",
"Local only",
"Duplicate",
...android,
...buttons
]);
break;
case "notebook":
props[0] = db.notebooks.notebook(item.id).data;
props.push(["edit-notebook", "pin", "add-shortcut", "trash"]);
props.push(["Edit Notebook", "Pin", "Add Shortcut", "Delete"]);
break;
case "topic":
props[0] = db.notebooks
.notebook(item.notebookId)
.topics.topic(item.id)._topic;
props.push(["move-notes", "edit-topic", "add-shortcut", "trash"]);
props.push(["Move notes", "Edit Topic", "Add Shortcut", "Delete"]);
break;
case "tag":
props[0] = db.tags.tag(item.id);
props.push(["add-shortcut", "trash", "rename-tag"]);
props.push(["Add Shortcut", "Delete", "Rename Tag"]);
break;
case "reminder": {
props[0] = db.reminders.reminder(item.id);
props.push(["edit-reminder", "trash", "disable-reminder"]);
props.push(["Edit reminder", "Delete", "ReminderOnOff"]);
break;
}
}
@@ -217,7 +239,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";
@@ -32,19 +33,19 @@ export const Items = ({ item, buttons, close }) => {
const colors = useThemeStore((state) => state.colors);
const dimensions = useSettingStore((state) => state.dimensions);
const actions = useActions({ item, close });
const data = actions.filter((i) => buttons.indexOf(i.id) > -1 && !i.hidden);
const data = actions.filter((i) => buttons.indexOf(i.name) > -1 && !i.hidden);
let width = dimensions.width > 600 ? 600 : dimensions.width;
let columnItemsCount = DDS.isLargeTablet() ? 7 : 5;
let columnItemWidth = DDS.isTab
? (width - 12) / columnItemsCount
: (width - 12) / columnItemsCount;
? (width - 24) / columnItemsCount
: (width - 24) / columnItemsCount;
const _renderRowItem = ({ item }) => (
<View
onPress={item.func}
key={item.id}
testID={"icon-" + item.id}
key={item.name}
testID={"icon-" + item.name}
style={{
alignItems: "center",
width: columnItemWidth,
@@ -57,7 +58,7 @@ export const Items = ({ item, buttons, close }) => {
customStyle={{
height: columnItemWidth - 12,
width: columnItemWidth - 12,
borderRadius: 10,
borderRadius: 5,
justifyContent: "center",
alignItems: "center",
textAlign: "center",
@@ -71,22 +72,21 @@ export const Items = ({ item, buttons, close }) => {
color={
item.on
? colors.accent
: item.id.match(/(delete|trash)/g)
: item.name === "Delete" || item.name === "PermDelete"
? colors.errorText
: colors.icon
}
/>
</PressableButton>
<Paragraph size={SIZE.xs} style={{ textAlign: "center" }}>
<Paragraph size={SIZE.xs + 1} style={{ textAlign: "center" }}>
{item.title}
</Paragraph>
</View>
);
const renderColumnItem = (item) => (
const renderColumnItem = ({ item }) => (
<Button
key={item.name + item.title}
buttonType={{
text: item.on
? colors.accent
@@ -108,107 +108,31 @@ export const Items = ({ item, buttons, close }) => {
/>
);
const renderTopBarItem = (item, index) => {
const isLast = index === topBarItems.length;
return (
<PressableButton
onPress={item.func}
key={item.id}
testID={"icon-" + item.id}
customStyle={{
alignSelf: "flex-start",
width: topBarItemWidth,
marginRight: isLast ? 0 : 10,
backgroundColor: "transparent",
paddingHorizontal: 0
}}
>
<PressableButton
onPress={item.func}
type={item.on ? "shade" : "gray"}
customStyle={{
height: topBarItemWidth,
width: topBarItemWidth,
justifyContent: "center",
alignItems: "center",
textAlign: "center",
textAlignVertical: "center",
marginBottom: DDS.isTab ? 7 : 3.5,
borderRadius: 100
}}
>
<Icon
name={item.icon}
size={DDS.isTab ? SIZE.xxl : SIZE.md + 4}
color={
item.on
? colors.accent
: item.name === "Delete" || item.name === "PermDelete"
? colors.errorText
: colors.icon
}
/>
</PressableButton>
<Paragraph size={SIZE.xxs + 1} style={{ textAlign: "center" }}>
{item.title}
</Paragraph>
</PressableButton>
);
};
const topBarItemsList = [
"pin",
"favorite",
"copy",
"share",
"export",
"lock-unlock",
"publish"
];
const topBarItems = data.filter(
(item) => topBarItemsList.indexOf(item.id) > -1
);
const bottomGridItems = data.filter(
(item) => topBarItemsList.indexOf(item.id) === -1
);
const topBarItemWidth =
(width - (topBarItems.length * 10 + 14)) / topBarItems.length;
return item.type === "note" ? (
<>
<ScrollView
horizontal
style={{
paddingHorizontal: 12,
paddingTop: 12
}}
>
{topBarItems.map(renderTopBarItem)}
</ScrollView>
<FlatList
data={bottomGridItems}
keyExtractor={(item) => item.title}
key={columnItemsCount + "key"}
numColumns={columnItemsCount}
disableVirtualization={true}
style={{
marginTop: item.type !== "note" ? 10 : 0,
paddingTop: 10,
marginLeft: 6
}}
contentContainerStyle={{
alignSelf: "center",
width: buttons.length < 5 ? "100%" : null,
paddingLeft: buttons.length < 5 ? 10 : 0
}}
renderItem={_renderRowItem}
/>
</>
<FlatList
data={data}
keyExtractor={(item) => item.title}
key={columnItemsCount + "key"}
numColumns={columnItemsCount}
style={{
marginTop: item.type !== "note" ? 10 : 0,
paddingTop: 10
}}
columnWrapperStyle={{
justifyContent: "flex-start"
}}
contentContainerStyle={{
alignSelf: "center",
width: buttons.length < 5 ? "100%" : null,
paddingLeft: buttons.length < 5 ? 10 : 0
}}
renderItem={_renderRowItem}
/>
) : (
<View data={data}>{data.map(renderColumnItem)}</View>
<FlatList
data={data}
keyExtractor={(item) => item.title}
renderItem={renderColumnItem}
/>
);
};

View File

@@ -23,49 +23,39 @@ 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 { 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 { PressableButton } from "../ui/pressable";
import Heading from "../ui/typography/heading";
export default function Notebooks({ note, close, full }) {
export default function Notebooks({ note, close }) {
const colors = useThemeStore((state) => state.colors);
const notebooks = useNotebookStore((state) => state.notebooks);
function getNotebooks(item) {
let filteredNotebooks = [];
const relations = db.relations.to(note, "notebook");
filteredNotebooks.push(
...relations.map((notebook) => ({
...notebook,
topics: []
}))
);
if (!item.notebooks || item.notebooks.length < 1) return filteredNotebooks;
for (let notebookReference of item.notebooks) {
let notebook = {
...(notebooks.find((item) => item.id === notebookReference.id) || {})
};
if (notebook.id) {
notebook.topics = notebook.topics.filter((topic) => {
return notebookReference.topics.findIndex((t) => t === topic.id) > -1;
});
const index = filteredNotebooks.findIndex(
(item) => item.id === notebook.id
);
if (index > -1) {
filteredNotebooks[index].topics = notebook.topics;
} else {
filteredNotebooks.push(notebook);
}
function getNotebooks(item) {
if (!item.notebooks || item.notebooks.length < 1) return [];
let notebooks = [];
for (let notebook of item.notebooks) {
let item_notebook = db.notebooks.notebook(notebook.id);
if (item_notebook) {
let data = {
id: notebook.id,
title: item_notebook.title,
topics: notebook.topics
.map((item) => {
let topic = item_notebook.topics.topic(item)?._topic;
if (!topic) return null;
return {
id: topic.id,
title: topic.title
};
})
.filter((i) => i !== null)
};
notebooks.push(data);
}
}
return filteredNotebooks;
return notebooks;
}
const noteNotebooks = getNotebooks(note);
const navigateNotebook = (id) => {
let item = db.notebooks.notebook(id)?.data;
@@ -78,134 +68,85 @@ export default function Notebooks({ note, close, full }) {
if (!item) return;
TopicNotes.navigate(item, true);
};
const renderItem = (item) => (
<View
key={item.id}
style={{
justifyContent: "flex-start",
paddingHorizontal: 12,
flexDirection: "row",
alignItems: "center",
flexShrink: 1,
flexGrow: 1,
padding: 6,
borderWidth: full ? 0 : 1,
borderColor: colors.nav,
borderRadius: 10,
backgroundColor: full ? "transparent" : colors.nav,
minHeight: 42
}}
>
<Icon
name="book-outline"
color={colors.accent}
size={SIZE.sm}
style={{
marginRight: 5
}}
/>
<Heading
numberOfLines={1}
style={{
maxWidth: "50%"
}}
size={SIZE.sm}
onPress={() => {
navigateNotebook(item.id);
close();
}}
>
{item.title}
</Heading>
<ScrollView
horizontal={true}
showsHorizontalScrollIndicator={false}
style={{
flexDirection: "row",
marginLeft: 8,
borderLeftColor: colors.nav,
borderLeftWidth: 1,
paddingLeft: 8
}}
>
{item.topics.map((topic) => (
<Button
key={topic.id}
onPress={() => {
navigateTopic(topic.id, item.id);
close();
}}
onLongPress={async () => {
await db.notes.removeFromNotebook(
{
id: item.id,
topic: topic.id
},
note
);
useNotebookStore.getState().setNotebooks();
Navigation.queueRoutesForUpdate();
ToastEvent.show({
heading: "Note removed from topic",
context: "local",
type: "success"
});
}}
title={topic.title}
type="gray"
height={30}
fontSize={SIZE.xs + 1}
icon="bookmark-outline"
style={{
marginRight: 5,
borderRadius: 100,
paddingHorizontal: 8
}}
/>
))}
<View style={{ width: 10 }} />
</ScrollView>
</View>
);
return noteNotebooks.length === 0 ? null : (
return !note.notebooks || note.notebooks.length === 0 ? null : (
<View
style={{
width: "100%",
borderRadius: 10,
marginTop: 6
borderTopWidth: 1,
borderTopColor: colors.nav
}}
>
{full
? noteNotebooks.map(renderItem)
: noteNotebooks.slice(0, 1).map(renderItem)}
{noteNotebooks.length > 1 && !full ? (
<Button
title={`See all linked notebooks`}
fontSize={SIZE.xs + 1}
style={{
alignSelf: "flex-end",
marginRight: 12,
paddingHorizontal: 0,
backgroundColor: "transparent"
}}
type="gray"
textStyle={{
textDecorationLine: "underline"
}}
height={20}
{getNotebooks(note).map((item) => (
<PressableButton
key={item.id}
onPress={() => {
presentSheet({
context: "properties",
component: (ref, close) => (
<Notebooks note={note} close={close} full={true} />
)
});
navigateNotebook(item.id);
close();
}}
/>
) : undefined}
customStyle={{
justifyContent: "flex-start",
paddingHorizontal: 12,
flexDirection: "row",
alignItems: "center",
flexShrink: 1,
flexGrow: 1,
marginTop: 5,
paddingVertical: 6
}}
>
<Icon
name="book-outline"
color={colors.accent}
size={SIZE.sm + 1}
style={{
marginRight: 5
}}
/>
<Heading
numberOfLines={1}
style={{
maxWidth: "50%"
}}
size={SIZE.sm}
>
{item.title}
</Heading>
<ScrollView
horizontal={true}
showsHorizontalScrollIndicator={false}
style={{
flexDirection: "row",
marginLeft: 8,
borderLeftColor: colors.nav,
borderLeftWidth: 1,
paddingLeft: 8
}}
>
{item.topics.map((topic) => (
<Button
key={topic.id}
onPress={() => {
navigateTopic(topic.id, item.id);
close();
}}
title={topic.title}
type="grayBg"
height={22}
fontSize={SIZE.xs + 1}
icon="bookmark-outline"
style={{
marginRight: 5,
borderRadius: 100,
paddingHorizontal: 8
}}
/>
))}
<View style={{ width: 10 }} />
</ScrollView>
</PressableButton>
))}
</View>
);
}

View File

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

View File

@@ -21,12 +21,12 @@ 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 }) => {
const colors = useThemeStore((state) => state.colors);
@@ -34,53 +34,41 @@ export const Tags = ({ item, close }) => {
<View
style={{
marginTop: 5,
paddingTop: 6,
paddingBottom: 6,
flexDirection: "row",
flexWrap: "wrap",
alignItems: "center",
paddingHorizontal: 12,
alignSelf: "center",
justifyContent: "space-between",
width: "100%"
marginBottom: 5
}}
>
<Button
onPress={async () => {
ManageTagsSheet.present(item);
}}
buttonType={{
text: colors.accent
}}
title="Add tags"
type="grayBg"
icon="plus"
iconPosition="right"
height={30}
fontSize={SIZE.xs + 1}
<View
style={{
marginRight: 5,
borderRadius: 100,
paddingHorizontal: 8
flexDirection: "row",
flexWrap: "wrap",
alignItems: "center"
}}
/>
<ColorTags item={item} />
</View>
) : null;
};
export const TagStrip = ({ item, close }) => {
return item.tags?.length > 0 ? (
<View
style={{
flexDirection: "row",
flexWrap: "wrap",
alignItems: "center"
}}
>
{item.tags.map((tag) =>
tag ? <TagItem key={tag} tag={tag} close={close} /> : null
)}
>
<Button
onPress={async () => {
close();
await sleep(300);
eSendEvent(eOpenTagsDialog, item);
}}
buttonType={{
text: colors.accent
}}
title="Add tags"
type="grayBg"
icon="plus"
iconPosition="right"
height={25}
fontSize={SIZE.xs + 1}
style={{
marginRight: 5,
borderRadius: 100,
paddingHorizontal: 8
}}
/>
{item.tags.map((item) =>
item ? <TagItem key={item} tag={item} close={close} /> : null
)}
</View>
</View>
) : null;
};
@@ -95,23 +83,23 @@ const TagItem = ({ tag, close }) => {
};
const style = {
paddingHorizontal: 0,
paddingHorizontal: 8,
marginVertical: 5,
borderRadius: 100,
marginRight: 10,
marginTop: 0,
backgroundColor: "transparent"
marginRight: 5
};
return (
<Button
onPress={onPress}
title={"#" + tag}
// buttonType={{
// text: colors.accent
// }}
type="grayBg"
height={20}
height={25}
fontSize={SIZE.xs + 1}
style={style}
textStyle={{
textDecorationLine: "underline"
}}
/>
);
};

View File

@@ -21,18 +21,17 @@ 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";
@@ -48,9 +47,7 @@ export const SelectionHeader = React.memo(() => {
const currentScreen = useNavigationStore((state) => state.currentScreen);
const screen = currentScreen.name;
const insets = useGlobalSafeAreaInsets();
SearchService.prepareSearch?.();
const allItems = SearchService.getSearchInformation()?.get() || [];
const allSelected = allItems.length === selectedItemsList.length;
useEffect(() => {
if (selectionMode) {
tabBarRef.current?.lock();
@@ -64,7 +61,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();
}
};
@@ -76,7 +79,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({
@@ -103,7 +115,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();
}
},
@@ -128,15 +149,14 @@ export const SelectionHeader = React.memo(() => {
<View
style={{
width: "100%",
height: Platform.OS === "android" ? 50 + insets.top : 50,
height: 50 + insets.top,
paddingTop: Platform.OS === "android" ? insets.top : null,
backgroundColor: colors.bg,
justifyContent: "space-between",
alignItems: "center",
flexDirection: "row",
zIndex: 999,
paddingHorizontal: 12,
marginVertical: 10
paddingHorizontal: 12
}}
>
<View
@@ -177,7 +197,7 @@ export const SelectionHeader = React.memo(() => {
}}
>
<Heading size={SIZE.md} color={colors.accent}>
{selectedItemsList.length}
{selectedItemsList.length + " Selected"}
</Heading>
</View>
</View>
@@ -188,21 +208,20 @@ export const SelectionHeader = React.memo(() => {
alignItems: "center"
}}
>
<IconButton
{/* <ActionIcon
onPress={async () => {
useSelectionStore
.getState()
.setAll(allSelected ? [] : [...allItems]);
// await sleep(100);
// eSendEvent(eOpenMoveNoteDialog);
useSelectionStore.getState().setAll([...SearchService.getSearchInformation().data]);
}}
tooltipText="Select all"
tooltipPosition={4}
customStyle={{
marginLeft: 10
}}
color={allSelected ? colors.accent : colors.pri}
color={colors.pri}
name="select-all"
size={SIZE.xl}
/>
/> */}
{screen === "Trash" ||
screen === "Notebooks" ||
@@ -213,14 +232,12 @@ export const SelectionHeader = React.memo(() => {
onPress={async () => {
//setSelectionMode(false);
await sleep(100);
MoveNoteSheet.present();
eSendEvent(eOpenMoveNoteDialog);
}}
customStyle={{
marginLeft: 10
}}
color={colors.pri}
tooltipText="Add to notebooks"
tooltipPosition={4}
name="plus"
size={SIZE.xl}
/>
@@ -228,8 +245,6 @@ export const SelectionHeader = React.memo(() => {
onPress={async () => {
ExportNotesSheet.present(selectedItemsList);
}}
tooltipText="Export"
tooltipPosition={4}
customStyle={{
marginLeft: 10
}}
@@ -240,41 +255,35 @@ 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"
}`}
tooltipPosition={4}
testID="select-minus"
color={colors.pri}
name="minus"
@@ -288,8 +297,6 @@ export const SelectionHeader = React.memo(() => {
customStyle={{
marginLeft: 10
}}
tooltipText="Remove from favorites"
tooltipPosition={4}
color={colors.pri}
name="star-off"
size={SIZE.xl}
@@ -320,8 +327,6 @@ export const SelectionHeader = React.memo(() => {
return;
}}
color={colors.pri}
tooltipText="Move to trash"
tooltipPosition={1}
name="delete"
size={SIZE.xl}
/>
@@ -336,8 +341,6 @@ export const SelectionHeader = React.memo(() => {
color={colors.pri}
onPress={restoreItem}
name="delete-restore"
tooltipText="Restore"
tooltipPosition={4}
size={SIZE.xl - 3}
/>
@@ -347,8 +350,6 @@ export const SelectionHeader = React.memo(() => {
}}
color={colors.pri}
onPress={deleteItem}
tooltipText="Delete"
tooltipPosition={4}
name="delete"
size={SIZE.xl - 3}
/>

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(500);
}
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,12 +197,12 @@ 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={45}
@@ -205,14 +214,15 @@ const SheetProvider = ({ context = "global" }) => {
/>
) : 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
}}
@@ -221,7 +231,7 @@ const SheetProvider = ({ context = "global" }) => {
/>
))}
{data?.learnMore ? (
{dialogData?.learnMore ? (
<Paragraph
style={{
alignSelf: "center",
@@ -229,7 +239,7 @@ const SheetProvider = ({ context = "global" }) => {
textDecorationLine: "underline"
}}
size={SIZE.xs}
onPress={data.learnMorePress}
onPress={dialogData.learnMorePress}
color={colors.icon}
>
<Icon
@@ -237,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,166 +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

@@ -1,29 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { createContext, useContext } from "react";
export const SelectionContext = createContext({
toggleSelection: (item) => null,
deselect: (item) => null,
select: (item) => null,
deselectAll: () => null
});
export const SelectionProvider = SelectionContext.Provider;
export const useSelectionContext = () => useContext(SelectionContext);

View File

@@ -1,76 +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, { useCallback, useEffect, useRef, useState } from "react";
import { FlatList } from "react-native-actions-sheet";
import { db } from "../../../common/database";
import { ListHeaderInputItem } from "./list-header-item.js";
export const FilteredList = ({
data,
itemType,
onAddItem,
hasHeaderSearch,
listRef,
...restProps
}) => {
const [filtered, setFiltered] = useState(data);
const query = useRef();
const onChangeText = useCallback(
(value) => {
query.current = value;
try {
if (!value) return setFiltered(data);
const results = db.lookup[itemType + "s"]([...data], value);
setFiltered(results);
} catch (e) {
console.warn(e.message);
}
},
[data, itemType]
);
const onSubmit = async (value) => {
return await onAddItem(value);
};
useEffect(() => {
onChangeText(query.current);
}, [data, onChangeText]);
return (
<FlatList
{...restProps}
data={filtered}
ref={listRef}
ListHeaderComponent={
hasHeaderSearch ? (
<ListHeaderInputItem
onSubmit={onSubmit}
onChangeText={onChangeText}
itemType={itemType}
testID={"list-input" + itemType}
placeholder={`Search or add a new ${itemType}`}
/>
) : null
}
keyboardShouldPersistTaps="always"
keyboardDismissMode="none"
/>
);
};

View File

@@ -17,65 +17,206 @@ 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, useEffect, useState } from "react";
import { Keyboard, TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { FlatList } from "react-native-gesture-handler";
import { notesnook } from "../../../../e2e/test.ids";
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 { getTotalNotes } from "../../../utils";
import { eOpenMoveNoteDialog } from "../../../utils/events";
import layoutmanager from "../../../utils/layout-manager";
import { SIZE } from "../../../utils/size";
import { Dialog } from "../../dialog";
import DialogHeader from "../../dialog/dialog-header";
import { presentDialog } from "../../dialog/functions";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
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 { SelectionProvider } from "./context";
import { FilteredList } from "./filtered-list";
import { ListItem } from "./list-item";
import { useItemSelectionStore } from "./store";
import { useCallback } from "react";
let newNotebookTitle = null;
const notebookInput = createRef();
const actionSheetRef = createRef();
const AddToNotebookSheet = () => {
const [visible, setVisible] = useState(false);
const [note, setNote] = useState(null);
const MoveNoteSheet = ({ note, actionSheetRef }) => {
function open(note) {
setNote(note);
setVisible(true);
actionSheetRef.current?.setModalVisible(true);
}
useEffect(() => {
eSubscribeEvent(eOpenMoveNoteDialog, open);
return () => {
eUnSubscribeEvent(eOpenMoveNoteDialog, open);
};
}, []);
const _onClose = () => {
setVisible(false);
newNotebookTitle = null;
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 notebooks = useNotebookStore((state) =>
state.notebooks.filter((n) => n?.type === "notebook")
);
const dimensions = useSettingStore((state) => state.dimensions);
const selectedItemsList = useSelectionStore(
(state) => state.selectedItemsList
);
const setNotebooks = useNotebookStore((state) => state.setNotebooks);
const multiSelect = useItemSelectionStore((state) => state.multiSelect);
const onAddNotebook = async (title) => {
if (!title || title.trim().length === 0) {
ToastEvent.show({
const [expanded, setExpanded] = useState("");
const [notebookInputFocused, setNotebookInputFocused] = useState(false);
const [noteExists, setNoteExists] = useState([]);
const addNewNotebook = async () => {
if (!newNotebookTitle || newNotebookTitle.trim().length === 0)
return ToastEvent.show({
heading: "Notebook title is required",
type: "error",
context: "local"
});
let id = await db.notebooks.add({
title: newNotebookTitle,
description: null,
topics: [],
id: null
});
setExpanded(id);
openAddTopicDialog(db.notebooks.notebook(id).data);
notebookInput.current?.clear();
notebookInput.current?.blur();
setNotebooks();
updateNoteExists();
};
const addNewTopic = async (value, item) => {
if (!value || value.trim().length === 0) {
ToastEvent.show({
heading: "Topic title is required",
type: "error",
context: "local"
});
return false;
}
await db.notebooks.add({
title: title,
description: null,
topics: [],
id: null
});
await db.notebooks.notebook(item.id).topics.add(value);
setNotebooks();
updateNoteExists();
return true;
};
const handlePress = async (item) => {
let noteIds =
selectedItemsList.length > 0
? selectedItemsList.map((n) => n.id)
: [note?.id];
if (getCount(item)) {
await db.notes.removeFromNotebook(
{
id: item.notebookId,
topic: item.id
},
...noteIds
);
} else {
await db.notes.addToNotebook(
{
topic: item.id,
id: item.notebookId,
rebuildCache: true
},
...noteIds
);
}
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes"
);
setNotebooks();
updateNoteExists();
SearchService.updateAndSearch();
};
useEffect(() => {
updateNoteExists();
}, [updateNoteExists]);
const updateNoteExists = useCallback(() => {
if (!note?.id && selectedItemsList?.length === 0) return;
let notes =
selectedItemsList.length > 0
? selectedItemsList.map((n) => n.id)
: [note?.id];
let ids = [];
let notebooks = db.notebooks.all;
for (let i = 0; i < notebooks.length; i++) {
if (notebooks[i].topics) {
for (let t = 0; t < notebooks[i].topics.length; t++) {
let topic = notebooks[i].topics[t];
if (topic.type !== "topic") continue;
for (let id of notes) {
const noteIds = db.notes?.topicReferences.get(topic.id);
if (noteIds.indexOf(id) > -1) {
if (ids.indexOf(notebooks[i].id) === -1) {
ids.push(notebooks[i].id);
}
if (ids.indexOf(topic.id) === -1) ids.push(topic.id);
}
}
}
}
}
setNoteExists(ids);
}, [note?.id, selectedItemsList]);
const openAddTopicDialog = (item) => {
presentDialog({
context: "move_note",
@@ -85,218 +226,27 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
paragraph: "Add a new topic in " + item.title,
positiveText: "Add",
positivePress: (value) => {
return onAddTopic(value, item);
return addNewTopic(value, item);
}
});
};
const onAddTopic = useCallback(
async (value, item) => {
if (!value || value.trim().length === 0) {
ToastEvent.show({
heading: "Topic title is required",
type: "error",
context: "local"
});
return false;
}
await db.notebooks.notebook(item.id).topics.add(value);
setNotebooks();
return true;
},
[setNotebooks]
);
// eslint-disable-next-line react-hooks/exhaustive-deps
const getSelectedNotesCountInItem = React.useCallback(
(item) => {
switch (item.type) {
case "notebook": {
const notes = db.relations.from(item, "note");
if (notes.length === 0) return 0;
let count = 0;
selectedItemsList.forEach((item) =>
notes.findIndex((note) => note.id === item.id) > -1
? count++
: undefined
);
return count;
}
case "topic": {
const noteIds = db.notes?.topicReferences.get(item.id);
let count = 0;
selectedItemsList.forEach((item) =>
noteIds.indexOf(item.id) > -1 ? count++ : undefined
);
return count;
}
}
},
[selectedItemsList]
);
useEffect(() => {
resetItemState();
return () => {
useItemSelectionStore.getState().setMultiSelect(false);
useItemSelectionStore.getState().setItemState({});
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const resetItemState = useCallback(
(state) => {
const itemState = {};
const notebooks = db.notebooks.all;
let count = 0;
for (let notebook of notebooks) {
itemState[notebook.id] = state
? state
: areAllSelectedItemsInNotebook(notebook, selectedItemsList)
? "selected"
: getSelectedNotesCountInItem(notebook, selectedItemsList) > 0
? "intermediate"
: "deselected";
if (itemState[notebook.id] === "selected") {
count++;
contextValue.select(notebook);
} else {
contextValue.deselect(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") {
count++;
contextValue.select(topic);
} else {
contextValue.deselect(topic);
}
}
}
if (count > 1) {
useItemSelectionStore.getState().setMultiSelect(true);
} else {
useItemSelectionStore.getState().setMultiSelect(false);
}
useItemSelectionStore.getState().setItemState(itemState);
},
[contextValue, getSelectedNotesCountInItem, selectedItemsList]
);
const getItemsForItem = (item) => {
switch (item.type) {
case "notebook":
return item.topics?.filter((t) => t.type === "topic");
}
};
function areAllSelectedItemsInNotebook(notebook, items) {
const notes = db.relations.from(notebook, "note");
if (notes.length === 0) return false;
return items.every((item) => {
return notes.find((note) => note.id === item.id);
});
}
function areAllSelectedItemsInTopic(topic, items) {
return items.every((item) => {
return db.notes.topicReferences.get(topic.id).indexOf(item.id) > -1;
});
}
const updateItemState = useCallback(function (item, state) {
const itemState = useItemSelectionStore.getState().itemState;
const mergeState = {
[item.id]: state
};
useItemSelectionStore.getState().setItemState({
...itemState,
...mergeState
});
}, []);
const contextValue = useMemo(
() => ({
toggleSelection: (item) => {
const itemState = useItemSelectionStore.getState().itemState;
if (itemState[item.id] === "selected") {
updateItemState(item, "deselected");
} else {
updateItemState(item, "selected");
}
},
deselect: (item) => {
updateItemState(item, "deselected");
},
select: (item) => {
updateItemState(item, "selected");
},
deselectAll: (state) => {
resetItemState(state);
}
}),
[resetItemState, updateItemState]
);
const getItemFromId = (id) => {
for (const nb of notebooks) {
if (nb.id === id) return nb;
for (const tp of nb.topics) {
if (tp.id === id) return tp;
const getCount = (topic) => {
if (!topic) return;
let notes =
selectedItemsList.length > 0
? selectedItemsList.map((n) => n.id)
: [note?.id];
let count = 0;
for (let id of notes) {
const noteIds = db.notes?.topicReferences.get(topic.id);
if (noteIds.indexOf(id) > -1) {
count++;
}
}
};
const onSave = async () => {
const noteIds = note ? [note.id] : selectedItemsList.map((n) => n.id);
const itemState = useItemSelectionStore.getState().itemState;
for (const id in itemState) {
const item = getItemFromId(id);
if (itemState[id] === "selected") {
if (item.type === "notebook") {
for (let noteId of noteIds) {
db.relations.add(item, { id: noteId, type: "note" });
}
} else {
await db.notes.addToNotebook(
{
topic: item.id,
id: item.notebookId,
rebuildCache: true
},
...noteIds
);
}
} else if (itemState[id] === "deselected") {
if (item.type === "notebook") {
for (let noteId of noteIds) {
db.relations.unlink(item, { id: noteId, type: "note" });
}
} else {
await db.notes.removeFromNotebook(
{
id: item.notebookId,
topic: item.id,
rebuildCache: true
},
...noteIds
);
}
}
}
Navigation.queueRoutesForUpdate();
setNotebooks();
eSendEvent(eOnTopicSheetUpdate);
SearchService.updateAndSearch();
actionSheetRef.current?.hide();
return count > 0 && notes.length > 0
? `${count} of ${notes.length} selected notes exist in this topic. (Tap to remove)`
: null;
};
return (
@@ -316,163 +266,216 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
<View
style={{
paddingHorizontal: 12,
justifyContent: "space-between",
flexDirection: "row",
alignItems: "flex-start"
justifyContent: "space-between"
}}
>
<DialogHeader
style={{
minHeight: 10,
flexShrink: 1
}}
title="Select notebooks"
paragraph={
!multiSelect
? "Long press to enable multi-select."
: "Select topics you want to add note(s) to."
}
/>
<Button
height={35}
style={{
borderRadius: 100,
paddingHorizontal: 24,
alignSelf: "flex-start"
}}
title="Save"
type={"accent"}
onPress={onSave}
title="Add to notebook"
paragraph={"Add your notes to notebooks to find them easily."}
/>
</View>
<View
<FlatList
nestedScrollEnabled={true}
onMomentumScrollEnd={() => {
actionSheetRef.current?.handleChildScrollEnd();
}}
keyboardShouldPersistTaps="always"
keyboardDismissMode="none"
data={notebooks}
ListFooterComponent={
<View
style={{
height: 200
}}
/>
}
ListHeaderComponent={
<View
style={{
width: "100%",
marginTop: 10
}}
>
<Input
fwdRef={notebookInput}
onChangeText={(value) => {
newNotebookTitle = value;
}}
testID={notesnook.ids.dialogs.addTo.addNotebook}
blurOnSubmit={false}
onFocusInput={() => {
setNotebookInputFocused(true);
}}
onBlurInput={() => {
setNotebookInputFocused(false);
}}
button={{
icon: "check",
color: notebookInputFocused ? colors.accent : colors.icon,
onPress: addNewNotebook
}}
onSubmit={addNewNotebook}
placeholder="Create a new notebook"
/>
</View>
}
style={{
paddingHorizontal: 12
}}
>
<Button
title="Reset selection"
height={30}
style={{
alignSelf: "flex-start",
paddingHorizontal: 0,
width: "100%",
marginTop: 6
}}
type="grayAccent"
onPress={() => {
resetItemState();
}}
/>
</View>
<SelectionProvider value={contextValue}>
<FilteredList
style={{
paddingHorizontal: 12,
maxHeight: dimensions.height * 0.85
}}
ListEmptyComponent={
notebooks.length > 0 ? null : (
renderItem={({ item }) => (
<View
style={{
borderWidth: 1,
borderColor: expanded ? colors.nav : "transparent",
borderRadius: 6,
overflow: "hidden",
marginBottom: 10
}}
>
<PressableButton
onPress={() => {
if (!item.topics || item.topics.length === 0) {
setExpanded(item.id);
openAddTopicDialog(item);
return;
}
layoutmanager.withAnimation(200);
setExpanded(item.id === expanded ? null : item.id);
setNotebookInputFocused(false);
}}
type="grayBg"
customStyle={{
height: 50,
width: "100%",
borderRadius: 5,
alignItems: "flex-start"
}}
>
<View
style={{
width: "100%",
height: "100%",
justifyContent: "center",
alignItems: "center"
height: 50,
justifyContent: "space-between",
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 12
}}
>
<Icon name="book-outline" color={colors.icon} size={100} />
<Paragraph style={{ marginBottom: 10 }}>
You do not have any notebooks.
</Paragraph>
<View>
<Heading
color={
noteExists.indexOf(item.id) > -1 ? colors.accent : null
}
size={SIZE.md}
>
{item.title}
</Heading>
{item.topics?.length > 0 ? (
<Paragraph size={SIZE.xs} color={colors.icon}>
{getTotalNotes(item) + " notes" + " & "}
{item.topics.length === 1
? item.topics.length + " topic"
: item.topics.length + " topics"}
</Paragraph>
) : null}
</View>
<IconButton
name={expanded === item.id ? "plus" : "chevron-down"}
color={expanded === item.id ? colors.accent : colors.pri}
size={SIZE.xl}
onPress={() => {
if (expanded !== item.id) {
setExpanded(item.id);
return;
}
layoutmanager.withAnimation(200);
setExpanded(item.id);
openAddTopicDialog(item);
}}
/>
</View>
)
}
data={notebooks}
hasHeaderSearch={true}
renderItem={({ item, index }) => (
<ListItem
item={item}
key={item.id}
index={index}
hasNotes={getSelectedNotesCountInItem(item) > 0}
sheetRef={actionSheetRef}
infoText={
<>
{item.topics.length === 1
? item.topics.length + " topic"
: item.topics.length + " topics"}
</>
}
getListItems={getItemsForItem}
getSublistItemProps={(topic) => ({
hasNotes: getSelectedNotesCountInItem(topic) > 0,
style: {
marginBottom: 0,
height: 40
},
onPress: (item) => {
const itemState =
useItemSelectionStore.getState().itemState;
const currentState = itemState[item.id];
if (currentState !== "selected") {
resetItemState("deselected");
contextValue.select(item);
} else {
contextValue.deselect(item);
}
},
key: item.id,
type: "transparent"
})}
icon={(expanded) => ({
name: expanded ? "chevron-up" : "chevron-down",
color: expanded ? colors.accent : colors.pri
})}
onScrollEnd={() => {
actionSheetRef.current?.handleChildScrollEnd();
}}
hasSubList={true}
hasHeaderSearch={false}
type="grayBg"
sublistItemType="topic"
onAddItem={(title) => {
return onAddTopic(title, item);
}}
onAddSublistItem={(item) => {
openAddTopicDialog(item);
}}
onPress={(item) => {
const itemState = useItemSelectionStore.getState().itemState;
const currentState = itemState[item.id];
if (currentState !== "selected") {
resetItemState("deselected");
contextValue.select(item);
} else {
contextValue.deselect(item);
}
}}
/>
)}
itemType="notebook"
onAddItem={async (title) => {
return await onAddNotebook(title);
}}
ListFooterComponent={<View style={{ height: 20 }} />}
/>
</SelectionProvider>
</PressableButton>
{expanded === item.id ? (
<FlatList
nestedScrollEnabled
data={item.topics?.filter((t) => t.type === "topic")}
keyboardShouldPersistTaps="always"
keyboardDismissMode="none"
onMomentumScrollEnd={() => {
actionSheetRef.current?.handleChildScrollEnd();
}}
style={{
width: "100%",
alignSelf: "flex-end",
maxHeight: 500
}}
renderItem={({ item, index }) => (
<PressableButton
onPress={() => handlePress(item, index)}
type="gray"
customStyle={{
minHeight: 50,
borderTopWidth: index === 0 ? 0 : 1,
borderTopColor: index === 0 ? null : colors.nav,
width: "100%",
borderRadius: 0,
alignItems: "center",
flexDirection: "row",
paddingHorizontal: 12,
justifyContent: "space-between",
paddingVertical: 12
}}
>
<View>
<Paragraph color={colors.heading}>
{item.title}
</Paragraph>
<Paragraph color={colors.icon} size={SIZE.xs}>
{getTotalNotes(item) + " notes"}
</Paragraph>
{getCount(item) ? (
<View
style={{
backgroundColor: colors.nav,
borderRadius: 5,
paddingHorizontal: 5,
paddingVertical: 2,
marginTop: 5
}}
>
<Paragraph color={colors.icon} size={SIZE.xs}>
{getCount(item)}
</Paragraph>
</View>
) : null}
</View>
{noteExists.indexOf(item.id) > -1 ? (
<Button
onPress={() => handlePress(item, index)}
icon="check"
testID="icon-check"
iconColor={colors.accent}
iconSize={SIZE.lg}
height={35}
width={35}
style={{
borderRadius: 100,
paddingHorizontal: 0
}}
/>
) : null}
</PressableButton>
)}
/>
) : null}
</View>
)}
/>
</View>
</>
);
};
MoveNoteSheet.present = (note) => {
presentSheet({
component: (ref) => <MoveNoteSheet actionSheetRef={ref} note={note} />,
enableGesturesInScrollView: false,
noBottomPadding: true
});
};
export default MoveNoteSheet;

View File

@@ -1,87 +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 { View } from "react-native";
import { useThemeStore } from "../../../stores/use-theme-store";
import Input from "../../ui/input";
import Paragraph from "../../ui/typography/paragraph";
export const ListHeaderInputItem = ({
onSubmit,
onChangeText,
placeholder,
testID
}) => {
const [focused, setFocused] = useState(false);
const colors = useThemeStore((state) => state.colors);
const [inputValue, setInputValue] = useState();
const inputRef = useRef();
return (
<View
style={{
width: "100%",
marginTop: 10,
marginBottom: 5
}}
>
<Input
fwdRef={inputRef}
onChangeText={(value) => {
setInputValue(value);
onChangeText?.(value);
}}
testID={testID}
blurOnSubmit={false}
onFocusInput={() => {
setFocused(true);
}}
onBlurInput={() => {
setFocused(false);
}}
button={{
icon: inputValue ? "plus" : "magnify",
color: focused ? colors.accent : colors.icon,
onPress: async () => {
const result = await onSubmit(inputValue);
if (result) {
inputRef.current?.blur();
}
}
}}
placeholder={placeholder}
marginBottom={5}
/>
{inputValue ? (
<View
style={{
backgroundColor: colors.shade,
padding: 5,
borderRadius: 5,
marginBottom: 10
}}
>
<Paragraph color={colors.accent}>
Tap on + to add {`"${inputValue}"`}
</Paragraph>
</View>
) : null}
</View>
);
};

View File

@@ -1,292 +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, 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";
import { PressableButton } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { useSelectionContext } from "./context";
import { FilteredList } from "./filtered-list";
import { useItemSelectionStore } from "./store";
const SelectionIndicator = ({
item,
hasNotes,
selectItem,
onPress,
onChange
}) => {
const itemState = useItemSelectionStore((state) => state.itemState[item.id]);
const multiSelect = useItemSelectionStore((state) => state.multiSelect);
const isSelected = itemState === "selected";
const isIntermediate = itemState === "intermediate";
const isRemoved = !isSelected && hasNotes;
const colors = 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 = ({
item,
index,
icon,
infoText,
hasSubList,
onPress,
onScrollEnd,
getListItems,
style,
type,
sublistItemType,
onAddItem,
getSublistItemProps,
hasHeaderSearch,
onAddSublistItem,
hasNotes,
onChange,
sheetRef
}) => {
const { toggleSelection } = useSelectionContext();
const multiSelect = useItemSelectionStore((state) => state.multiSelect);
const [showSelectedIndicator, setShowSelectedIndicator] = useState(false);
const colors = useThemeStore((state) => state.colors);
const [expanded, setExpanded] = useState(false);
function selectItem() {
toggleSelection(item);
}
const getSelectedNotesCountInNotebookTopics = (item) => {
if (item.type === "topic") return;
let count = 0;
const noteIds = [];
for (let topic of item.topics) {
noteIds.push(...(db.notes?.topicReferences.get(topic.id) || []));
if (useItemSelectionStore.getState().itemState[topic.id] === "selected") {
count++;
}
}
useSelectionStore.getState().selectedItemsList.forEach((item) => {
if (noteIds.indexOf(item.id) > -1) {
count++;
}
});
return count;
};
useEffect(() => {
setShowSelectedIndicator(getSelectedNotesCountInNotebookTopics(item) > 0);
}, [item]);
const onChangeSubItem = () => {
setShowSelectedIndicator(getSelectedNotesCountInNotebookTopics(item) > 0);
};
return (
<View
style={{
overflow: "hidden",
marginBottom: 10,
...style
}}
>
<PressableButton
onPress={() => {
if (hasSubList) return setExpanded(!expanded);
if (multiSelect) return selectItem();
onPress?.(item);
}}
type={type}
onLongPress={() => {
useItemSelectionStore.getState().setMultiSelect(true);
selectItem();
}}
customStyle={{
height: style?.height || 50,
width: "100%",
alignItems: "flex-start"
}}
>
<View
style={{
width: "100%",
height: 50,
justifyContent: "space-between",
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 12
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center"
}}
>
<SelectionIndicator
hasNotes={hasNotes}
onPress={onPress}
item={item}
onChange={onChange}
selectItem={selectItem}
/>
<View>
{hasSubList && expanded ? (
<Heading size={SIZE.md}>{item.title}</Heading>
) : (
<Paragraph size={SIZE.sm}>{item.title}</Paragraph>
)}
{infoText ? (
<Paragraph size={SIZE.xs} color={colors.icon}>
{infoText}
</Paragraph>
) : null}
</View>
</View>
<View
style={{
flexDirection: "row",
alignItems: "center"
}}
>
{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}
size={SIZE.xl}
onPress={() => {
onAddSublistItem(item);
}}
/>
) : null}
{icon ? (
<IconButton
name={icon(expanded).name}
color={icon(expanded).color}
size={icon(expanded).size || SIZE.xl}
onPress={
hasSubList
? () => setExpanded(!expanded)
: icon(expanded).onPress
}
/>
) : null}
</View>
</View>
</PressableButton>
{expanded && hasSubList ? (
<FilteredList
nestedScrollEnabled
data={getListItems(item)}
keyboardShouldPersistTaps="always"
keyboardDismissMode="none"
onMomentumScrollEnd={onScrollEnd}
style={{
width: "95%",
alignSelf: "flex-end",
maxHeight: 250
}}
itemType={sublistItemType}
hasHeaderSearch={hasHeaderSearch}
renderItem={({ item, index }) => (
<ListItem
item={item}
{...getSublistItemProps(item)}
index={index}
onChange={onChangeSubItem}
onScrollEnd={onScrollEnd}
/>
)}
onAddItem={onAddItem}
/>
) : null}
</View>
);
};

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

@@ -277,6 +277,7 @@ const ExportNotesSheet = ({ notes, update }) => {
});
});
}}
height={50}
/>
<Button
title="Share"
@@ -300,6 +301,7 @@ const ExportNotesSheet = ({ notes, update }) => {
}).catch(console.log);
}
}}
height={50}
/>
<Button
title="Export in another format"
@@ -315,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,32 +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 { presentSheet, ToastEvent } 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";
const ManageTagsSheet = (props) => {
import { useCallback } from "react";
const ManageTagsSheet = () => {
const colors = useThemeStore((state) => state.colors);
const [note, setNote] = useState(props.note);
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, note, 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];
@@ -69,9 +88,26 @@ const ManageTagsSheet = (props) => {
setTags(combinedTags);
}, [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;
@@ -103,98 +139,115 @@ 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} note={note} setNote={setNote} />
))}
</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 = (note) => {
presentSheet({
component: (ref) => {
return <ManageTagsSheet actionSheetRef={ref} note={note} />;
}
});
};
export default ManageTagsSheet;
const TagItem = ({ tag, note, setNote }) => {
@@ -216,7 +269,13 @@ const TagItem = ({ tag, note, setNote }) => {
console.error(e);
}
setTimeout(() => {
Navigation.queueRoutesForUpdate();
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes"
);
}, 1);
};

View File

@@ -17,11 +17,11 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { FlashList } from "@shopify/flash-list";
import { NotebookType, NoteType, TopicType } from "app/utils/types";
import React, { RefObject, useState } from "react";
import { Platform, useWindowDimensions, View } from "react-native";
import { ActionSheetRef } from "react-native-actions-sheet";
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
import ActionSheet from "react-native-actions-sheet";
import { db } from "../../../common/database";
import {
eSendEvent,
@@ -58,7 +58,7 @@ export const MoveNotes = ({
}: {
notebook: NotebookType;
selectedTopic?: TopicType;
fwdRef: RefObject<ActionSheetRef>;
fwdRef: RefObject<ActionSheet>;
}) => {
const colors = useThemeStore((state) => state.colors);
const [currentNotebook, setCurrentNotebook] = useState(notebook);
@@ -122,7 +122,15 @@ export const MoveNotes = ({
db.notebooks?.notebook(currentNotebook.id).data as NotebookType
);
Navigation.queueRoutesForUpdate();
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes",
"Notebook",
"Notebooks"
);
return true;
};
@@ -269,6 +277,10 @@ export const MoveNotes = ({
)}
<FlashList
nestedScrollEnabled
onMomentumScrollEnd={() => {
fwdRef.current?.handleChildScrollEnd();
}}
ListEmptyComponent={
<View
style={{
@@ -310,7 +322,15 @@ export const MoveNotes = ({
},
...selectedNoteIds
);
Navigation.queueRoutesForUpdate();
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes",
"Notebook",
"Notebooks"
);
SearchService.updateAndSearch();
eSendEvent(eCloseSheet);
}}
@@ -325,7 +345,7 @@ export const MoveNotes = ({
MoveNotes.present = (notebook: NotebookType, topic: TopicType) => {
presentSheet({
component: (ref: RefObject<ActionSheetRef>) => (
component: (ref: RefObject<ActionSheet>) => (
<MoveNotes fwdRef={ref} notebook={notebook} selectedTopic={topic} />
)
});

View File

@@ -93,13 +93,7 @@ NewFeature.present = () => {
});
return;
}
if (!version || version === getVersion()) {
SettingsService.set({
version: getVersion()
});
return false;
}
if (version && version === getVersion()) return false;
SettingsService.set({
version: getVersion()
});

View File

@@ -18,14 +18,22 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Clipboard from "@react-native-clipboard/clipboard";
import React, { useRef, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { ActivityIndicator, TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../../common/database";
import { presentSheet, ToastEvent } from "../../../services/event-manager";
import {
eSubscribeEvent,
eUnSubscribeEvent,
ToastEvent
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { useAttachmentStore } from "../../../stores/use-attachment-store";
import { useThemeStore } from "../../../stores/use-theme-store";
import {
eClosePublishNoteDialog,
eOpenPublishNoteDialog
} from "../../../utils/events";
import { openLinkInBrowser } from "../../../utils/functions";
import { SIZE } from "../../../utils/size";
import DialogHeader from "../../dialog/dialog-header";
@@ -33,38 +41,75 @@ import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import SheetWrapper from "../../ui/sheet";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
const PublishNoteSheet = ({ note: item, update }) => {
let passwordValue = null;
const PublishNoteSheet = () => {
const colors = useThemeStore((state) => state.colors);
const [visible, setVisible] = useState(false);
const actionSheetRef = useRef();
const loading = useAttachmentStore((state) => state.loading);
const [selfDestruct, setSelfDestruct] = useState(false);
const [isLocked, setIsLocked] = useState(false);
const [note, setNote] = useState(item);
const [note, setNote] = useState(null);
const [publishing, setPublishing] = useState(false);
const publishUrl =
note &&
`https://monograph.notesnook.com/${db?.monographs.monograph(note?.id)}`;
const isPublished = note && db?.monographs.isPublished(note?.id);
const pwdInput = useRef();
const passwordValue = useRef();
useEffect(() => {
eSubscribeEvent(eOpenPublishNoteDialog, open);
eSubscribeEvent(eClosePublishNoteDialog, close);
return () => {
eUnSubscribeEvent(eOpenPublishNoteDialog, open);
eUnSubscribeEvent(eClosePublishNoteDialog, close);
};
}, []);
const open = (item) => {
if (!item) return;
setNote(item);
setPublishing(false);
setSelfDestruct(false);
setIsLocked(false);
setVisible(true);
passwordValue = null;
};
useEffect(() => {
if (visible) {
actionSheetRef.current?.show();
}
}, [visible]);
const close = () => {
passwordValue = null;
actionSheetRef.current?.hide();
};
const publishNote = async () => {
if (publishing) return;
setPublishLoading(true);
setPublishing(true);
try {
if (note?.id) {
if (isLocked && !passwordValue) return;
await db.monographs.publish(note.id, {
selfDestruct: selfDestruct,
password: isLocked && passwordValue.current
password: isLocked && passwordValue
});
setNote(db.notes.note(note.id)?.data);
Navigation.queueRoutesForUpdate();
setPublishLoading(false);
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes"
);
}
} catch (e) {
ToastEvent.show({
@@ -75,22 +120,23 @@ const PublishNoteSheet = ({ note: item, update }) => {
});
}
setPublishLoading(false);
};
const setPublishLoading = (value) => {
setPublishing(value);
setPublishing(false);
};
const deletePublishedNote = async () => {
if (publishing) return;
setPublishLoading(true);
setPublishing(true);
try {
if (note?.id) {
await db.monographs.unpublish(note.id);
setNote(db.notes.note(note.id)?.data);
Navigation.queueRoutesForUpdate();
setPublishLoading(false);
Navigation.queueRoutesForUpdate(
"Notes",
"Favorites",
"ColoredNotes",
"TaggedNotes",
"TopicNotes"
);
}
} catch (e) {
ToastEvent.show({
@@ -101,281 +147,267 @@ const PublishNoteSheet = ({ note: item, update }) => {
});
}
actionSheetRef.current?.hide();
setPublishLoading(false);
setPublishing(false);
};
return (
<View
style={{
width: "100%",
alignSelf: "center",
paddingHorizontal: 12
return !visible ? null : (
<SheetWrapper
centered={false}
fwdRef={actionSheetRef}
closeOnTouchBackdrop={!publishing}
gestureEnabled={!publishing}
onClose={async () => {
passwordValue = null;
setVisible(false);
}}
>
<DialogHeader
title={note?.title}
paragraph={`Anyone with the link${
isLocked ? " and password" : ""
} of the published note can view it.`}
/>
<View
style={{
width: "100%",
alignSelf: "center",
paddingHorizontal: 12
}}
>
<DialogHeader
title={note?.title}
paragraph={`Anyone with the link${
isLocked ? " and password" : ""
} of the published note can view it.`}
/>
{publishing ? (
<View
style={{
justifyContent: "center",
alignContent: "center",
height: 150,
width: "100%"
}}
>
<ActivityIndicator size={25} color={colors.accent} />
<Paragraph
{publishing ? (
<View
style={{
textAlign: "center"
justifyContent: "center",
alignContent: "center",
height: 150,
width: "100%"
}}
>
Please wait...
{loading && loading.current && loading.total
? `\nDownloading attachments (${
loading?.current / loading?.total
})`
: ""}
</Paragraph>
</View>
) : (
<>
{isPublished && (
<View
<ActivityIndicator size={25} color={colors.accent} />
<Paragraph
style={{
textAlign: "center"
}}
>
Please wait...
{loading && loading.current && loading.total
? `\nDownloading attachments (${
loading?.current / loading?.total
})`
: ""}
</Paragraph>
</View>
) : (
<>
{isPublished && (
<View
style={{
flexDirection: "row",
alignItems: "center",
marginTop: 15,
backgroundColor: colors.nav,
padding: 12,
borderRadius: 5
}}
>
<View
style={{
width: "100%",
flexShrink: 1
}}
>
<Heading size={SIZE.sm}>Published at:</Heading>
<Paragraph size={SIZE.xs} numberOfLines={1}>
{publishUrl}
</Paragraph>
<Paragraph
onPress={async () => {
try {
await openLinkInBrowser(publishUrl, colors.accent);
} catch (e) {
console.error(e);
}
}}
size={SIZE.xs}
style={{
marginTop: 5,
color: colors.pri
}}
>
<Icon color={colors.accent} name="open-in-new" /> Open in
browser
</Paragraph>
</View>
<IconButton
onPress={() => {
Clipboard.setString(publishUrl);
ToastEvent.show({
heading: "Note publish url copied",
type: "success",
context: "local"
});
}}
color={colors.accent}
size={SIZE.lg}
name="content-copy"
/>
</View>
)}
<Seperator />
<TouchableOpacity
onPress={() => {
if (publishing) return;
setIsLocked(!isLocked);
}}
activeOpacity={0.9}
style={{
flexDirection: "row",
alignItems: "center",
marginTop: 10,
backgroundColor: colors.nav,
padding: 12,
borderRadius: 5
marginBottom: 10
}}
>
<IconButton
onPress={() => {
if (publishing) return;
setIsLocked(!isLocked);
}}
color={isLocked ? colors.accent : colors.icon}
size={SIZE.lg}
name={
isLocked
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
<View
style={{
width: "100%",
flexShrink: 1
}}
>
<Heading size={SIZE.md}>Published at:</Heading>
<Paragraph size={SIZE.sm} numberOfLines={1}>
{publishUrl}
</Paragraph>
<Paragraph
onPress={async () => {
try {
await openLinkInBrowser(publishUrl, colors.accent);
} catch (e) {
console.error(e);
}
}}
size={SIZE.xs}
style={{
marginTop: 5,
color: colors.pri
}}
>
<Icon color={colors.accent} name="open-in-new" /> Open in
browser
<Heading size={SIZE.md}>Password protection</Heading>
<Paragraph>
Published note can only be viewed by someone with the
password.
</Paragraph>
</View>
</TouchableOpacity>
<IconButton
onPress={() => {
Clipboard.setString(publishUrl);
ToastEvent.show({
heading: "Note publish url copied",
type: "success",
context: "local"
});
}}
color={colors.accent}
size={SIZE.lg}
name="content-copy"
/>
</View>
)}
<TouchableOpacity
onPress={() => {
if (publishing) return;
setIsLocked(!isLocked);
}}
activeOpacity={0.9}
style={{
flexDirection: "row",
alignItems: "center",
marginBottom: 10,
backgroundColor: colors.nav,
paddingVertical: 12,
borderRadius: 5,
marginTop: 10
}}
>
<IconButton
onPress={() => {
if (publishing) return;
setIsLocked(!isLocked);
}}
color={isLocked ? colors.accent : colors.icon}
size={SIZE.xl}
name={
isLocked
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
<View
style={{
width: "100%",
flexShrink: 1
}}
>
<Heading size={SIZE.md}>Password protection</Heading>
<Paragraph>
Published note can only be viewed by someone with the password.
</Paragraph>
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
setSelfDestruct(!selfDestruct);
}}
activeOpacity={0.9}
style={{
flexDirection: "row",
alignItems: "center",
backgroundColor: colors.nav,
paddingVertical: 12,
borderRadius: 5
}}
>
<IconButton
<TouchableOpacity
onPress={() => {
setSelfDestruct(!selfDestruct);
}}
color={selfDestruct ? colors.accent : colors.icon}
size={SIZE.xl}
name={
selfDestruct
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
<View
style={{
width: "100%",
flexShrink: 1
}}
>
<Heading size={SIZE.md}>Self destruct</Heading>
<Paragraph>
Published note link will be automatically deleted once it is
viewed by someone.
</Paragraph>
</View>
</TouchableOpacity>
<View
style={{
width: "100%",
alignSelf: "center",
marginTop: 10
}}
>
{isLocked ? (
<>
<Input
fwdRef={pwdInput}
onChangeText={(value) => (passwordValue.current = value)}
blurOnSubmit
secureTextEntry
defaultValue={passwordValue.current}
placeholder="Enter Password"
/>
<Seperator half />
</>
) : null}
<View
activeOpacity={0.9}
style={{
flexDirection: "row",
width: "100%",
justifyContent: "center"
alignItems: "center"
}}
>
{isPublished && (
<IconButton
onPress={() => {
setSelfDestruct(!selfDestruct);
}}
color={selfDestruct ? colors.accent : colors.icon}
size={SIZE.lg}
name={
selfDestruct
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
<View
style={{
width: "100%",
flexShrink: 1
}}
>
<Heading size={SIZE.md}>Self destruct</Heading>
<Paragraph>
Published note link will be automatically deleted once it is
viewed by someone.
</Paragraph>
</View>
</TouchableOpacity>
<View
style={{
width: "100%",
alignSelf: "center",
marginTop: 10
}}
>
{isLocked ? (
<>
<Button
onPress={deletePublishedNote}
fontSize={SIZE.md}
type="error"
title="Unpublish"
style={{
width: "49%"
}}
<Input
fwdRef={pwdInput}
onChangeText={(value) => (passwordValue = value)}
blurOnSubmit
secureTextEntry
defaultValue={passwordValue}
placeholder="Enter Password"
/>
<Seperator half />
</>
)}
<Seperator half />
) : null}
<Button
onPress={publishNote}
fontSize={SIZE.md}
width="100%"
style={{
width: isPublished ? "49%" : 250,
borderRadius: isPublished ? 5 : 100
marginTop: 10
}}
height={50}
type="accent"
title={isPublished ? "Update" : "Publish"}
title={isPublished ? "Update published note" : "Publish note"}
/>
</View>
</View>
</>
)}
<Paragraph
color={colors.icon}
size={SIZE.xs}
style={{
textAlign: "center",
marginTop: 10,
textDecorationLine: "underline"
}}
onPress={async () => {
try {
await openLinkInBrowser(
"https://docs.notesnook.com/monographs/",
colors.accent
);
} catch (e) {
console.error(e);
}
}}
>
Learn more about Notesnook Monograph
</Paragraph>
</View>
{isPublished && (
<>
<Seperator half />
<Button
onPress={deletePublishedNote}
fontSize={SIZE.md}
width="100%"
height={50}
type="error"
title="Unpublish note"
/>
</>
)}
</View>
</>
)}
<Paragraph
color={colors.icon}
size={SIZE.xs}
style={{
textAlign: "center",
marginTop: 5,
textDecorationLine: "underline"
}}
onPress={async () => {
try {
await openLinkInBrowser(
"https://docs.notesnook.com/monographs/",
colors.accent
);
} catch (e) {
console.error(e);
}
}}
>
Learn more about Notesnook Monograph
</Paragraph>
</View>
</SheetWrapper>
);
};
PublishNoteSheet.present = (note) => {
presentSheet({
component: (ref, close, update) => (
<PublishNoteSheet
actionSheetRef={ref}
close={close}
update={update}
note={note}
/>
)
});
};
export default PublishNoteSheet;

View File

@@ -96,6 +96,7 @@ const RateAppSheet = () => {
onPress={rateApp}
fontSize={SIZE.md}
width="100%"
height={50}
type="accent"
title="Rate now (It takes only a second)"
/>
@@ -120,12 +121,14 @@ const RateAppSheet = () => {
fontSize={SIZE.md}
type="error"
width="48%"
height={50}
title="Never"
/>
<Button
onPress={onClose}
fontSize={SIZE.md}
width="48%"
height={50}
type="grayBg"
title="Later"
/>

View File

@@ -298,6 +298,7 @@ class RecoveryKeySheet extends React.Component {
width="100%"
type="grayAccent"
fontSize={SIZE.md}
height={50}
/>
<Seperator />
<Button
@@ -307,6 +308,7 @@ class RecoveryKeySheet extends React.Component {
type="grayAccent"
fontSize={SIZE.md}
icon="qrcode"
height={50}
/>
<Seperator />
<Button
@@ -316,6 +318,7 @@ class RecoveryKeySheet extends React.Component {
type="grayAccent"
icon="text"
fontSize={SIZE.md}
height={50}
/>
<Seperator />
@@ -326,6 +329,7 @@ class RecoveryKeySheet extends React.Component {
type="grayAccent"
icon="cloud"
fontSize={SIZE.md}
height={50}
/>
<Seperator />
@@ -345,6 +349,7 @@ class RecoveryKeySheet extends React.Component {
<Button
title="I have saved the key."
width="100%"
height={50}
type="error"
fontSize={SIZE.md}
onPress={this.close}

View File

@@ -18,13 +18,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { RefObject, useEffect, useState } from "react";
import { View } from "react-native";
import { ActionSheetRef } from "react-native-actions-sheet";
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
import ActionSheet from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../../common/database";
import {
presentSheet,
PresentSheetOptions
PresentSheetOptions,
presentSheet
} from "../../../services/event-manager";
import { Reminder } from "../../../services/notifications";
import { useRelationStore } from "../../../stores/use-relation-store";
@@ -38,7 +37,7 @@ import { PressableButtonProps } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph";
type RelationsListProps = {
actionSheetRef: RefObject<ActionSheetRef>;
actionSheetRef: RefObject<ActionSheet>;
close?: () => void;
update?: (options: PresentSheetOptions) => void;
item: { id: string; type: string };
@@ -63,6 +62,8 @@ const IconsByType = {
export const RelationsList = ({
actionSheetRef,
close,
update,
item,
referenceType,
relationType,
@@ -74,6 +75,7 @@ export const RelationsList = ({
const [items, setItems] = useState<Reminder[]>([]);
const colors = useThemeStore((state) => state.colors);
const hasNoRelations = !items || items.length === 0;
useEffect(() => {
setItems(
db.relations?.[relationType]?.(
@@ -121,7 +123,6 @@ export const RelationsList = ({
) : (
<List
listData={items}
ScrollComponent={FlashList}
loading={false}
type={referenceType}
headerProps={null}

View File

@@ -18,8 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import dayjs from "dayjs";
import React, { RefObject } from "react";
import { View } from "react-native";
import { ActionSheetRef, ScrollView } from "react-native-actions-sheet";
import { ScrollView, View } from "react-native";
import ActionSheet from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../../common/database";
import {
@@ -36,7 +36,7 @@ import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
type ReminderSheetProps = {
actionSheetRef: RefObject<ActionSheetRef>;
actionSheetRef: RefObject<ActionSheet>;
close?: () => void;
update?: (options: PresentSheetOptions) => void;
reminder?: Reminder;

View File

@@ -17,13 +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 React, { RefObject, useRef, useState } from "react";
import { Platform, TextInput, View } from "react-native";
import { ActionSheetRef, ScrollView } from "react-native-actions-sheet";
import {
Platform,
ScrollView,
TextInput,
useWindowDimensions,
View
} from "react-native";
import ActionSheet from "react-native-actions-sheet";
import DateTimePickerModal from "react-native-modal-datetime-picker";
import {
presentSheet,
PresentSheetOptions,
ToastEvent,
presentSheet
ToastEvent
} from "../../../services/event-manager";
import { useThemeStore } from "../../../stores/use-theme-store";
import { SIZE } from "../../../utils/size";
@@ -33,20 +39,16 @@ import Input from "../../ui/input";
import dayjs from "dayjs";
import DatePicker from "react-native-date-picker";
import { db } from "../../../common/database";
import { DDS } from "../../../services/device-detection";
import Navigation from "../../../services/navigation";
import Notifications, { Reminder } from "../../../services/notifications";
import PremiumService from "../../../services/premium";
import SettingsService from "../../../services/settings";
import { useRelationStore } from "../../../stores/use-relation-store";
import { NoteType } from "../../../utils/types";
import { Dialog } from "../../dialog";
import { ReminderTime } from "../../ui/reminder-time";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
type ReminderSheetProps = {
actionSheetRef: RefObject<ActionSheetRef>;
actionSheetRef: RefObject<ActionSheet>;
close?: (ctx?: string) => void;
update?: (options: PresentSheetOptions) => void;
reminder?: Reminder;
@@ -113,16 +115,10 @@ export default function ReminderSheet({
>(reminder?.priority || SettingsService.get().reminderNotificationMode);
const [isDatePickerVisible, setDatePickerVisibility] = useState(false);
const [repeatFrequency, setRepeatFrequency] = useState(1);
const referencedItem = reference
? (db.notes?.note(reference.id)?.data as NoteType)
: null;
const title = useRef<string | undefined>(
reminder?.title || referencedItem?.title
);
const title = useRef<string | undefined>(reminder?.title);
const details = useRef<string | undefined>(reminder?.description);
const titleRef = useRef<TextInput>(null);
const timer = useRef<NodeJS.Timeout>();
const { height } = useWindowDimensions();
const showDatePicker = () => {
setDatePickerVisibility(true);
};
@@ -132,10 +128,9 @@ export default function ReminderSheet({
};
const handleConfirm = (date: Date) => {
timer.current = setTimeout(() => {
hideDatePicker();
setDate(date);
}, 50);
hideDatePicker();
setDate(date);
console.log(date);
};
function nth(n: number) {
return (
@@ -161,7 +156,7 @@ export default function ReminderSheet({
async function saveReminder() {
try {
if (!(await Notifications.checkAndRequestPermissions(true)))
if (!(await Notifications.checkAndRequestPermissions()))
throw new Error(
"App does not have permission to schedule notifications"
);
@@ -200,8 +195,7 @@ export default function ReminderSheet({
if (!_reminder) {
ToastEvent.show({
heading: "Failed to add a new reminder",
context: "local"
heading: "Failed to add a new reminder"
});
}
if (reference) {
@@ -211,73 +205,47 @@ export default function ReminderSheet({
});
}
Notifications.scheduleNotification(_reminder as Reminder);
Navigation.queueRoutesForUpdate();
Navigation.queueRoutesForUpdate(
"TaggedNotes",
"ColoredNotes",
"Notes",
"NotesPage",
"Reminders",
"Favorites",
"TopicNotes"
);
useRelationStore.getState().update();
close?.();
} catch (e) {
ToastEvent.error(e as Error, undefined, "local");
ToastEvent.error(e as Error);
}
}
return (
<View
style={{
paddingHorizontal: 12,
maxHeight: DDS.isTab ? "90%" : "99.99%"
paddingHorizontal: 12
}}
>
<View
<ScrollView
onScrollEndDrag={() => actionSheetRef.current?.handleChildScrollEnd()}
style={{
flexDirection: "row",
width: "100%",
justifyContent: "space-between",
alignItems: "center"
maxHeight: height * 0.85
}}
>
<Heading size={SIZE.lg}>Set reminder</Heading>
<Button
title="Save"
type="accent"
height={40}
style={{
borderRadius: 100,
paddingHorizontal: 24
}}
fontSize={SIZE.md}
onPress={saveReminder}
/>
</View>
<Dialog context="local" />
<ScrollView>
<Input
fwdRef={titleRef}
defaultValue={reminder?.title || referencedItem?.title}
defaultValue={reminder?.title}
placeholder="Remind me of..."
onChangeText={(text) => (title.current = text)}
wrapperStyle={{
marginTop: 10
}}
containerStyle={{ borderWidth: 0, borderBottomWidth: 1 }}
/>
<Input
defaultValue={
reminder ? reminder?.description : referencedItem?.headline
}
placeholder="Add a short note"
defaultValue={reminder?.description}
placeholder="Add a quick note"
onChangeText={(text) => (details.current = text)}
containerStyle={{
maxHeight: 80
}}
multiline
textAlignVertical="top"
inputStyle={{
minHeight: 80,
paddingVertical: 12
}}
height={80}
wrapperStyle={{
marginBottom: 20
}}
containerStyle={{ borderWidth: 0, borderBottomWidth: 1 }}
/>
<View
@@ -310,16 +278,6 @@ export default function ReminderSheet({
mode as keyof typeof ReminderModes
] as Reminder["mode"]
);
if (mode === "Repeat") {
setSelectedDays((days) => {
if (days.length > 0) return days;
if (days.indexOf(date.getDay()) > -1) {
return days;
}
days.push(date.getDay());
return [...days];
});
}
}}
/>
))}
@@ -461,13 +419,11 @@ export default function ReminderSheet({
<DatePicker
date={date}
maximumDate={dayjs(date).add(3, "months").toDate()}
onDateChange={handleConfirm}
textColor={colors.night ? "#ffffff" : "#000000"}
fadeToColor={colors.bg}
theme={colors.night ? "dark" : "light"}
is24hourSource="locale"
androidVariant="nativeAndroid"
mode={reminderMode === ReminderModes.Repeat ? "time" : "datetime"}
/>
@@ -585,6 +541,15 @@ export default function ReminderSheet({
}}
/>
</ScrollView>
<Button
style={{
width: "100%"
}}
title="Save"
type="accent"
fontSize={SIZE.md}
onPress={saveReminder}
/>
</View>
);
}
@@ -596,8 +561,6 @@ ReminderSheet.present = (
) => {
presentSheet({
context: isSheet ? "local" : undefined,
enableGesturesInScrollView: true,
noBottomPadding: true,
component: (ref, close, update) => (
<ReminderSheet
actionSheetRef={ref}

View File

@@ -18,10 +18,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { EVENTS } from "@notesnook/core/common";
import React, { useCallback, useEffect, useRef, useState } from "react";
import React, { createRef, useCallback, useEffect, useState } from "react";
import { ActivityIndicator, Platform, View } from "react-native";
import { FlatList } from "react-native-actions-sheet";
import DocumentPicker from "react-native-document-picker";
import { FlatList } from "react-native-gesture-handler";
import * as ScopedStorage from "react-native-scoped-storage";
import { db } from "../../../common/database";
import storage from "../../../common/database/storage";
@@ -35,7 +35,7 @@ import { initialize } from "../../../stores";
import { useThemeStore } from "../../../stores/use-theme-store";
import { eCloseRestoreDialog, eOpenRestoreDialog } from "../../../utils/events";
import { SIZE } from "../../../utils/size";
import { timeConverter } from "../../../utils/time";
import { sleep, timeConverter } from "../../../utils/time";
import { Dialog } from "../../dialog";
import DialogHeader from "../../dialog/dialog-header";
import { presentDialog } from "../../dialog/functions";
@@ -44,18 +44,12 @@ import { Button } from "../../ui/button";
import Seperator from "../../ui/seperator";
import SheetWrapper from "../../ui/sheet";
import Paragraph from "../../ui/typography/paragraph";
const actionSheetRef = createRef();
let RNFetchBlob;
const RestoreDataSheet = () => {
const [visible, setVisible] = useState(false);
const [restoring, setRestoring] = useState(false);
const sheet = useRef();
useEffect(() => {
const open = async () => {
setVisible(true);
setTimeout(() => {
sheet.current?.show();
}, 1);
};
eSubscribeEvent(eOpenRestoreDialog, open);
eSubscribeEvent(eCloseRestoreDialog, close);
return () => {
@@ -64,15 +58,21 @@ const RestoreDataSheet = () => {
};
}, [close]);
const open = async () => {
setVisible(true);
await sleep(30);
actionSheetRef.current?.setModalVisible(true);
};
const close = useCallback(() => {
if (restoring) {
showIsWorking();
return;
}
sheet.current?.hide();
actionSheetRef.current?.setModalVisible(false);
setTimeout(() => {
setVisible(false);
}, 150);
}, 300);
}, [restoring]);
const showIsWorking = () => {
@@ -86,19 +86,15 @@ const RestoreDataSheet = () => {
return !visible ? null : (
<SheetWrapper
fwdRef={sheet}
fwdRef={actionSheetRef}
gestureEnabled={!restoring}
closeOnTouchBackdrop={!restoring}
onClose={() => {
setVisible(false);
close();
}}
onClose={close}
>
<RestoreDataComponent
close={close}
restoring={restoring}
setRestoring={setRestoring}
actionSheetRef={sheet}
/>
<Toast context="local" />
</SheetWrapper>
@@ -113,6 +109,7 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
const [loading, setLoading] = useState(true);
const [backupDirectoryAndroid, setBackupDirectoryAndroid] = useState(false);
const [progress, setProgress] = useState();
useEffect(() => {
const subscription = db.eventManager.subscribe(
EVENTS.migrationProgress,
@@ -126,9 +123,7 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
}, []);
useEffect(() => {
setTimeout(() => {
checkBackups();
}, 300);
checkBackups();
}, []);
const restore = async (item) => {
@@ -367,6 +362,10 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
</View>
<Seperator half />
<FlatList
nestedScrollEnabled
onMomentumScrollEnd={() => {
actionSheetRef.current?.handleChildScrollEnd();
}}
ListEmptyComponent={
!restoring ? (
loading ? (

View File

@@ -25,22 +25,23 @@ import Navigation from "../../../services/navigation";
import { useThemeStore } from "../../../stores/use-theme-store";
import { GROUP, SORT } from "../../../utils/constants";
import { refreshNotesPage } from "../../../utils/events";
import layoutmanager from "../../../utils/layout-manager";
import { SIZE } from "../../../utils/size";
import { Button } from "../../ui/button";
import Seperator from "../../ui/seperator";
import Heading from "../../ui/typography/heading";
const Sort = ({ type, screen }) => {
const colors = useThemeStore((state) => state.colors);
const isTopicSheet = screen === "TopicSheet";
const [groupOptions, setGroupOptions] = useState(
db.settings.getGroupOptions(type)
);
const updateGroupOptions = async (_groupOptions) => {
await db.settings.setGroupOptions(type, _groupOptions);
layoutmanager.withSpringAnimation(600);
setGroupOptions(_groupOptions);
setTimeout(() => {
if (screen !== "TopicSheet") Navigation.queueRoutesForUpdate(screen);
Navigation.queueRoutesForUpdate(screen);
eSendEvent("groupOptionsUpdate");
eSendEvent(refreshNotesPage);
}, 1);
@@ -51,9 +52,6 @@ const Sort = ({ type, screen }) => {
...groupOptions,
sortDirection: groupOptions.sortDirection === "asc" ? "desc" : "asc"
};
if (type === "topics") {
_groupOptions.groupBy = "none";
}
await updateGroupOptions(_groupOptions);
};
@@ -102,7 +100,7 @@ const Sort = ({ type, screen }) => {
height={25}
iconPosition="right"
fontSize={SIZE.sm - 1}
type="transparent"
type="grayBg"
buttonType={{
text: colors.accent
}}
@@ -121,7 +119,7 @@ const Sort = ({ type, screen }) => {
flexDirection: "row",
justifyContent: "flex-start",
flexWrap: "wrap",
borderBottomWidth: isTopicSheet ? 0 : 1,
borderBottomWidth: 1,
borderBottomColor: colors.nav,
marginBottom: 12,
paddingHorizontal: 12,
@@ -168,9 +166,6 @@ const Sort = ({ type, screen }) => {
...groupOptions,
sortBy: type === "trash" ? "dateDeleted" : item
};
if (type === "topics") {
_groupOptions.groupBy = "none";
}
await updateGroupOptions(_groupOptions);
}}
iconSize={SIZE.md}
@@ -180,70 +175,64 @@ const Sort = ({ type, screen }) => {
)}
</View>
{isTopicSheet ? null : (
<>
<Heading
style={{
marginLeft: 12
<Heading
style={{
marginLeft: 12
}}
size={SIZE.lg}
>
Group by
</Heading>
<Seperator />
<View
style={{
borderRadius: 0,
flexDirection: "row",
flexWrap: "wrap",
paddingHorizontal: 12
}}
>
{Object.keys(GROUP).map((item) => (
<Button
key={item}
testID={"btn-" + item}
type={groupOptions.groupBy === GROUP[item] ? "grayBg" : "gray"}
buttonType={{
text:
groupOptions.groupBy === GROUP[item]
? colors.accent
: colors.icon
}}
size={SIZE.lg}
>
Group by
</Heading>
onPress={async () => {
let _groupOptions = {
...groupOptions,
groupBy: GROUP[item]
};
<Seperator />
<View
style={{
borderRadius: 0,
flexDirection: "row",
flexWrap: "wrap",
paddingHorizontal: 12
}}
>
{Object.keys(GROUP).map((item) => (
<Button
key={item}
testID={"btn-" + item}
type={groupOptions.groupBy === GROUP[item] ? "grayBg" : "gray"}
buttonType={{
text:
groupOptions.groupBy === GROUP[item]
? colors.accent
: colors.icon
}}
onPress={async () => {
let _groupOptions = {
...groupOptions,
groupBy: GROUP[item]
};
if (item === "abc") {
_groupOptions.sortBy = "title";
_groupOptions.sortDirection = "asc";
} else {
if (groupOptions.sortBy === "title") {
_groupOptions.sortBy = "dateEdited";
_groupOptions.sortDirection = "desc";
}
}
updateGroupOptions(_groupOptions);
}}
height={40}
icon={groupOptions.groupBy === GROUP[item] ? "check" : null}
title={
item.slice(0, 1).toUpperCase() + item.slice(1, item.length)
if (item === "abc") {
_groupOptions.sortBy = "title";
_groupOptions.sortDirection = "asc";
} else {
if (groupOptions.sortBy === "title") {
_groupOptions.sortBy = "dateEdited";
_groupOptions.sortDirection = "desc";
}
style={{
paddingHorizontal: 8,
marginBottom: 10,
marginRight: 10
}}
/>
))}
</View>
</>
)}
}
updateGroupOptions(_groupOptions);
}}
height={40}
icon={groupOptions.groupBy === GROUP[item] ? "check" : null}
title={item.slice(0, 1).toUpperCase() + item.slice(1, item.length)}
style={{
paddingHorizontal: 8,
marginBottom: 10,
marginRight: 10
}}
/>
))}
</View>
</View>
);
};

View File

@@ -1,556 +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 qclone from "qclone";
import React, {
createContext,
RefObject,
useCallback,
useContext,
useEffect,
useRef,
useState
} from "react";
import { RefreshControl, View } from "react-native";
import ActionSheet, {
ActionSheetRef,
FlatList
} from "react-native-actions-sheet";
import { db } from "../../../common/database";
import { IconButton } from "../../../components/ui/icon-button";
import { PressableButton } from "../../../components/ui/pressable";
import Paragraph from "../../../components/ui/typography/paragraph";
import { TopicNotes } from "../../../screens/notes/topic-notes";
import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent,
presentSheet
} from "../../../services/event-manager";
import useNavigationStore, {
NotebookScreenParams
} from "../../../stores/use-navigation-store";
import { useThemeStore } from "../../../stores/use-theme-store";
import {
eOnNewTopicAdded,
eOnTopicSheetUpdate,
eOpenAddTopicDialog
} from "../../../utils/events";
import { normalize, SIZE } from "../../../utils/size";
import { GroupHeader, NotebookType, TopicType } from "../../../utils/types";
import { groupArray } from "@notesnook/core/utils/grouping";
import Config from "react-native-config";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../../e2e/test.ids";
import { MMKV } from "../../../common/database/mmkv";
import { openEditor } from "../../../screens/notes/common";
import { getTotalNotes, history } from "../../../utils";
import { deleteItems } from "../../../utils/functions";
import { presentDialog } from "../../dialog/functions";
import { Properties } from "../../properties";
import Sort from "../sort";
import Heading from "../../ui/typography/heading";
type ConfigItem = { id: string; type: string };
class TopicSheetConfig {
static storageKey: "$$sp";
static makeId(item: ConfigItem) {
return `${TopicSheetConfig.storageKey}:${item.type}:${item.id}`;
}
static get(item: ConfigItem) {
return MMKV.getInt(TopicSheetConfig.makeId(item)) || 0;
}
static set(item: ConfigItem, index = 0) {
MMKV.setInt(TopicSheetConfig.makeId(item), index);
}
}
export const TopicsSheet = () => {
const [collapsed, setCollapsed] = useState(false);
const currentScreen = useNavigationStore((state) => state.currentScreen);
const canShow =
currentScreen.name === "Notebook" || currentScreen.name === "TopicNotes";
const [notebook, setNotebook] = useState(
canShow
? db.notebooks?.notebook(
currentScreen?.notebookId || currentScreen?.id || ""
)?.data
: null
);
const [selection, setSelection] = useState<TopicType[]>([]);
const [enabled, setEnabled] = useState(false);
const colors = useThemeStore((state) => state.colors);
const ref = useRef<ActionSheetRef>(null);
const isTopic = currentScreen.name === "TopicNotes";
const [topics, setTopics] = useState(
notebook
? qclone(
groupArray(notebook.topics, db.settings?.getGroupOptions("topics"))
)
: []
);
const [groupOptions, setGroupOptions] = useState(
db.settings?.getGroupOptions("topics")
);
const onRequestUpdate = React.useCallback(
(data?: NotebookScreenParams) => {
if (!canShow) return;
if (!data) data = { item: notebook } as NotebookScreenParams;
const _notebook = db.notebooks?.notebook(data.item?.id)
?.data as NotebookType;
if (_notebook) {
setNotebook(_notebook);
setTopics(
qclone(
groupArray(_notebook.topics, db.settings?.getGroupOptions("topics"))
)
);
}
},
[canShow, notebook]
);
const onUpdate = useCallback(() => {
setGroupOptions({ ...(db.settings?.getGroupOptions("topics") as any) });
onRequestUpdate();
}, [onRequestUpdate]);
useEffect(() => {
eSubscribeEvent("groupOptionsUpdate", onUpdate);
return () => {
eUnSubscribeEvent("groupOptionsUpdate", onUpdate);
};
}, [onUpdate]);
useEffect(() => {
const onTopicUpdate = () => {
setTimeout(() => {
onRequestUpdate();
}, 1);
};
eSubscribeEvent(eOnTopicSheetUpdate, onTopicUpdate);
eSubscribeEvent(eOnNewTopicAdded, onRequestUpdate);
return () => {
eUnSubscribeEvent(eOnTopicSheetUpdate, onRequestUpdate);
eUnSubscribeEvent(eOnNewTopicAdded, onTopicUpdate);
};
}, [onRequestUpdate]);
const PLACEHOLDER_DATA = {
heading: "Topics",
paragraph: "You have not added any topics yet.",
button: "Add first topic",
action: () => {
eSendEvent(eOpenAddTopicDialog, { notebookId: notebook.id });
},
loading: "Loading notebook topics"
};
const renderTopic = ({
item,
index
}: {
item: TopicType | GroupHeader;
index: number;
}) =>
(item as GroupHeader).type === "header" ? null : (
<TopicItem sheetRef={ref} item={item as TopicType} index={index} />
);
const selectionContext = {
selection: selection,
enabled,
setEnabled,
toggleSelection: (item: TopicType) => {
setSelection((state) => {
const selection = [...state];
const index = selection.findIndex(
(selected) => selected.id === item.id
);
if (index > -1) {
selection.splice(index, 1);
if (selection.length === 0) {
setEnabled(false);
}
return selection;
}
selection.push(item);
return selection;
});
}
};
useEffect(() => {
if (canShow) {
setTimeout(() => {
const id = isTopic ? currentScreen?.notebookId : currentScreen?.id;
const notebook = db.notebooks?.notebook(id as string)?.data;
const snapPoint = isTopic
? 0
: TopicSheetConfig.get({
type: isTopic ? "topic" : "notebook",
id: currentScreen.id as string
});
if (ref.current?.isOpen()) {
ref.current?.snapToIndex(snapPoint);
} else {
ref.current?.show(snapPoint);
}
if (notebook) {
onRequestUpdate({
item: notebook
} as any);
}
}, 300);
} else {
ref.current?.hide();
}
}, [
canShow,
currentScreen?.id,
currentScreen.name,
currentScreen?.notebookId,
onRequestUpdate,
isTopic
]);
return (
<ActionSheet
ref={ref}
isModal={false}
containerStyle={{
maxHeight: 300,
borderTopRightRadius: 15,
borderTopLeftRadius: 15,
backgroundColor: colors.bg,
borderWidth: 1,
borderColor: colors.nav,
borderBottomWidth: 0
}}
openAnimationConfig={{
friction: 10
}}
onSnapIndexChange={(index) => {
setCollapsed(index === 0);
TopicSheetConfig.set(
{
type: isTopic ? "topic" : "notebook",
id: currentScreen.id as string
},
index
);
}}
closable={!canShow}
elevation={10}
indicatorStyle={{
width: 100,
backgroundColor: colors.nav
}}
keyboardHandlerEnabled={false}
snapPoints={Config.isTesting === "true" ? [100] : [25, 100]}
initialSnapIndex={1}
backgroundInteractionEnabled
gestureEnabled
>
<View
style={{
position: "absolute",
right: 12,
marginTop: -80
}}
>
<PressableButton
testID={notesnook.buttons.add}
type="accent"
accentColor={"accent"}
accentText="light"
onPress={openEditor}
customStyle={{
borderRadius: 100
}}
>
<View
style={{
alignItems: "center",
justifyContent: "center",
height: normalize(60),
width: normalize(60)
}}
>
<Icon name="plus" color="white" size={SIZE.xxl} />
</View>
</PressableButton>
</View>
<View
style={{
maxHeight: 300,
height: 300,
width: "100%"
}}
>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
paddingHorizontal: 12,
alignItems: "center"
}}
>
<Paragraph size={SIZE.xs} color={colors.icon}>
TOPICS
</Paragraph>
<View
style={{
flexDirection: "row"
}}
>
{enabled ? (
<IconButton
customStyle={{
marginLeft: 10
}}
onPress={async () => {
//@ts-ignore
history.selectedItemsList = selection;
presentDialog({
title: `Delete ${
selection.length > 1 ? "topics" : "topics"
}`,
paragraph: `Are you sure you want to delete ${
selection.length > 1 ? "these topics?" : "this topic?"
}`,
positiveText: "Delete",
negativeText: "Cancel",
positivePress: async () => {
await deleteItems();
history.selectedItemsList = [];
setEnabled(false);
setSelection([]);
},
positiveType: "errorShade"
});
return;
}}
color={colors.pri}
tooltipText="Move to trash"
tooltipPosition={1}
name="delete"
size={22}
/>
) : (
<>
<IconButton
name={
groupOptions?.sortDirection === "asc"
? "sort-ascending"
: "sort-descending"
}
onPress={() => {
presentSheet({
component: <Sort screen="TopicSheet" type="topics" />
});
}}
testID="group-topic-button"
color={colors.pri}
size={22}
customStyle={{
width: 40,
height: 40
}}
/>
<IconButton
name="plus"
onPress={PLACEHOLDER_DATA.action}
testID="add-topic-button"
color={colors.pri}
size={22}
customStyle={{
width: 40,
height: 40
}}
/>
<IconButton
name={collapsed ? "chevron-up" : "chevron-down"}
onPress={() => {
if (ref.current?.currentSnapIndex() !== 0) {
setCollapsed(true);
ref.current?.snapToIndex(0);
} else {
setCollapsed(false);
ref.current?.snapToIndex(1);
}
}}
color={colors.pri}
size={22}
customStyle={{
width: 40,
height: 40
}}
/>
</>
)}
</View>
</View>
<SelectionContext.Provider value={selectionContext}>
<FlatList
data={topics}
style={{
width: "100%"
}}
refreshControl={
<RefreshControl
refreshing={false}
onRefresh={() => {
onRequestUpdate();
}}
colors={[colors.accent]}
progressBackgroundColor={colors.bg}
/>
}
keyExtractor={(item) => (item as TopicType).id}
renderItem={renderTopic}
ListEmptyComponent={
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
height: 200
}}
>
<Paragraph color={colors.icon}>No topics</Paragraph>
</View>
}
ListFooterComponent={<View style={{ height: 50 }} />}
/>
</SelectionContext.Provider>
</View>
</ActionSheet>
);
};
const SelectionContext = createContext<{
selection: TopicType[];
enabled: boolean;
setEnabled: (value: boolean) => void;
toggleSelection: (item: TopicType) => void;
}>({
selection: [],
enabled: false,
setEnabled: (_value: boolean) => {},
toggleSelection: (_item: TopicType) => {}
});
const useSelection = () => useContext(SelectionContext);
const TopicItem = ({
item,
index,
sheetRef
}: {
item: TopicType;
index: number;
sheetRef: RefObject<ActionSheetRef>;
}) => {
const screen = useNavigationStore((state) => state.currentScreen);
const colors = useThemeStore((state) => state.colors);
const selection = useSelection();
const isSelected =
selection.selection.findIndex((selected) => selected.id === item.id) > -1;
const isFocused = screen.id === item.id;
const notesCount = getTotalNotes(item);
return (
<PressableButton
type={isSelected || isFocused ? "grayBg" : "transparent"}
onLongPress={() => {
if (selection.enabled) return;
selection.setEnabled(true);
selection.toggleSelection(item);
}}
testID={`topic-sheet-item-${index}`}
onPress={() => {
if (selection.enabled) {
selection.toggleSelection(item);
return;
}
TopicNotes.navigate(item, true);
}}
customStyle={{
justifyContent: "space-between",
width: "100%",
alignItems: "center",
flexDirection: "row",
paddingHorizontal: 12,
borderRadius: 0
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center"
}}
>
{selection.enabled ? (
<IconButton
size={SIZE.lg}
color={isSelected ? colors.accent : colors.icon}
name={
isSelected
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
) : null}
<Paragraph size={SIZE.sm}>
{item.title}{" "}
{notesCount ? (
<Paragraph size={SIZE.xs} color={colors.icon}>
{notesCount}
</Paragraph>
) : null}
</Paragraph>
</View>
<IconButton
name="dots-horizontal"
customStyle={{
width: 40,
height: 40
}}
testID={notesnook.ids.notebook.menu}
onPress={() => {
Properties.present(item);
}}
left={0}
right={0}
bottom={0}
top={0}
color={colors.pri}
size={SIZE.xl}
/>
</PressableButton>
);
};

View File

@@ -19,10 +19,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useEffect, useState } from "react";
import { Linking, View } from "react-native";
import { ScrollView } from "react-native-actions-sheet";
import { checkVersion } from "react-native-check-version";
import Config from "react-native-config";
import deviceInfoModule from "react-native-device-info";
import { ScrollView } from "react-native-gesture-handler";
import { useThemeStore } from "../../../stores/use-theme-store";
import { STORE_LINK } from "../../../utils/constants";
import { SIZE } from "../../../utils/size";
@@ -148,6 +148,9 @@ export const Update = ({ version: appVersion, fwdRef }) => {
<Seperator />
<ScrollView
nestedScrollEnabled={true}
onMomentumScrollEnd={() => {
fwdRef?.current?.handleChildScrollEnd();
}}
style={{
width: "100%"
}}

View File

@@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useCallback } from "react";
import { FlatList, View } from "react-native";
import { notesnook } from "../../../e2e/test.ids";
import umami from "../../common/analytics";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { DDS } from "../../services/device-detection";
import { eSendEvent } from "../../services/event-manager";
@@ -75,6 +76,7 @@ export const SideMenu = React.memo(
name: "Notesnook Pro",
icon: "crown",
func: () => {
umami.pageView("/pro-screen", "/sidemenu");
eSendEvent(eOpenPremiumDialog);
}
};

View File

@@ -164,6 +164,7 @@ export const PinItem = React.memo(
}}
fontSize={SIZE.md}
width="95%"
height={50}
customStyle={{
marginBottom: 30
}}

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 { TouchableOpacity, View } from "react-native";
import { Keyboard, TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../e2e/test.ids";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
@@ -34,25 +34,24 @@ import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
let toastMessages = [];
export const Toast = ({ context = "global" }) => {
const colors = useThemeStore((state) => state.colors);
const [keyboard, setKeyboard] = useState(false);
const [data, setData] = useState({});
const insets = useGlobalSafeAreaInsets();
const hideTimeout = useRef();
const [visible, setVisible] = useState(false);
const toastMessages = useRef([]);
const show = useCallback(
const showToastFunc = useCallback(
async (data) => {
if (!data) return;
if (data.context !== context) return;
if (
toastMessages.current.findIndex((m) => m.message === data.message) >= 0
) {
if (toastMessages.findIndex((m) => m.message === data.message) >= 0) {
return;
}
toastMessages.current.push(data);
if (toastMessages.current?.length > 1) return;
toastMessages.push(data);
if (toastMessages?.length > 1) return;
setData(data);
setVisible(true);
@@ -60,16 +59,16 @@ export const Toast = ({ context = "global" }) => {
clearTimeout(hideTimeout.current);
}
hideTimeout.current = setTimeout(() => {
hide();
hideToastFunc();
}, data.duration);
},
[context, hide]
[context, hideToastFunc]
);
const next = useCallback(
const showNext = useCallback(
(data) => {
if (!data) {
hide();
hideToastFunc();
return;
}
setData(data);
@@ -77,28 +76,27 @@ export const Toast = ({ context = "global" }) => {
clearTimeout(hideTimeout.current);
}
hideTimeout.current = setTimeout(() => {
hide();
hideToastFunc();
}, data?.duration);
},
[hide]
[hideToastFunc]
);
const hide = useCallback(() => {
const hideToastFunc = useCallback(() => {
if (hideTimeout.current) {
clearTimeout(hideTimeout.current);
}
let msg =
toastMessages.current.length > 1 ? toastMessages.current.shift() : null;
let msg = toastMessages.length > 1 ? toastMessages.shift() : null;
if (msg) {
setVisible(false);
next(msg);
showNext(msg);
setTimeout(() => {
setVisible(true);
}, 300);
} else {
setVisible(false);
toastMessages.current.shift();
toastMessages.shift();
setTimeout(() => {
setData({});
if (hideTimeout.current) {
@@ -106,22 +104,44 @@ export const Toast = ({ context = "global" }) => {
}
}, 100);
}
}, [next]);
}, [showNext]);
const _onKeyboardShow = () => {
setKeyboard(true);
};
const _onKeyboardHide = () => {
setKeyboard(false);
};
useEffect(() => {
eSubscribeEvent(eShowToast, show);
eSubscribeEvent(eHideToast, hide);
toastMessages = [];
let sub1 = Keyboard.addListener("keyboardDidShow", _onKeyboardShow);
let sub2 = Keyboard.addListener("keyboardDidHide", _onKeyboardHide);
eSubscribeEvent(eShowToast, showToastFunc);
eSubscribeEvent(eHideToast, hideToastFunc);
return () => {
toastMessages.current = [];
eUnSubscribeEvent(eShowToast, show);
eUnSubscribeEvent(eHideToast, hide);
if (hideTimeout.current) {
clearTimeout(hideTimeout.current);
}
toastMessages = [];
sub1?.remove();
sub2?.remove();
eUnSubscribeEvent(eShowToast, showToastFunc);
eUnSubscribeEvent(eHideToast, hideToastFunc);
};
}, [hide, show]);
}, [hideToastFunc, keyboard, showToastFunc]);
return (
visible && (
<TouchableOpacity
onPress={hide}
onPress={() => {
if (hideTimeout.current) {
clearTimeout(hideTimeout.current);
}
hideToastFunc();
}}
activeOpacity={1}
style={{
width: DDS.isTab ? 400 : "100%",
@@ -189,7 +209,7 @@ export const Toast = ({ context = "global" }) => {
color={colors.pri}
size={SIZE.md}
onPress={() => {
hide();
hideToastFunc();
}}
>
{data.heading}
@@ -204,7 +224,7 @@ export const Toast = ({ context = "global" }) => {
paddingRight: 10
}}
onPress={() => {
hide();
hideToastFunc();
}}
>
{data.message}

View File

@@ -73,7 +73,6 @@ interface InputProps extends TextInputProps {
buttonLeft?: React.ReactNode;
inputStyle?: TextInputProps["style"];
containerStyle?: ViewStyle;
wrapperStyle?: ViewStyle;
flexGrow?: number;
}
@@ -92,7 +91,7 @@ const Input = ({
button,
onBlurInput,
onPress,
height = 45,
height = 50,
fontSize = SIZE.md,
onFocusInput,
buttons,
@@ -101,7 +100,6 @@ const Input = ({
flexGrow = 1,
inputStyle = {},
containerStyle = {},
wrapperStyle = {},
...restProps
}: InputProps) => {
const colors = useThemeStore((state) => state.colors);
@@ -238,8 +236,7 @@ const Input = ({
marginBottom: marginBottom,
flexGrow: flexGrow,
maxHeight: height,
marginRight: marginRight,
...wrapperStyle
marginRight: marginRight
}}
>
<TouchableOpacity

View File

@@ -37,9 +37,7 @@ const SheetWrapper = ({
onHasReachedTop,
keyboardMode,
overlay,
overlayOpacity = 0.3,
enableGesturesInScrollView = false,
bottomPadding = true
overlayOpacity = 0.3
}) => {
const colors = useThemeStore((state) => state.colors);
const deviceMode = useSettingStore((state) => state.deviceMode);
@@ -61,8 +59,8 @@ const SheetWrapper = ({
zIndex: 10,
paddingTop: 5,
paddingBottom: 0,
borderTopRightRadius: 15,
borderTopLeftRadius: 15,
borderTopRightRadius: 10,
borderTopLeftRadius: 10,
alignSelf: "center",
borderBottomRightRadius: 0,
borderBottomLeftRadius: 0
@@ -86,8 +84,7 @@ const SheetWrapper = ({
backdrop: "sheet-backdrop"
}}
indicatorStyle={{
width: 100,
backgroundColor: colors.nav
width: 100
}}
drawUnderStatusBar={false}
containerStyle={style}
@@ -101,35 +98,32 @@ const SheetWrapper = ({
indicatorColor={colors.nav}
onOpen={_onOpen}
keyboardDismissMode="none"
enableGesturesInScrollView={enableGesturesInScrollView}
defaultOverlayOpacity={overlayOpacity}
overlayColor={pitchBlack ? "#585858" : "#2b2b2b"}
overlayColor={pitchBlack ? "#585858" : "#000000"}
keyboardShouldPersistTaps="always"
ExtraOverlayComponent={
<>
{overlay}
<Toast context="local" />
<PremiumToast
context="sheet"
close={() => fwdRef?.current?.hide()}
offset={50}
/>
<Toast context="local" />
</>
}
onClose={_onClose}
>
<BouncingView>
{children}
{bottomPadding ? (
<View
style={{
height:
Platform.OS === "ios" && insets.bottom !== 0
? insets.bottom + 5
: 20
}}
/>
) : null}
<View
style={{
height:
Platform.OS === "ios" && insets.bottom !== 0
? insets.bottom + 5
: 20
}}
/>
</BouncingView>
</ActionSheet>
);

View File

@@ -46,7 +46,6 @@ export default function Tag({
marginLeft: 2,
marginTop: -10,
height: 20,
justifyContent: "center",
...style
}}
>

View File

@@ -21,7 +21,23 @@ import { FeatureType } from "./components/sheets/new-feature";
export const features: FeatureType[] = [
{
title: "Default font size & font family",
body: "Now you can set default font size and font family in editor that will be used across all your new and old notes."
title: "Cross platform Reminders 🔔",
body: "Finally reminders are here. You can set reminders on notes or independently. Go to Side Menu > Reminders to set your first reminder!"
},
{
title: "Multi-factor auth by default",
body: "Multi-factor auth is enabled by default for all users to improve login security."
},
{
title: "Change account email",
body: "Now you can go to Settings > Account Settings to change your email."
},
{
title: "Sync settings",
body: "Added options to disable sync, auto-sync & real-time sync from settings."
},
{
title: "Improved PDF exports styling",
body: "Tables, checklists, codeblocks, quotes are now properly formatted in PDF exports."
}
];

View File

@@ -21,16 +21,12 @@ import Clipboard from "@react-native-clipboard/clipboard";
import React, { useCallback, useEffect, useState } from "react";
import { Platform } from "react-native";
import Share from "react-native-share";
import { notesnook } from "../../e2e/test.ids";
import { db } from "../common/database";
import { AttachmentDialog } from "../components/attachments";
import { presentDialog } from "../components/dialog/functions";
import NoteHistory from "../components/note-history";
import { AddNotebookSheet } from "../components/sheets/add-notebook";
import MoveNoteSheet from "../components/sheets/add-to";
import ExportNotesSheet from "../components/sheets/export-notes";
import { MoveNotes } from "../components/sheets/move-notes/movenote";
import PublishNoteSheet from "../components/sheets/publish-note";
import { RelationsList } from "../components/sheets/relations-list/index";
import ReminderSheet from "../components/sheets/reminder";
import {
eSendEvent,
@@ -45,7 +41,6 @@ import Notifications from "../services/notifications";
import { useEditorStore } from "../stores/use-editor-store";
import { useMenuStore } from "../stores/use-menu-store";
import useNavigationStore from "../stores/use-navigation-store";
import { useRelationStore } from "../stores/use-relation-store";
import { useSelectionStore } from "../stores/use-selection-store";
import { useTagStore } from "../stores/use-tag-store";
import { useThemeStore } from "../stores/use-theme-store";
@@ -53,12 +48,17 @@ import { useUserStore } from "../stores/use-user-store";
import { toTXT } from "../utils";
import { toggleDarkMode } from "../utils/color-scheme/utils";
import {
eOnTopicSheetUpdate,
eOpenAddNotebookDialog,
eOpenAddTopicDialog,
eOpenLoginDialog
eOpenAttachmentsDialog,
eOpenLoginDialog,
eOpenMoveNoteDialog,
eOpenPublishNoteDialog
} from "../utils/events";
import { deleteItems } from "../utils/functions";
import { sleep } from "../utils/time";
import { RelationsList } from "../components/sheets/relations-list/index";
import { useRelationStore } from "../stores/use-relation-store";
export const useActions = ({ close = () => null, item }) => {
const colors = useThemeStore((state) => state.colors);
@@ -106,15 +106,6 @@ export const useActions = ({ close = () => null, item }) => {
);
};
const isNoteInNotebook = () => {
const currentScreen = useNavigationStore.getState().currentScreen;
if (item.type !== "note" || currentScreen.name !== "Notebook") return;
return !!db.relations
.to(item, "notebook")
.find((notebook) => notebook.id === currentScreen.id);
};
const onUpdate = useCallback(
async (type) => {
if (type === "unpin") {
@@ -139,16 +130,25 @@ export const useActions = ({ close = () => null, item }) => {
}
function addTo() {
close();
clearSelection(true);
setSelectedItem(item);
MoveNoteSheet.present(item);
setTimeout(() => {
eSendEvent(eOpenMoveNoteDialog, item);
}, 300);
}
async function addToFavorites() {
if (!item.id) return;
close();
await db.notes.note(item.id).favorite();
Navigation.queueRoutesForUpdate();
Navigation.queueRoutesForUpdate(
"TaggedNotes",
"ColoredNotes",
"TopicNotes",
"Favorites",
"Notes"
);
}
async function pinItem() {
@@ -156,7 +156,14 @@ export const useActions = ({ close = () => null, item }) => {
close();
let type = item.type;
await db[`${type}s`][type](item.id).pin();
Navigation.queueRoutesForUpdate();
Navigation.queueRoutesForUpdate(
"TaggedNotes",
"ColoredNotes",
"TopicNotes",
"Favorites",
"Notes",
"Notebooks"
);
}
async function pinToNotifications() {
@@ -170,17 +177,9 @@ export const useActions = ({ close = () => null, item }) => {
checkNotifPinned();
return;
}
if (item.locked) {
ToastEvent.show({
heading: "Note is locked",
type: "error",
message: "Locked notes cannot be pinned to notifications",
context: "local"
});
return;
}
let text = await toTXT(item, false);
let html = text.replace(/\n/g, "<br />");
if (item.locked) return;
let html = await db.notes.note(item.id).content();
let text = await toTXT(item);
Notifications.displayNotification({
title: item.title,
message: item.headline || text,
@@ -199,7 +198,15 @@ export const useActions = ({ close = () => null, item }) => {
if (!checkNoteSynced()) return;
close();
await db.trash.restore(item.id);
Navigation.queueRoutesForUpdate();
Navigation.queueRoutesForUpdate(
"TaggedNotes",
"ColoredNotes",
"TopicNotes",
"Favorites",
"Notes",
"Notebooks",
"Trash"
);
let type = item.type === "trash" ? item.itemType : item.type;
ToastEvent.show({
heading:
@@ -264,7 +271,9 @@ export const useActions = ({ close = () => null, item }) => {
});
return;
}
PublishNoteSheet.present(item);
close();
await sleep(300);
eSendEvent(eOpenPublishNoteDialog, item);
}
const checkNoteSynced = () => {
@@ -315,7 +324,13 @@ export const useActions = ({ close = () => null, item }) => {
let note = db.notes.note(item.id).data;
if (note.locked) {
close();
Navigation.queueRoutesForUpdate();
Navigation.queueRoutesForUpdate(
"TaggedNotes",
"ColoredNotes",
"TopicNotes",
"Favorites",
"Notes"
);
}
} catch (e) {
close();
@@ -381,11 +396,16 @@ export const useActions = ({ close = () => null, item }) => {
positivePress: async (value) => {
if (!value || value === "" || value.trimStart().length == 0) return;
await db.tags.rename(item.id, db.tags.sanitize(value));
setImmediate(() => {
useTagStore.getState().setTags();
useMenuStore.getState().setMenuPins();
Navigation.queueRoutesForUpdate();
});
useTagStore.getState().setTags();
useMenuStore.getState().setMenuPins();
Navigation.queueRoutesForUpdate(
"TaggedNotes",
"ColoredNotes",
"TopicNotes",
"Favorites",
"Notes",
"Tags"
);
},
input: true,
defaultValue: alias,
@@ -428,16 +448,24 @@ export const useActions = ({ close = () => null, item }) => {
? "This reminder will be removed"
: "This tag will be removed from all notes.",
positivePress: async () => {
const routes = [];
routes.push(
"TaggedNotes",
"ColoredNotes",
"Notes",
"NotesPage",
"Reminders",
"Favorites"
);
if (item.type === "reminder") {
await db.reminders.remove(item.id);
} else {
await db.tags.remove(item.id);
}
setImmediate(() => {
useTagStore.getState().setTags();
Navigation.queueRoutesForUpdate();
useRelationStore.getState().update();
});
routes.push("Tags");
}
Navigation.queueRoutesForUpdate(...routes);
useRelationStore.getState().update();
},
positiveText: "Delete",
positiveType: "errorShade"
@@ -475,16 +503,15 @@ export const useActions = ({ close = () => null, item }) => {
},
item.id
);
Navigation.queueRoutesForUpdate();
eSendEvent(eOnTopicSheetUpdate);
close();
}
async function removeNoteFromNotebook() {
const currentScreen = useNavigationStore.getState().currentScreen;
if (currentScreen.name !== "Notebook") return;
await db.relations.unlink({ type: "notebook", id: currentScreen.id }, item);
Navigation.queueRoutesForUpdate();
Navigation.queueRoutesForUpdate(
"TaggedNotes",
"ColoredNotes",
"TopicNotes",
"Favorites",
"Notes",
"Notebook",
"Notebooks"
);
close();
}
@@ -499,14 +526,12 @@ export const useActions = ({ close = () => null, item }) => {
negativeText: "Cancel",
positivePress: async () => {
await db.trash.delete(item.id);
setImmediate(() => {
Navigation.queueRoutesForUpdate();
useSelectionStore.getState().setSelectionMode(false);
ToastEvent.show({
heading: "Permanantly deleted items",
type: "success",
context: "local"
});
Navigation.queueRoutesForUpdate("Trash");
useSelectionStore.getState().setSelectionMode(false);
ToastEvent.show({
heading: "Permanantly deleted items",
type: "success",
context: "local"
});
},
positiveType: "errorShade"
@@ -514,32 +539,35 @@ export const useActions = ({ close = () => null, item }) => {
}
async function openHistory() {
close();
await sleep(300);
presentSheet({
component: (ref) => <NoteHistory fwdRef={ref} note={item} />
});
}
async function showAttachments() {
AttachmentDialog.present();
close();
await sleep(300);
eSendEvent(eOpenAttachmentsDialog, item);
}
async function exportNote() {
if (item.locked) {
ToastEvent.show({
heading: "Note is locked",
type: "error",
message: "Locked notes cannot be exported",
context: "local"
});
return;
}
close();
await sleep(300);
ExportNotesSheet.present([item]);
}
async function toggleLocalOnly() {
if (!checkNoteSynced() || !user) return;
db.notes.note(item.id).localOnly();
Navigation.queueRoutesForUpdate();
Navigation.queueRoutesForUpdate(
"TaggedNotes",
"ColoredNotes",
"TopicNotes",
"Favorites",
"Notes"
);
close();
}
@@ -550,109 +578,101 @@ export const useActions = ({ close = () => null, item }) => {
useEditorStore.getState().setReadonly(current);
// tiny.call(EditorWebView, tiny.toogleReadMode(current ? 'readonly' : 'design'));
}
Navigation.queueRoutesForUpdate();
Navigation.queueRoutesForUpdate(
"TaggedNotes",
"ColoredNotes",
"TopicNotes",
"Favorites",
"Notes"
);
close();
};
const duplicateNote = async () => {
if (!checkNoteSynced()) return;
await db.notes.note(item.id).duplicate();
Navigation.queueRoutesForUpdate();
Navigation.queueRoutesForUpdate(
"TaggedNotes",
"ColoredNotes",
"TopicNotes",
"Favorites",
"Notes"
);
close();
};
const actions = [
{
id: "notebooks",
title: "Link Notebooks",
name: "Add to notebook",
title: "Add to notebook",
icon: "book-outline",
func: addTo
},
{
id: "add-tag",
title: "Add tags",
icon: "pound",
func: addTo
},
{
id: "add-reminder",
title: "Remind me",
icon: "clock-plus-outline",
name: "Add-Reminder",
title: "Add reminder",
icon: "bell-plus",
func: () => {
ReminderSheet.present(null, { id: item.id, type: "note" });
},
close: true
},
{
id: "lock-unlock",
title: item.locked ? "Unlock" : "Lock",
icon: item.locked ? "lock-open-outline" : "key-outline",
func: addToVault,
on: item.locked
},
{
id: "publish",
title: isPublished ? "Published" : "Publish",
icon: "cloud-upload-outline",
on: isPublished,
func: publishNote
},
{
id: "export",
title: "Export",
icon: "export",
func: exportNote
},
{
id: "move-notes",
name: "Move notes",
title: "Add notes",
icon: "plus",
func: async () => {
close();
await sleep(500);
MoveNotes.present(db.notebooks.notebook(item.notebookId).data, item);
}
},
{
id: "pin",
title: "Pin",
name: "Pin",
title: item.pinned ? "Unpin" : "Pin to top",
icon: item.pinned ? "pin-off-outline" : "pin-outline",
func: pinItem,
close: false,
check: true,
on: item.pinned,
pro: true
nopremium: true,
id: notesnook.ids.dialogs.actionsheet.pin
},
{
id: "favorite",
title: "Favorite",
name: "Favorite",
title: !item.favorite ? "Favorite" : "Unfavorite",
icon: item.favorite ? "star-off" : "star-outline",
func: addToFavorites,
close: false,
check: true,
on: item.favorite,
pro: true,
nopremium: true,
id: notesnook.ids.dialogs.actionsheet.favorite,
color: "orange"
},
{
id: "pin-to-notifications",
name: "PinToNotif",
title:
notifPinned !== null
? "Unpin from notifications"
: "Pin to notifications",
icon: "message-badge-outline",
? "Unpin from Notifications"
: "Pin to Notifications",
icon: "bell",
on: notifPinned !== null,
func: pinToNotifications
},
{
id: "edit-notebook",
name: "Edit Notebook",
title: "Edit notebook",
icon: "square-edit-outline",
func: async () => {
AddNotebookSheet.present(item);
close();
await sleep(300);
eSendEvent(eOpenAddNotebookDialog, item);
}
},
{
id: "edit-topic",
name: "Edit Topic",
title: "Edit topic",
icon: "square-edit-outline",
func: async () => {
@@ -665,85 +685,146 @@ export const useActions = ({ close = () => null, item }) => {
}
},
{
id: "copy",
name: "Copy",
title: "Copy",
icon: "content-copy",
func: copyContent
},
{
id: "restore",
name: "Restore",
title: "Restore " + item.itemType,
icon: "delete-restore",
func: restoreTrashItem
},
{
id: "add-shortcut",
name: "Publish",
title: isPublished ? "Published" : "Publish",
icon: "cloud-upload-outline",
on: isPublished,
func: publishNote
},
{
name: "Vault",
title: item.locked ? "Remove from vault" : "Add to vault",
icon: item.locked ? "shield-off-outline" : "shield-outline",
func: addToVault,
on: item.locked
},
{
name: "Add Shortcut",
title: isPinnedToMenu ? "Remove Shortcut" : "Add Shortcut",
icon: isPinnedToMenu ? "link-variant-remove" : "link-variant",
func: createMenuShortcut,
close: false,
check: true,
on: isPinnedToMenu,
pro: true
nopremium: true,
id: notesnook.ids.dialogs.actionsheet.pinMenu
},
{
id: "rename-tag",
name: "Rename Tag",
title: "Rename tag",
icon: "square-edit-outline",
func: renameTag
},
{
id: "share",
name: "Share",
title: "Share",
icon: "share-variant",
func: shareNote
},
{
id: "read-only",
name: "Attachments",
title: "Attachments",
icon: "attachment",
func: showAttachments
},
{
name: "Export",
title: "Export",
icon: "export",
func: exportNote
},
{
name: "RemoveTopic",
title: "Remove from topic",
hidden: !isNoteInTopic(),
icon: "minus-circle-outline",
func: removeNoteFromTopic
},
{
name: "Delete",
title:
item.type !== "notebook" && item.type !== "note"
? "Delete " + item.type
: "Move to trash",
icon: "delete-outline",
type: "error",
func: deleteItem
},
{
name: "PermDelete",
title: "Delete " + item.itemType,
icon: "delete",
func: deleteTrashItem
},
{
name: "ReadOnly",
title: "Read only",
icon: "pencil-lock",
func: toggleReadyOnlyMode,
on: item.readonly
},
{
id: "local-only",
name: "Local only",
title: "Local only",
icon: "sync-off",
func: toggleLocalOnly,
on: item.localOnly
},
{
id: "duplicate",
name: "History",
title: "History",
icon: "history",
func: openHistory
},
{
name: "Duplicate",
title: "Duplicate",
icon: "content-duplicate",
func: duplicateNote
},
{
id: "dark-mode",
name: "Dark Mode",
title: "Dark mode",
icon: "theme-light-dark",
func: switchTheme,
switch: true,
on: colors.night ? true : false,
close: false,
pro: true
nopremium: true,
id: notesnook.ids.dialogs.actionsheet.night
},
{
id: "edit-reminder",
name: "Edit reminder",
title: "Edit reminder",
icon: "pencil",
func: async () => {
close();
await sleep(300);
ReminderSheet.present(item);
},
close: false
},
{
id: "reminders",
name: "Reminders",
title: "Reminders",
icon: "clock-outline",
icon: "bell",
func: async () => {
close();
RelationsList.present({
reference: item,
referenceType: "reminder",
@@ -761,20 +842,7 @@ export const useActions = ({ close = () => null, item }) => {
close: false
},
{
id: "attachments",
title: "Attachments",
icon: "attachment",
func: showAttachments
},
{
id: "history",
title: "History",
icon: "history",
func: openHistory
},
{
id: "disable-reminder",
name: "ReminderOnOff",
title: !item.disabled ? "Turn off reminder" : "Turn on reminder",
icon: !item.disabled ? "bell-off-outline" : "bell",
func: async () => {
@@ -785,41 +853,19 @@ export const useActions = ({ close = () => null, item }) => {
});
Notifications.scheduleNotification(item);
useRelationStore.getState().update();
Navigation.queueRoutesForUpdate();
Navigation.queueRoutesForUpdate(
"TaggedNotes",
"ColoredNotes",
"TopicNotes",
"Favorites",
"Notes",
"NotesPage",
"Reminders"
);
}
},
{
id: "remove-from-topic",
title: "Remove from topic",
hidden: !isNoteInTopic(),
icon: "minus-circle-outline",
func: removeNoteFromTopic
},
{
id: "remove-from-notebook",
title: "Remove from notebook",
hidden: !isNoteInNotebook(),
icon: "minus-circle-outline",
func: removeNoteFromNotebook
},
{
id: "trash",
title:
item.type !== "notebook" && item.type !== "note"
? "Delete " + item.type
: "Move to trash",
icon: "delete-outline",
type: "error",
func: deleteItem
},
{
id: "delete",
title: "Delete " + item.itemType,
icon: "delete",
func: deleteTrashItem
}
// {
// id: "ReferencedIn",
// name: "ReferencedIn",
// title: "References",
// icon: "link",
// func: async () => {

View File

@@ -26,13 +26,12 @@ import {
Linking,
NativeEventEmitter,
NativeModules,
Platform,
Keyboard
Platform
} from "react-native";
import * as RNIap from "react-native-iap";
import { enabled } from "react-native-privacy-snapshot";
import { Walkthrough } from "../components/walkthroughs";
import { editorController, editorState } from "../screens/editor/tiptap/utils";
import { editorState } from "../screens/editor/tiptap/utils";
import {
clearMessage,
setEmailVerifyMessage,
@@ -70,7 +69,6 @@ import { clearAppState } from "../screens/editor/tiptap/utils";
import { tabBarRef } from "../utils/global-refs";
import BackupService from "../services/backup";
import { sleep } from "../utils/time";
import notifee from "@notifee/react-native";
const SodiumEventEmitter = new NativeEventEmitter(NativeModules.Sodium);
export const useAppEvents = () => {
@@ -318,10 +316,6 @@ export const useAppEvents = () => {
const onLogout = async (reason) => {
console.log("Logged out", reason);
DatabaseLogger.log("User Logged Out" + reason);
SettingsService.set({
introCompleted: true
});
};
const unsubIAP = () => {
@@ -402,9 +396,11 @@ export const useAppEvents = () => {
const checkAutoBackup = useCallback(async () => {
if (verify || syncing) {
console.log("backup is waiting");
refValues.current.backupDidWait = true;
return;
}
console.log("backup running immediate");
const user = await db.user.getUser();
if (PremiumService.get() && user) {
if (SettingsService.get().reminder === "off") {
@@ -425,6 +421,7 @@ export const useAppEvents = () => {
useEffect(() => {
if (!verify && !syncing && refValues.current.backupDidWait) {
console.log("backup run after wait");
refValues.current.backupDidWait = false;
checkAutoBackup();
}
@@ -447,7 +444,6 @@ export const useAppEvents = () => {
const onAppStateChanged = useCallback(
async (state) => {
if (state === "active") {
notifee.setBadgeCount(0);
updateStatusBarColor();
if (
SettingsService.get().appLockMode !== "background" &&
@@ -492,10 +488,6 @@ export const useAppEvents = () => {
!useUserStore.getState().verifyUser
) {
useUserStore.getState().setVerifyUser(true);
if (Platform.OS === "ios") {
editorController.current?.commands.blur();
Keyboard.dismiss();
}
}
if (
SettingsService.get().privacyScreen ||
@@ -558,7 +550,7 @@ export const useAppEvents = () => {
let shareExtensionOpened = MMKV.getString("shareExtensionOpened");
if (notesAddedFromIntent) {
if (Platform.OS === "ios") {
await db.initCollections();
await db.init();
await db.notes.init();
}
useNoteStore.getState().setNotes();
@@ -571,8 +563,8 @@ export const useAppEvents = () => {
if (notesAddedFromIntent || shareExtensionOpened) {
let id = useEditorStore.getState().currentEditingNote;
let note = id && db.notes.note(id).data;
eSendEvent("loadingNote", note);
eSendEvent("webview_reset");
setTimeout(() => eSendEvent("loadingNote", note), 1);
MMKV.removeItem("shareExtensionOpened");
}
} catch (e) {

View File

@@ -17,46 +17,35 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useState } from "react";
import { useEffect, useState } from "react";
import { useAttachmentStore } from "../stores/use-attachment-store";
type AttachmentProgress = {
type: string;
value?: number;
percent?: string;
};
export const useAttachmentProgress = (
attachment: any,
encryption?: boolean
) => {
export const useAttachmentProgress = (attachment, encryption) => {
const progress = useAttachmentStore((state) => state.progress);
const [currentProgress, setCurrentProgress] = useState<
AttachmentProgress | undefined
>(
const [currentProgress, setCurrentProgress] = useState(
encryption
? {
type: "encrypt"
}
: undefined
: null
);
const attachmentProgress = progress?.[attachment.metadata.hash];
if (attachmentProgress) {
const type = attachmentProgress.type;
const loaded =
attachmentProgress.type === "download"
? attachmentProgress.recieved
: attachmentProgress.sent;
const value = loaded / attachmentProgress.total;
setCurrentProgress({
value: value * 100,
percent: (value * 100).toFixed(0) + "%",
type: type
});
} else {
setCurrentProgress(undefined);
}
useEffect(() => {
let prog = progress[attachment.metadata.hash];
if (prog) {
let type = prog.type;
let loaded = prog.type === "download" ? prog.recieved : prog.sent;
prog = loaded / prog.total;
prog = (prog * 100).toFixed(0);
setCurrentProgress({
value: prog,
percent: prog + "%",
type: type
});
} else {
setCurrentProgress(null);
}
}, [attachment.metadata.hash, progress]);
return [currentProgress, setCurrentProgress];
};

View File

@@ -0,0 +1,61 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useCallback, useEffect } from "react";
import BiometicService from "../services/biometrics";
import { eSubscribeEvent, eUnSubscribeEvent } from "../services/event-manager";
import { db } from "../common/database";
const VaultStatusCache = {
exists: false,
biometryEnrolled: false,
isBiometryAvailable: false
};
export const useVaultStatus = () => {
const [vaultStatus, setVaultStatus] = React.useState(VaultStatusCache);
const checkVaultStatus = useCallback(() => {
db.vault.exists().then(async (exists) => {
let available = await BiometicService.isBiometryAvailable();
let fingerprint = await BiometicService.hasInternetCredentials();
if (
VaultStatusCache.exists === exists &&
VaultStatusCache.biometryEnrolled === fingerprint &&
VaultStatusCache.isBiometryAvailable === available
)
return;
setVaultStatus({
exists: exists,
biometryEnrolled: fingerprint,
isBiometryAvailable: available ? true : false
});
});
}, []);
useEffect(() => {
checkVaultStatus();
eSubscribeEvent("vaultUpdated", () => checkVaultStatus());
return () => {
eUnSubscribeEvent("vaultUpdated", () => checkVaultStatus());
};
}, [checkVaultStatus]);
return vaultStatus;
};

View File

@@ -24,7 +24,6 @@ import { SafeAreaView } from "react-native";
import Container from "../components/container";
import DelayLayout from "../components/delay-layout";
import Intro from "../components/intro";
import { TopicsSheet } from "../components/sheets/topic-sheet";
import useGlobalSafeAreaInsets from "../hooks/use-global-safe-area-insets";
import { hideAllTooltips } from "../hooks/use-tooltip";
import Favorites from "../screens/favorites";
@@ -199,7 +198,6 @@ const _NavigationStack = () => {
<NavigationContainer onStateChange={onStateChange} ref={rootNavigatorRef}>
<Tabs />
</NavigationContainer>
<TopicsSheet />
</Container>
);
};

View File

@@ -69,7 +69,6 @@ import {
import { editorRef, tabBarRef } from "../utils/global-refs";
import { sleep } from "../utils/time";
import { NavigationStack } from "./navigation-stack";
import changeNavigationBarColor from "react-native-navigation-bar-color";
const _TabsHolder = () => {
const colors = useThemeStore((state) => state.colors);
@@ -236,17 +235,14 @@ const _TabsHolder = () => {
let needsUpdate = current !== deviceMode;
if (fullscreen && current !== "mobile") {
// Runs after size is set via state.
setTimeout(() => {
editorRef.current?.setNativeProps({
style: {
width: size.width,
zIndex: 999,
paddingHorizontal:
current === "smallTablet" ? size.width * 0 : size.width * 0.15
}
});
}, 1);
editorRef.current?.setNativeProps({
style: {
width: size.width,
zIndex: 999,
paddingHorizontal:
current === "smallTablet" ? size.width * 0 : size.width * 0.15
}
});
} else {
if (fullscreen) eSendEvent(eCloseFullscreenEditor, current);
editorRef.current?.setNativeProps({
@@ -285,8 +281,10 @@ const _TabsHolder = () => {
!editorState().movedAway &&
useEditorStore.getState().currentEditingNote
) {
console.log("editor");
tabBarRef.current?.goToIndex(2, false);
} else {
console.log("home");
tabBarRef.current?.goToIndex(1, false);
}
break;
@@ -387,10 +385,6 @@ const _TabsHolder = () => {
};
}, []);
useEffect(() => {
changeNavigationBarColor(colors.bg, !colors.night, true);
}, [colors.night, colors.bg]);
return (
<View
onLayout={_onLayout}

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