Skip to content

Desktop, Mobile: Resolves #16038: Add note history support for locked notes - #5

Open
keshav0479 wants to merge 20 commits into
gsoc-local-note-encryption-mobile-lock-uifrom
gsoc-local-note-encryption-revisions
Open

Desktop, Mobile: Resolves #16038: Add note history support for locked notes#5
keshav0479 wants to merge 20 commits into
gsoc-local-note-encryption-mobile-lock-uifrom
gsoc-local-note-encryption-revisions

Conversation

@keshav0479

@keshav0479 keshav0479 commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Summary

Adds note history for locked notes, behind the feature flag. Issue: laurent22#16038. Stacked on laurent22#16069

Revisions of a locked note are stored standalone, no parent and full contents, since diffing a ciphertext body just produces meaningless patches. The revision viewer on desktop and mobile shows the note screen's unlock panel for an encrypted revision and decrypts it once the session is unlocked. Restoring keeps the body encrypted so the restored copy stays locked, and plaintext revisions are deleted when a note arrives locked through sync.

The revision viewer now takes the selected note id instead of the form note's, since a locked note has no form note while the session is locked and the history was coming up empty. hasNoteLockKey moved out of NoteEditor into its own module so both screens can use it.

Locked notes build up history faster than normal ones, since re-encryption changes the ciphertext on every save so a revision is never seen as unchanged.

The Data API block isn't in this PR. It needs the same thing on the notes route to be consistent, so it goes with the API guardrails.

With the flag off, note history behaves exactly as before.

Testing

  • yarn tsc
  • yarn workspace @joplin/lib test (RevisionService, RevisionService.noteLock, noteLock, Synchronizer.revisions)
  • yarn workspace @joplin/app-mobile test NoteRevisionViewer
  • yarn workspace @joplin/app-desktop test useFormNote
  • manually on desktop and the mobile web app: the panel with the session locked, unlocking to read a revision, restore keeping the note encrypted, and note history with the flag off behaving as before. Also the cannot-decrypt message after a password reset on desktop, and a revision with an attachment on mobile

Screenshots

Desktop

Locked session

1-desktop-locked

After unlocking

2-desktop-unlocked

After a password reset

3-desktop-cannot-decrypt

Mobile

Locked session Revision with an attachment
4-mobile-locked 5-mobile-attachment

AI Assistance Disclosure

I used AI tools while working on this PR for code suggestions and review, checking scope and tests, and drafting parts of this description, including the disclosure. I reviewed the final changes and reran the tests listed above myself.

Repository owner deleted a comment from github-actions Bot Jul 27, 2026
Repository owner deleted a comment from github-actions Bot Jul 27, 2026
Repository owner deleted a comment from github-actions Bot Jul 28, 2026
# Conflicts:
#	packages/app-desktop/gui/NoteEditor/NoteEditor.tsx
#	packages/app-desktop/gui/WindowCommandsAndDialogs/commands/enableNoteEncryption.ts
#	packages/app-desktop/gui/WindowCommandsAndDialogs/commands/print.ts
#	packages/app-mobile/components/NoteItem.tsx
#	packages/app-mobile/components/screens/Note/Note.tsx
#	packages/app-mobile/components/screens/Note/NoteLockPanel.tsx
#	packages/lib/components/shared/note-screen-shared.test.ts
#	packages/lib/components/shared/note-screen-shared.ts
#	packages/lib/reducer.ts
#	packages/lib/services/commands/stateToWhenClauseContext.ts
@keshav0479
keshav0479 changed the base branch from gsoc-local-note-encryption-mobile-safe-behavior to gsoc-local-note-encryption-mobile-lock-ui July 28, 2026 18:13
Repository owner deleted a comment from github-actions Bot Jul 28, 2026
return (
<div style={revStyle} ref={containerRef}>
<NoteRevisionViewer customCss={props.customCss} noteId={formNote.id} onBack={noteRevisionViewer_onBack} />
<NoteRevisionViewer customCss={props.customCss} noteId={isNoteLockEnabled() ? effectiveNoteId : formNote.id} onBack={noteRevisionViewer_onBack} />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a locked note is selected with the session locked there's no form note at all ( gated load returns nothing), so formNote.id is empty. we can still open note history from that state and the viewer needs to know which note to load, so it takes the selected note id instead. With the flag off it sticks to formNote.id so the original behaviour is untouched.

output.title_diff = Revision.createTextPatch('', noteTitle);
output.body_diff = Revision.createTextPatch('', noteBody);
output.metadata_diff = Revision.createObjectPatch({}, noteMd);
} else if (!parentRev) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wasn't in the original spec, but based on your questions on the guard rails PR, please make the following change here (deliberately not including the feature flag):
else if (!parentRev || (!!parentRev.is_locked && !note.is_locked))

This means that when transitioning from encrypted to unencrypted note, the unencrypted revisions will start a fresh chain, and it is safe to exclude locked revisions from the api

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. The test that covered this expected the unencrypted revision to chain onto the locked one, so it now expects the fresh chain instead. Restoring the older locked revision is still covered.

Repository owner deleted a comment from github-actions Bot Aug 2, 2026
@mrjo118

mrjo118 commented Aug 4, 2026

Copy link
Copy Markdown

This looks good to me but I have not tested it. I'll defer manual testing until some of your other PRs are merged

});

await waitFor(() => {
expect(screen.getByText('This note is encrypted. Enter the note lock password to unlock encrypted notes for this session.')).toBeVisible();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per laurent22#16160, this text needs to be changed upstream, then sync this PR and update the test to match

const renderForm = () => {
if (props.undecryptable) {
return (
<Text style={styles.message}>{_('This note could not be decrypted. If it was encrypted prior to a password reset, the contents are no longer recoverable.')}</Text>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change to:
"This note could not be unlocked. If it was locked prior to a password reset, the contents are no longer recoverable"

…cal-note-encryption-revisions

# Conflicts:
#	packages/app-mobile/components/screens/NoteRevisionViewer.tsx
#	packages/lib/Synchronizer.ts
Repository owner deleted a comment from github-actions Bot Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants