Encrypted Note Editor App In React Native
Tags: reactnative, richtext, editor, opensource
Published: 2024-03-04 17:43:18

Hey everyone! If you're anything like me, you're probably always taking notes. It could be for class, personal reflections, or even those random poems that pop into your head at 3 AM. So, how cool would it be to build our own notes app? This app is going to be more than just a place to dump your thoughts. We're talking offline and persisted, encrypted, neat lists, text formatting and even a search function to find stuff easily enhanced with AI Image Captioning. Thanks to React Native and its awesome community, we can get this done without it taking ages.

I'm going to keep this guide straightforward and skip the super nitty-gritty details, because let’s face it, that would take all day and you want to see the juicy stuff. But don't worry, the entire app is open source. So, if you're curious about the bits and bobs of how everything works or want to dive deeper on your own, everything's available right here: https://github.com/10play/EncryptedNotesApp 

Before we get into the code, let’s do a quick overview of the main packages we will use to make this app.

**The Editor**: The core of our app is the editor. We need an easy to use and robust rich text editor, that supports all of the features we want such as: headings, lists, placeholders, markdown, color, images, bold italic etc… For this we will use [@10play/tentap-editor](https://github.com/10play/10Tap-Editor) which is a rich text editor for react native based on [Tiptap](https://tiptap.dev/). 

**Storing the notes**: For storing the notes, we will use the amazing [WatermelonDB](https://github.com/Nozbe/WatermelonDB) package which is a popular sqlite wrapper for react-native. Instead of using the default package we will use a [fork](https://github.com/10play/WatermelonDBCipher) of this that uses sqlcipher instead of the regular sqlite, allowing us to encrypt the database by passing a secret key.

**Storing the secret key**: Since our db requires a key, it is important to store that key somewhere secure, so we will use [react-native-keychain](https://github.com/oblador/react-native-keychain) which will store our key securely.

**Camera and Image Captioning**: For taking pictures we will use [react-native-vision-camera](https://github.com/mrousavy/react-native-vision-camera) and for generating captions from the images, [react-native-quick-tflite](https://github.com/mrousavy/react-native-fast-tflite).

Let’s get started!

## Creating The Editor

First things first, let’s create the core of the app, the editor. We will be using [TenTap](https://github.com/10play/10Tap-Editor). TenTap is based on TipTap, and comes with a bunch pre-built plugins, if we wanted to, we could create more such as mentions or drop cursors, but for now we just use the out of the box ones.

We will create a new component `Editor.tsx` and add our editor
```tsx
export const Editor = () => {
 const editor = useEditorBridge({
   avoidIosKeyboard: true, 
   autofocus: true,
   initialContent: '<h1>Untitled Note</h1>',
 });


 return (
   <SafeAreaView style={{flex: 1}}>
     <RichText editor={editor} />
   </SafeAreaView>
 );
};
```
We create our editor instance with `useEditorBridge` and pass the following params:
`avoidIOSKeyboard`- keep content above the keyboard on ios
`autoFocus`- autofocus the note and open the keyboard 
`initialContent` - the initial content to display in the editor (eventually we will pass the note stored in our db)

Now we have an Editor component but it is pretty boring, let’s add a toolbar. The TenTap docs have a bunch of guides that show us [how to do just this](https://10play.github.io/10tap-editor/docs/examples/basic#adding-a-keyboard-aware-toolbar)
```tsx
   <SafeAreaView style={{flex: 1}}>
     <RichText editor={editor} />
     <KeyboardAvoidingView
       behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
       style={{
         position: 'absolute',
         width: '100%',
         bottom: 0,
       }}>
       <Toolbar
         editor={editor}
       />
     </KeyboardAvoidingView>
   </SafeAreaView>
```

We add the toolbar in a `KeyboardAvoidingView` to keep it just above the keyboard, we could also make it static like in some mail apps.

TenTap allows us to customize the extensions, this can be anything from adding a custom editor schema, to adding a placeholder to adding light/dark/custom themes.

First let’s make our notes always start with a heading, to do this we will `extendExtension` of the `CoreBridge` to make it’s content enforce a heading node followed by many blocks.
```tsx
 const editor = useEditorBridge({
   bridgeExtensions: [
     ...TenTapStartKit,
     CoreBridge.extendExtension({
       content: 'heading block+',
     })]
    ....
```

Now, let’s add a placeholder to that first node

```tsx
     CoreBridge.extendExtension({
       content: 'heading block+',
     }),
     PlaceholderBridge.configureExtension({
       showOnlyCurrent: false,
       placeholder: 'Enter a Title',
     }).configureCSS(`
       .ProseMirror h1.is-empty::before {
         content: attr(data-placeholder);
         float: left;
         color: #ced4da;
         height: 0;
       }
       `),
```
Here we configure the `Placeholder` extension to add placeholders with the text of `Enter a Title`, we can then add some custom css to show the placeholder.

Now finally, let’s add some custom css to make it prettier!
We will start by creating a const with out custom css

```tsx
const editorCss = `
 * {
   font-family: sans-serif;
 }
 body {
   padding: 12px;
 }
 img {
   max-width: 80%;
   height: auto;
   padding: 0 10%;
 }
`;
```

And now we will configure the `CoreBridge` to use this css
```tsx
CoreBridge.configureCSS(editorCss)
```

**Taking Pictures**
To add images with searchable captions to the editor, we need to implement two things

1. A way to take pictures and insert them into the editor, (i won’t be going into this, you can follow this blog https://dev.to/guyserfaty/rich-text-editor-with-react-native-upload-photo-3hgo)
2. A way to generate captions from images, I used the example provided in `react-native-quick-tflite` [see here](https://github.com/mrousavy/react-native-fast-tflite/blob/main/example/src/App.tsx)

In the end, a new component called `Camera`, the `Camera` component will receive a function called `onPhoto`, when a picture is taken the callback will be called with
`path`: the path to the picture taken
`captions`: the captions generated from the picture.

```tsx
<EditorCamera onPhoto={async (path, captions) => {
 // Add the image to the editor
 editor.setImage(`file://${photoPath}`);
 // Update the editors selection
 const editorState = editor.getEditorState();
 editor.setSelection(editorState.selection.from, editorState.selection.to);
 // Focus back to the editor
 editor.focus();
 // TODO - update the note’s captions
}} />
```

> If you want to take a deeper look at the caption implementation check [this](https://github.com/10play/EncryptedNotesApp/blob/main/src/Editor/EditorCamera.tsx)

### Ok, now we have most of the editor setup let's start making it persistent 

First thing is to get the content from the editor each time it changes, there are a couple of ways to get the content from the editor.

1. Use the `onChange` param, which will be called each time the editor content is change and then use either `editor.getHTML`, `editor.getText` or `editor.getJSON`. 
2. Use the `useEditorContent` hook - monitors changes to the editor's content and then debounces the content

Both are viable options for us, but we will use the `useEditorContent` hook. 
Since the `useEditorContent` hook will render each content change, we will create another component called `Autosave` and pass the `editor` and later on the `note` model there to avoid rendering the Editor component too much.

```tsx
export const AutoSave = ({editor, note}: AutoSaveProps) => {
 const docTitle = useEditorTitle(editor);
 const htmlContent = useEditorContent(editor, {type: 'html'});
 const textContent = useEditorContent(editor, {type: 'text'});


 const saveContent = useCallback(
   debounce(
     async (note, title, html, text) => {
	// TODO save note
     },
   ),
   [],
 );


 useEffect(() => {
   if (htmlContent === undefined) return;
   if (docTitle === undefined) return;
   if (textContent === undefined) return;


   saveContent(note, docTitle, htmlContent, textContent);
 }, [note, saveContent, htmlContent, docTitle, textContent]);


 return null;
};
```

**Setting Up The Encrypted DB**

As mentioned before, we will use a [fork](https://github.com/10play/WatermelonDBCipher) of WatermelonDB that uses SQLCipher instead of SQLite (won’t go into how this fork was made but If you are interested let me know!)

First let’s define our db’s schema
```tsx
const schema = appSchema({
 tables: [
   tableSchema({
     name: NotesTable,
     columns: [
       {name: 'title', type: 'string'},
       {name: 'subtitle', type: 'string', isOptional: true},
       {name: 'html', type: 'string'},
       {name: 'captions', type: 'string', isIndexed: true},
       {name: 'text', type: 'string', isIndexed: true},
     ],
   }),
 ],
 version: 1,
});
```

> We save the text of the note in addition to the html, to give us the ability to later search for text in notes.

Now that we have the schema let’s create our Note Model
```tsx
export class NoteModel extends Model {
 static table = NotesTable;


 @text(NoteFields.Title) title!: string;
 @text(NoteFields.Subtitle) subtitle?: string;
 @text(NoteFields.Html) html!: string;
 @text(NoteFields.Text) text!: string;
 @json(NoteFields.Captions, sanitizeCaptions) captions!: string[];


 @writer async updateNote(
   title: string,
   htmlContent: string,
   textContent: string,
 ) {
   await this.update(note => {
     note.title = title;
     note.html = htmlContent;
     note.text = textContent;
   });
 }


 @writer async updateCaptions(captions: string[]) {
   await this.update(note => {
     note.captions = captions;
   });
 }


 @writer async deleteNote() {
   await this.destroyPermanently();
 }
}
```
We add some additional functionality into our note model, such as update for updating the note with new content, and updateCaptions for updating our notes captions.

Now let’s use `react-native-keychain` to get and set our db’s password.

```tsx
import * as Keychain from 'react-native-keychain';


const createPassphrase = () => {
 // This is not safe at all, but for now we'll just use a random string
 return Math.random().toString(36);
};


export const getPassphrase = async () => {
 const credentials = await Keychain.getGenericPassword();
 if (!credentials) {
   const passphrase = createPassphrase();
   await Keychain.setGenericPassword('passphrase', passphrase);
   return passphrase;
 }
 return credentials.password;
};
```

## Connecting Everything Together
Now that we have our editor set up, and our database ready, all that is left is to connect the two.

First we will create a `NoteList` component, that queries all of our notes and renders them, with WaterMelonDB this is done with `Observers`

```tsx
// Enhance our _NoteList with notes
const enhance = withObservables([], () => {
 const notesCollection = dbManager
   .getRequiredDB()
   .collections.get<NoteModel>(NotesTable);
 return {
   notes: notesCollection.query().observe(),
 };
});
const NotesList = enhance(_NotesList);
```

This is an HOC that queries all of our notes and passes them as props to the `_NotesList component`, which can be implemented as follows
```tsx
interface NotesListProps {
 notes: NoteModel[];
}
const _NotesList = ({notes}: NotesListProps) => {
 const renderNode: ListRenderItem<NoteModel> = ({item: note}) => (
   <NoteListButton
     onPress={() => {
	// Navigate to our editor with its the note
       navigate('Editor', {note});
     }}>
     <StyledText>{note.title || 'Untitled Note'}</StyledText>
     <DeleteButton onPress={() => note.deleteNote()}>
       <StyledText>Delete</StyledText>
     </DeleteButton>
   </NoteListButton>
 );
 return (
   <FlatList
     data={notes}
     renderItem={renderNode}
     keyExtractor={note => note.id}
   />
 );
};
```

We also need to add a button that creates notes

```tsx
     <CreateNoteButton
       onPress={async () => {
         await db.write(async () => {
           await db.collections.get<NoteModel>(NotesTable).create(() => {});
         });
       }}>
```

Now we should see a new note added each time we create a new note, and if we press it, it should navigate us to the Editor with the NodeModel that we pressed. Because we have the note model now we can set the editors initial content to the html saved in the NoteModel.

```tsx
 const editor = useEditorBridge({
   initialContent: note.html,
```

Then in our auto save component we can call `note.update`
```tsx
 const saveContent = useCallback(
   debounce(
     async (note: NoteModel, title: string, html: string, text: string) => {
       await note.updateNote(title, html, text); // <-- call the updateNote function we created on our model
     },
   ),
   [],
 );
```

And let’s also update the captions in our onPhotoCallback
```tsx
 const onPhoto = async (photoPath: string, captions: string[]) => {
   …
   const uniqcaptions = Array.from(new Set([...note.captions, ...captions]));
   await note.updateCaptions(uniqcaptions);
 };
```

This is looking much much better! We have an editor with encrypted and persisted notes, all that is left is to add search!

We will add a new parameter to our Notes Observer call query, and query all the notes that contain the text or caption in the query:
```tsx
const enhance = withObservables(['query'], ({query}: {query: string}) => {
 const notesCollection = dbManager
   .getRequiredDB()
   .collections.get<NoteModel>(NotesTable);
 return {
   notes: query
     ? notesCollection
         .query(
           Q.or([
             Q.where(NoteFields.Text, Q.like(`%${query}%`)),
             Q.where(NoteFields.Captions, Q.like(`%${query}%`)),
           ]),
         )
         .observe()
     : notesCollection.query().observe(),
 };
});
```

Now the NotesList component is used like this:
```tsx
     <SearchInput
       value={queryValue}
       onChangeText={text => {
         setQueryValue(text);
       }}
     />
     <NotesList query={queryValue} />
```

That is it we're done!

![Happy Cat](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k37nx6653n277vb3qn9l.gif)




I tried to get as much information in this blog as possible without making it a super long read, so many boring things were left out, but as mentioned before all of the code is open source, so for better understanding check it out and run it yourself!