mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-02-24 04:00:59 +01:00
Merge branch 'master' into fix-db-init-background
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
This commit is contained in:
@@ -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. 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.
|
||||
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.
|
||||
|
||||
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/).
|
||||
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/).
|
||||
|
||||
## Developer guide
|
||||
|
||||
|
||||
@@ -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 development environment
|
||||
### Setting up the 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 that has 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 with 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 have integral role in app functionality, for example, 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 are integral to the app's functionality. For example, the 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 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.
|
||||
- `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.
|
||||
|
||||
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 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:
|
||||
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:
|
||||
|
||||
### 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 iOS simulator, you must be on a Mac with XCode installed.
|
||||
To run e2e tests on the 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
|
||||
|
||||
@@ -218,12 +218,14 @@ const List = ({
|
||||
}
|
||||
/>
|
||||
</Animated.View>
|
||||
<JumpToSectionDialog
|
||||
screen={screen}
|
||||
data={listData}
|
||||
type={screen === "Notes" ? "home" : type}
|
||||
scrollRef={scrollRef}
|
||||
/>
|
||||
{listData ? (
|
||||
<JumpToSectionDialog
|
||||
screen={screen}
|
||||
data={listData}
|
||||
type={screen === "Notes" ? "home" : type}
|
||||
scrollRef={scrollRef}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -25,22 +25,22 @@ import {
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from "react-native";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { db } from "../../../common/database";
|
||||
import { DDS } from "../../../services/device-detection";
|
||||
import { presentSheet, ToastEvent } from "../../../services/event-manager";
|
||||
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 { ph, pv, SIZE } from "../../../utils/size";
|
||||
import { SIZE, ph, pv } from "../../../utils/size";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import Input from "../../ui/input";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import { MoveNotes } from "../move-notes/movenote";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
|
||||
let refs = [];
|
||||
export class AddNotebookSheet extends React.Component {
|
||||
@@ -245,13 +245,34 @@ 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;
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
maxHeight: DDS.isTab ? "90%" : "96%",
|
||||
maxHeight: DDS.isTab ? "90%" : "97%",
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
@@ -266,17 +287,34 @@ export class AddNotebookSheet extends React.Component {
|
||||
}}
|
||||
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 />
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.lg}>
|
||||
{notebook && notebook.dateCreated
|
||||
? "Edit Notebook"
|
||||
: "New Notebook"}
|
||||
</Heading>
|
||||
<Button
|
||||
title="Save"
|
||||
type="accent"
|
||||
height={40}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 24
|
||||
}}
|
||||
fontSize={SIZE.md}
|
||||
onPress={this.addNewNotebook}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Seperator />
|
||||
|
||||
<Input
|
||||
fwdRef={(ref) => (this.titleRef = ref)}
|
||||
@@ -344,48 +382,11 @@ export class AddNotebookSheet extends React.Component {
|
||||
keyExtractor={(item, index) => item + index.toString()}
|
||||
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%"
|
||||
fontSize={SIZE.md}
|
||||
title={
|
||||
notebook && notebook.dateCreated
|
||||
? "Save changes"
|
||||
: "Create notebook"
|
||||
ListFooterComponent={
|
||||
topics.length === 0 ? null : <View style={{ height: 50 }} />
|
||||
}
|
||||
type="accent"
|
||||
onPress={this.addNewNotebook}
|
||||
renderItem={this.renderTopicItem}
|
||||
/>
|
||||
{/*
|
||||
{Platform.OS === 'ios' && (
|
||||
<View
|
||||
style={{
|
||||
height: 40
|
||||
}}
|
||||
/>
|
||||
)} */}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,9 +21,9 @@ import { Platform, TextInput, View } from "react-native";
|
||||
import { ActionSheetRef, ScrollView } from "react-native-actions-sheet";
|
||||
import DateTimePickerModal from "react-native-modal-datetime-picker";
|
||||
import {
|
||||
presentSheet,
|
||||
PresentSheetOptions,
|
||||
ToastEvent
|
||||
ToastEvent,
|
||||
presentSheet
|
||||
} from "../../../services/event-manager";
|
||||
import { useThemeStore } from "../../../stores/use-theme-store";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
@@ -33,6 +33,7 @@ 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";
|
||||
@@ -41,6 +42,7 @@ 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 = {
|
||||
@@ -219,35 +221,57 @@ export default function ReminderSheet({
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12
|
||||
paddingHorizontal: 12,
|
||||
maxHeight: DDS.isTab ? "90%" : "99.99%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<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 keyboardShouldPersistTaps="always">
|
||||
<ScrollView>
|
||||
<Input
|
||||
fwdRef={titleRef}
|
||||
defaultValue={reminder?.title || referencedItem?.title}
|
||||
placeholder="Remind me of..."
|
||||
onChangeText={(text) => (title.current = text)}
|
||||
containerStyle={{ borderWidth: 0, borderBottomWidth: 1 }}
|
||||
wrapperStyle={{
|
||||
marginTop: 10
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
defaultValue={
|
||||
reminder ? reminder?.description : referencedItem?.headline
|
||||
}
|
||||
placeholder="Add a quick note"
|
||||
placeholder="Add a short note"
|
||||
onChangeText={(text) => (details.current = text)}
|
||||
containerStyle={{
|
||||
borderWidth: 0,
|
||||
borderBottomWidth: 1,
|
||||
maxHeight: 80,
|
||||
marginTop: 10
|
||||
maxHeight: 80
|
||||
}}
|
||||
multiline
|
||||
textAlignVertical="top"
|
||||
inputStyle={{
|
||||
height: 80
|
||||
minHeight: 80,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
height={80}
|
||||
wrapperStyle={{
|
||||
@@ -432,6 +456,7 @@ export default function ReminderSheet({
|
||||
fadeToColor={colors.bg}
|
||||
theme={colors.night ? "dark" : "light"}
|
||||
is24hourSource="locale"
|
||||
androidVariant="nativeAndroid"
|
||||
mode={reminderMode === ReminderModes.Repeat ? "time" : "datetime"}
|
||||
/>
|
||||
|
||||
@@ -548,15 +573,6 @@ export default function ReminderSheet({
|
||||
alignSelf: "flex-start"
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
title="Save"
|
||||
type="accent"
|
||||
fontSize={SIZE.md}
|
||||
onPress={saveReminder}
|
||||
/>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
@@ -570,6 +586,7 @@ ReminderSheet.present = (
|
||||
presentSheet({
|
||||
context: isSheet ? "local" : undefined,
|
||||
enableGesturesInScrollView: true,
|
||||
noBottomPadding: true,
|
||||
component: (ref, close, update) => (
|
||||
<ReminderSheet
|
||||
actionSheetRef={ref}
|
||||
|
||||
@@ -65,6 +65,7 @@ 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 {
|
||||
|
||||
@@ -80,10 +80,10 @@ export const normalize = (size) => {
|
||||
}
|
||||
};
|
||||
export const SIZE = {
|
||||
xxs: normalize(10.5) * scale.fontScale,
|
||||
xs: normalize(12) * scale.fontScale,
|
||||
sm: normalize(14.5) * scale.fontScale,
|
||||
md: normalize(16) * scale.fontScale,
|
||||
xxs: normalize(11) * scale.fontScale,
|
||||
xs: normalize(12.5) * scale.fontScale,
|
||||
sm: normalize(15) * scale.fontScale,
|
||||
md: normalize(16.5) * scale.fontScale,
|
||||
lg: normalize(22) * scale.fontScale,
|
||||
xl: normalize(24) * scale.fontScale,
|
||||
xxl: normalize(28) * scale.fontScale,
|
||||
@@ -91,10 +91,10 @@ export const SIZE = {
|
||||
};
|
||||
|
||||
export function updateSize() {
|
||||
SIZE.xxs = normalize(10.5) * scale.fontScale;
|
||||
SIZE.xs = normalize(12) * scale.fontScale;
|
||||
SIZE.sm = normalize(14.5) * scale.fontScale;
|
||||
SIZE.md = normalize(16) * scale.fontScale;
|
||||
SIZE.xxs = normalize(11) * scale.fontScale;
|
||||
SIZE.xs = normalize(12.5) * scale.fontScale;
|
||||
SIZE.sm = normalize(15) * scale.fontScale;
|
||||
SIZE.md = normalize(16.5) * scale.fontScale;
|
||||
SIZE.lg = normalize(22) * scale.fontScale;
|
||||
SIZE.xl = normalize(24) * scale.fontScale;
|
||||
SIZE.xxl = normalize(28) * scale.fontScale;
|
||||
|
||||
@@ -335,7 +335,13 @@ export const Search = ({ close, getKeyboardHeight, quicknote, mode }) => {
|
||||
renderItem={renderItem}
|
||||
estimatedItemSize={50}
|
||||
ListHeaderComponent={
|
||||
searchResults.length === 0 && searchKeyword.current ? (
|
||||
mode === "selectTags" &&
|
||||
(searchResults.length === 0 ||
|
||||
(searchKeyword.current &&
|
||||
searchKeyword.current.length > 0 &&
|
||||
!searchResults.find(
|
||||
(item) => item.title === searchKeyword.current
|
||||
))) ? (
|
||||
<ListItem
|
||||
item={{
|
||||
type: "tag",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
### The desktop app?
|
||||
|
||||
You can find all the desktop related code in [the `desktop/` directory](./desktop/). Since it uses the web app directly, we are keeping both together. (We should probably move it to it's own project at some point.)
|
||||
You can find all the desktop-related code in [the `desktop/` directory](./desktop/). Since it uses the web app directly, we are keeping both together. (We should probably move it to its own project at some point.)
|
||||
|
||||
### Downloads & releases
|
||||
|
||||
@@ -21,7 +21,7 @@ You can find all the desktop related code in [the `desktop/` directory](./deskto
|
||||
|
||||
## 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
|
||||
|
||||
@@ -66,11 +66,11 @@ npx serve apps/web/build
|
||||
|
||||
## 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 v17: UI framework
|
||||
2. Typescript/Javascript: The logical side of the app
|
||||
@@ -82,21 +82,21 @@ We try to keep the stack as lean as possible
|
||||
|
||||
### Project structure
|
||||
|
||||
1. `src/`: 99% of the source code lives here & this is also where you'll be spending most of your time.
|
||||
1. `index.tsx`: **the app entrypoint** responsible for loading the appropriate view based on the current route.
|
||||
2. `app.js`: **the default route** that contains the whole note taking experience (notes list, navigation, editor etc.)
|
||||
3. `views/`: Contains **all the views** including views for login, settings, notes, notebooks & topics.
|
||||
4. `components/`: All the **reusuable UI components** are here (e.g. button, editor, etc.)
|
||||
5. `stores/`: Contains the glue code & **logic for all the UI interaction**. For example, when you pin a note the `src/stores/note-store.js` is responsible for everything including refreshing the list to reflect the changes.
|
||||
6. `navigation/`: All the **routing & navigation** logic lives here. The app uses 2 kinds of routers:
|
||||
1. `routes.js`: This contains all the main routes like `/notes`, `/notebooks` with information on what to render when user goes to a particular route.
|
||||
2. `hash-routes.js`: The hash routes are used for temporary navigation like opening dialogs, opening a note. These look like `#/notes/6307bbd65d5d5d5cb86f6f74/edit`.
|
||||
7. `interfaces/`: This is where the **platform specific storage & encryption logic** lives. These interface implementations are used by the `@notesnook/core` to provide capabilities such as persistence & encryption.
|
||||
8. `hooks/`: Contains all the **general purpose React hooks**
|
||||
9. `utils/`: These are **general-purpose utilities** for performing various tasks such as downloading files, storing configuration etc.
|
||||
1. `src/`: 99% of the source code lives here & this is also where you'll spend most of your time.
|
||||
1. `index.tsx`: **the app entry point** responsible for loading the appropriate view based on the current route.
|
||||
2. `app.js`: **the default route** that contains the whole note-taking experience (notes list, navigation, editor, etc.)
|
||||
3. `views/`: Contains **all the views**, including views for login, settings, notes, notebooks & topics.
|
||||
4. `components/`: All the **reusable UI components** are here (e.g., button, editor, etc.)
|
||||
5. `stores/`: Contains the glue code & **logic for all the UI interactions**. For example, when you pin a note, the `src/stores/note-store.js` is responsible for everything, including refreshing the list to reflect the changes.
|
||||
6. `navigation/`: All the **routing & navigation** logic lives here. The app uses two kinds of routers:
|
||||
1. `routes.js`: This contains all the main routes like `/notes`, `/notebooks` with information on what to render when the user goes to a particular route.
|
||||
2. `hash-routes.js`: The hash routes are used for temporary navigation, like opening dialogs or opening a note. These look like `#/notes/6307bbd65d5d5d5cb86f6f74/edit`.
|
||||
7. `interfaces/`: This is where the **platform-specific storage & encryption logic** lives. These interface implementations are used by the `@notesnook/core` to provide capabilities such as persistence & encryption.
|
||||
8. `hooks/`: Contains all the **general-purpose React hooks**
|
||||
9. `utils/`: These are **general-purpose utilities** for performing various tasks such as downloading files, storing configuration, etc.
|
||||
10. `common/`: This directory contains **the shared logic between the whole app**. For example, this is where the database is instantiated for use throughout the app.
|
||||
11. `commands/`: These are **commands used by the desktop app** for things like checking for updates, storing backups etc.
|
||||
2. `desktop/`: The Electron layer for **the desktop app lives here**. (This should be moved outside into it's own project).
|
||||
11. `commands/`: These are **commands the desktop app uses** for things like checking for updates, storing backups etc.
|
||||
2. `desktop/`: The Electron layer for **the desktop app lives here**. (This should be moved outside into its own project).
|
||||
|
||||
### Running the tests
|
||||
|
||||
|
||||
@@ -94,7 +94,9 @@ export function showAddNotebookDialog() {
|
||||
isOpen={true}
|
||||
onDone={async (nb: Record<string, unknown>) => {
|
||||
// add the notebook to db
|
||||
await db.notebooks?.add({ ...nb });
|
||||
const notebook = await db.notebooks?.add({ ...nb });
|
||||
if (!notebook) return perform(false);
|
||||
|
||||
notebookStore.refresh();
|
||||
|
||||
showToast("success", "Notebook added successfully!");
|
||||
@@ -209,9 +211,8 @@ export function showError(title: string, message: string) {
|
||||
export function showMultiDeleteConfirmation(length: number) {
|
||||
return confirm({
|
||||
title: `Delete ${length} items?`,
|
||||
message: `These items will be **kept in your Trash for ${
|
||||
db.settings?.getTrashCleanupInterval() || 7
|
||||
} days** after which they will be permanently deleted.`,
|
||||
message: `These items will be **kept in your Trash for ${db.settings?.getTrashCleanupInterval() || 7
|
||||
} days** after which they will be permanently deleted.`,
|
||||
positiveButtonText: "Yes",
|
||||
negativeButtonText: "No"
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { useEffect, useState } from "react";
|
||||
import { initializeDatabase } from "../common/db";
|
||||
import "../utils/analytics";
|
||||
import "../app.css";
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
console.log = () => {};
|
||||
@@ -28,14 +29,13 @@ if (process.env.NODE_ENV === "production") {
|
||||
const memory = {
|
||||
isAppLoaded: false
|
||||
};
|
||||
export default function useDatabase(persistence) {
|
||||
export default function useDatabase(persistence?: "db" | "memory") {
|
||||
const [isAppLoaded, setIsAppLoaded] = useState(memory.isAppLoaded);
|
||||
|
||||
useEffect(() => {
|
||||
if (memory.isAppLoaded) return;
|
||||
|
||||
(async () => {
|
||||
await import("../app.css");
|
||||
await initializeDatabase(persistence);
|
||||
setIsAppLoaded(true);
|
||||
memory.isAppLoaded = true;
|
||||
@@ -32,7 +32,7 @@ const errorMessage =
|
||||
* Returns the validity state of the given media query.
|
||||
*
|
||||
*/
|
||||
const useMediaQuery = (mediaQuery) => {
|
||||
const useMediaQuery = (mediaQuery: string) => {
|
||||
if (!window.matchMedia) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(errorMessage);
|
||||
@@ -59,7 +59,7 @@ export default class Notes extends Collection {
|
||||
if (
|
||||
remoteNote.deleted &&
|
||||
remoteNote.deleteReason !== "localOnly" &&
|
||||
!localNote.localOnly
|
||||
(!localNote || !localNote.localOnly)
|
||||
)
|
||||
return await this._collection.addItem(remoteNote);
|
||||
|
||||
|
||||
@@ -79,9 +79,6 @@ export default class Tags extends Collection {
|
||||
return;
|
||||
}
|
||||
|
||||
newName = this.sanitize(newName);
|
||||
if (!newName) throw new Error("Tag title cannot be empty.");
|
||||
|
||||
await this._db.settings.setAlias(tagId, newName);
|
||||
await this._collection.addItem({ ...tag, alias: newName });
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ export const CodeBlock = Node.create<CodeBlockOptions>({
|
||||
id: {
|
||||
default: undefined,
|
||||
rendered: false,
|
||||
parseHTML: () => `codeblock-${nanoid(12)}`
|
||||
parseHTML: () => createCodeblockId()
|
||||
},
|
||||
caretPosition: {
|
||||
default: undefined,
|
||||
@@ -227,13 +227,18 @@ export const CodeBlock = Node.create<CodeBlockOptions>({
|
||||
setCodeBlock:
|
||||
(attributes) =>
|
||||
({ commands }) => {
|
||||
return commands.setNode(this.name, attributes);
|
||||
return commands.setNode(this.name, {
|
||||
...attributes,
|
||||
id: createCodeblockId()
|
||||
});
|
||||
},
|
||||
toggleCodeBlock:
|
||||
(attributes) =>
|
||||
({ commands }) => {
|
||||
console.log("TOGGLING!");
|
||||
return commands.toggleNode(this.name, "paragraph", attributes);
|
||||
return commands.toggleNode(this.name, "paragraph", {
|
||||
...attributes,
|
||||
id: createCodeblockId()
|
||||
});
|
||||
},
|
||||
changeCodeBlockIndentation:
|
||||
(options) =>
|
||||
@@ -477,14 +482,16 @@ export const CodeBlock = Node.create<CodeBlockOptions>({
|
||||
find: backtickInputRegex,
|
||||
type: this.type,
|
||||
getAttributes: (match) => ({
|
||||
language: match[1]
|
||||
language: match[1],
|
||||
id: createCodeblockId()
|
||||
})
|
||||
}),
|
||||
textblockTypeInputRule({
|
||||
find: tildeInputRegex,
|
||||
type: this.type,
|
||||
getAttributes: (match) => ({
|
||||
language: match[1]
|
||||
language: match[1],
|
||||
id: createCodeblockId()
|
||||
})
|
||||
})
|
||||
];
|
||||
@@ -520,7 +527,7 @@ export const CodeBlock = Node.create<CodeBlockOptions>({
|
||||
if (isCode && !isInsideCodeBlock) {
|
||||
tr.replaceSelectionWith(
|
||||
this.type.create({
|
||||
id: `codeblock-${nanoid(12)}`,
|
||||
id: createCodeblockId(),
|
||||
language,
|
||||
indentType: indent.type,
|
||||
indentLength: indent.amount
|
||||
@@ -805,3 +812,7 @@ export function inferLanguage(node: Element) {
|
||||
);
|
||||
return language?.filename;
|
||||
}
|
||||
|
||||
function createCodeblockId() {
|
||||
return `codeblock-${nanoid(12)}`;
|
||||
}
|
||||
|
||||
@@ -125,9 +125,14 @@ export function HighlighterPlugin({
|
||||
|
||||
const changedBlocks: Set<string> = new Set();
|
||||
for (const blockKey in pluginState.languages) {
|
||||
if (HIGHLIGHTED_BLOCKS.has(blockKey)) continue;
|
||||
|
||||
const language = pluginState.languages[blockKey];
|
||||
if (
|
||||
HIGHLIGHTED_BLOCKS.has(blockKey) &&
|
||||
refractor.registered(language)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const languageDefinition = Languages.find(
|
||||
(l) =>
|
||||
l.filename === language || l.alias?.some((a) => a === language)
|
||||
@@ -193,12 +198,21 @@ export function HighlighterPlugin({
|
||||
});
|
||||
if (changedBlocks.length > 0) {
|
||||
const updated: Set<number> = new Set();
|
||||
let hasChanges = false;
|
||||
|
||||
changedBlocks.forEach((block) => {
|
||||
if (updated.has(block.pos)) return;
|
||||
updated.add(block.pos);
|
||||
|
||||
const { id, language } = block.node.attrs;
|
||||
if (languages[id]) {
|
||||
|
||||
if (
|
||||
!languages[id] ||
|
||||
(language && !refractor.registered(language))
|
||||
) {
|
||||
languages[id] = language;
|
||||
hasChanges = true;
|
||||
} else {
|
||||
const newDecorations = getDecorations({
|
||||
block,
|
||||
defaultLanguage
|
||||
@@ -221,12 +235,12 @@ export function HighlighterPlugin({
|
||||
decorations = decorations.remove(toRemove);
|
||||
if (toAdd.length > 0)
|
||||
decorations = decorations.add(tr.doc, toAdd);
|
||||
} else {
|
||||
languages[id] = language;
|
||||
|
||||
hasChanges = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (decorations !== pluginState.decorations) {
|
||||
if (hasChanges) {
|
||||
return { decorations, languages };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`Adding a new codeblock & changing the language should apply the new highlighting 1`] = `"<div><div contenteditable=\\"true\\" translate=\\"no\\" tabindex=\\"0\\" class=\\"ProseMirror\\"><div class=\\"codeblock-view-content-wrap\\"><div class=\\"node-content-wrapper\\" style=\\"white-space: pre; min-width: 20px;\\"><span class=\\"token keyword\\">function</span> <span class=\\"token function\\">hello</span><span class=\\"token punctuation\\">(</span><span class=\\"token punctuation\\">)</span> <span class=\\"token punctuation\\">{</span> <span class=\\"token punctuation\\">}</span></div></div></div></div>"`;
|
||||
|
||||
exports[`codeblocks should get highlighted after pasting 1`] = `"<div><div contenteditable=\\"true\\" translate=\\"no\\" tabindex=\\"0\\" class=\\"ProseMirror\\"><div class=\\"codeblock-view-content-wrap\\"><div class=\\"node-content-wrapper\\" style=\\"white-space: pre; min-width: 20px;\\"><span class=\\"token keyword\\">function</span> <span class=\\"token function\\">hello</span><span class=\\"token punctuation\\">(</span><span class=\\"token punctuation\\">)</span> <span class=\\"token punctuation\\">{</span> <span class=\\"token punctuation\\">}</span></div></div><div class=\\"codeblock-view-content-wrap\\"><div class=\\"node-content-wrapper\\" style=\\"white-space: pre; min-width: 20px;\\"><span class=\\"token keyword\\">function</span> <span class=\\"token function\\">hello</span><span class=\\"token punctuation\\">(</span><span class=\\"token punctuation\\">)</span> <span class=\\"token punctuation\\">{</span> <span class=\\"token punctuation\\">}</span></div></div></div></div>"`;
|
||||
|
||||
exports[`codeblocks should get highlighted on init 1`] = `"<div><div contenteditable=\\"true\\" translate=\\"no\\" tabindex=\\"0\\" class=\\"ProseMirror\\"><div class=\\"codeblock-view-content-wrap\\"><div class=\\"node-content-wrapper\\" style=\\"white-space: pre; min-width: 20px;\\"><span class=\\"token keyword\\">function</span> <span class=\\"token function\\">hello</span><span class=\\"token punctuation\\">(</span><span class=\\"token punctuation\\">)</span> <span class=\\"token punctuation\\">{</span> <span class=\\"token punctuation\\">}</span></div></div><div class=\\"codeblock-view-content-wrap\\"><div class=\\"node-content-wrapper\\" style=\\"white-space: pre; min-width: 20px;\\"><span class=\\"token keyword\\">function</span> <span class=\\"token function\\">hello</span><span class=\\"token punctuation\\">(</span><span class=\\"token punctuation\\">)</span> <span class=\\"token punctuation\\">{</span> <span class=\\"token punctuation\\">}</span></div></div></div></div>"`;
|
||||
|
||||
@@ -22,6 +22,7 @@ import { expect, test, vi } from "vitest";
|
||||
import { CodeBlock, inferLanguage } from "../code-block";
|
||||
import { HighlighterPlugin } from "../highlighter";
|
||||
import { getChangedNodes } from "../../../utils/prosemirror";
|
||||
import { refractor } from "refractor/lib/core";
|
||||
|
||||
const CODEBLOCKS_HTML = h("div", [
|
||||
h("pre", [h("code", ["function hello() { }"])], {
|
||||
@@ -191,3 +192,37 @@ test("editing code in a highlighted code block should not be too slow", async ()
|
||||
expect(timings.reduce((a, b) => a + b) / timings.length).toBeLessThan(16);
|
||||
expect(editorElement.outerHTML).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("Adding a new codeblock & changing the language should apply the new highlighting", async () => {
|
||||
const editorElement = h("div");
|
||||
const { editor } = createEditor({
|
||||
element: editorElement,
|
||||
extensions: {
|
||||
codeblock: CodeBlock
|
||||
}
|
||||
});
|
||||
|
||||
editor.commands.setCodeBlock();
|
||||
editor.commands.insertContent("function hello() { }");
|
||||
|
||||
editor.commands.updateAttributes(CodeBlock.name, { language: "javascript" });
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
expect(editorElement.outerHTML).toMatchSnapshot();
|
||||
expect(refractor.registered("javascript")).toBe(true);
|
||||
});
|
||||
|
||||
test("Switching codeblock language should register the new language", async () => {
|
||||
const editorElement = h("div");
|
||||
const { editor } = createEditor({
|
||||
element: editorElement,
|
||||
initialContent: CODEBLOCKS_HTML,
|
||||
extensions: {
|
||||
codeblock: CodeBlock
|
||||
}
|
||||
});
|
||||
editor.commands.updateAttributes(CodeBlock.name, { language: "java" });
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
expect(refractor.registered("java")).toBe(true);
|
||||
});
|
||||
|
||||
@@ -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 { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import tinycolor from "tinycolor2";
|
||||
import { PopupWrapper } from "../../components/popup-presenter";
|
||||
import { config } from "../../utils/config";
|
||||
@@ -26,6 +26,7 @@ import { ColorPicker, DEFAULT_COLORS } from "../popups/color-picker";
|
||||
import { useToolbarLocation } from "../stores/toolbar-store";
|
||||
import { ToolProps } from "../types";
|
||||
import { getToolbarElement } from "../utils/dom";
|
||||
import { PositionOptions } from "../../utils/position";
|
||||
|
||||
type ColorToolProps = ToolProps & {
|
||||
onColorChange: (color?: string) => void;
|
||||
@@ -49,6 +50,15 @@ export function ColorTool(props: ColorToolProps) {
|
||||
const [colors, setColors] = useState(
|
||||
config.get<string[]>(`custom_${cacheKey}`, []) || []
|
||||
);
|
||||
const position: PositionOptions = useMemo(() => {
|
||||
return {
|
||||
isTargetAbsolute: true,
|
||||
target: getToolbarElement(),
|
||||
align: isBottom ? "center" : "end",
|
||||
location: isBottom ? "top" : "below",
|
||||
yOffset: 10
|
||||
};
|
||||
}, [isBottom]);
|
||||
|
||||
useEffect(() => {
|
||||
config.set(`custom_${cacheKey}`, colors);
|
||||
@@ -73,13 +83,7 @@ export function ColorTool(props: ColorToolProps) {
|
||||
isOpen={isOpen}
|
||||
id={props.icon}
|
||||
group={"color"}
|
||||
position={{
|
||||
isTargetAbsolute: true,
|
||||
target: getToolbarElement(),
|
||||
align: isBottom ? "center" : "end",
|
||||
location: isBottom ? "top" : "below",
|
||||
yOffset: 10
|
||||
}}
|
||||
position={position}
|
||||
focusOnRender={false}
|
||||
blocking={false}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user