Skip to content

feat: Meet Encly, and issues #170–#180 (list, search, toolbars, editors, reminders, history, order, undo) - #169

Merged
pasichDev merged 43 commits into
masterfrom
feat/meet-encly
Oct 5, 2026
Merged

pasichDev merged 43 commits into
masterfrom
feat/meet-encly

Conversation

@pasichDev

@pasichDev pasichDev commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

✅ Ready: Encly 2.1.0 is live in Google Play. Includes the release commit for 2.7.56.

After merge: tag the merge commit v2.7.56 on master → ci-cd.yml builds, signs and uploads the AAB to Play closed testing and creates the GitHub Release → promote to production in Play Console.

What

Why

Encly replaces My Notes, and users need a safe one-tap move to it. The other changes close issues #170–#180.

Behaviour changes

  • Database 22 → 23 (note_versions, notes.customPosition).
  • Custom order and version history are device-local and are not synced. Older app versions on other devices never see them.
  • A one-time reminder missed by less than 24 h now fires once on the next start or boot. Before, it was dropped.

Testing

  • ./gradlew :app:assembleDebug :app:test :app:lintDebug :app:spotlessCheck: pass on the branch head.
  • Device: a Pixel 7a debug build, driven over adb.
    • The real hand-off to Encly 2.1.0 debug worked: 16 notes and 2 tags, with the counts shown back.
    • The Meet Encly page was checked in light, dark and font scale 1.3.
    • The empty-tag fix was checked.
    • A full QA pass over every area: no crashes, 9 bugs found and fixed, then a second pass.
    • Rechecked on the device after the fixes: list position after the editor and after trashing the top note, untitled new note at the top, swipe-left select returns the card, drag in list and grid, reminders and history through rotation, Undo/Redo bar, More sheet labels, custom order hint and handles.

Not verified: MigrationTest 22→23 (instrumented, not run); RTL (the app has supportsRtl off).

References

Closes #167, closes #170, closes #171, closes #172, closes #173, closes #174, closes #175, closes #176, closes #177, closes #178, closes #179, closes #180 · pasichDev/Encly#46 · pasichDev/Encly#56

My Notes gets a successor, Encly: an encrypted, offline notes app. A new
"Meet Encly" page (drawer, next to the what's-new entry, plus a one-time
dialog after the update) explains what Encly is and what does not move.

With a trusted Encly installed, "Move my notes to Encly" writes a ZIP with
handoff.json (format mynotes-handoff, schema 1) to the cache and hands it
to com.pasich.encly.action.IMPORT_FROM_MY_NOTES through the FileProvider
with a read grant. Before any plaintext leaves, the receiver must resolve
the action and carry the pinned Encly signing certificate; API 26-27 fall
back to GET_SIGNATURES with exactly one signer. The payload is raw My
Notes data with stable ids from sync_metadata (or mynotes:<type>:<id>);
attachment files, reminders and pins are not sent, and the page counts
what stays behind. The ZIP is deleted after the result, and leftovers are
swept on the next start.

Nothing is deleted automatically. After Encly confirms the move, "Clear
My Notes" is offered as a separate step with its own confirmation; it
cancels reminders, deletes the records with sync tombstones so a Drive
copy follows, and removes attachment files.

Refs: #167
@pasichDev pasichDev self-assigned this Oct 5, 2026
@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Test coverage (unit + instrumentation)

Overall Project 17.97% -16.96% 🍏
Files changed 16.93% 🍏

Module Coverage
debug 19.61% -14.28% 🍏
debug 16.33% -19.65% 🍏
Files
Module File Coverage
debug ReminderRepeat.java 100% 🍏
CustomOrder.java 100% 🍏
NoteVersionEntity.java 100% 🍏
NoteListTransition.java 100% 🍏
DragTarget.java 100% 🍏
HandoffResult.java 100% 🍏
RetainedEditHistory.java 100% 🍏
KeyboardRequest.java 100% 🍏
EditorCursor.java 100% 🍏
PickedFile.java 100% 🍏
RecentAttachmentUploads.java 100% 🍏
SyncMetadata.java 98.89% 🍏
NoteSearchRanker.java 98.49% -1.51% 🍏
TextEditHistory.java 98.38% -1.62% 🍏
HandoffPayloadBuilder.java 98.35% -1.65% 🍏
ExtendedViewStateJson.java 97.78% -2.22% 🍏
FormatListTool.java 97.22% -2.78% 🍏
PositionRestorer.java 96.46% -3.54% 🍏
KeyboardBarState.java 96.43% -3.57% 🍏
EditorAttachmentBlocks.java 93.83% -4.19% 🍏
EnclyReceiverCheck.java 93.6% -6.4% 🍏
SearchText.java 93.59% -6.41% 🍏
NoteHistory.java 91.85% -8.15% 🍏
NoteVersionRestore.java 91.83% -8.17% 🍏
SyncConflictPresentation.java 90.56% -0.99% 🍏
RepeatRule.java 87.58% -12.42% 🍏
NoteViewState.java 85.71% -14.29% 🍏
HandoffArchive.java 83.9% -16.1% 🍏
PreferencesConfig.java 81.25% 🍏
NotePresenter.java 72.06% -5.31% 🍏
AttachmentCleaner.java 67.27% -1.29% 🍏
MainViewState.java 65.92% -28.7% 🍏
SyncMutationCoordinator.java 65.37% -2.03% 🍏
Note.java 63.95% 🍏
NoteDiff.java 56% -4% 🍏
MainPresenter.java 54.87% -1.9% 🍏
SearchHit.java 51.72% -48.28% 🍏
AppPreferencesHelper.java 50.36% -0.72% 🍏
NoteVersionReason.java 50% -50% 🍏
AppPreferencesCache.java 48.07% -8.42% 🍏
SearchHintFitter.java 15.38% -84.62% 🍏
NoteNavigator.java 8% -26% 🍏
AttachmentStorage.java 0.84% -21.52% 🍏
NoteHistoryActivity.java 0% 🍏
ReminderTapActivity.java 0% -19.35% 🍏
MeetEnclyActivity.java 0% 🍏
SnoozeActivity.java 0% -8.33% 🍏
MainActivity.java 0% -57.47% 🍏
BootReceiver.java 0% -74.47% 🍏
ReminderReceiver.java 0% -59.71% 🍏
UpdateChecker.java 0% -17.86% 🍏
MeetEnclyDialog.java 0% 🍏
NoteVersionSheet.java 0% 🍏
EditHistoryRows.java 0% 🍏
RestoreVersionDialog.java 0% 🍏
MoreNoteDialog.java 0% -14.67% 🍏
CustomRepeatDialog.java 0% 🍏
ReminderPickerBottomSheet.java 0% -51.08% 🍏
EditorJSInterface.java 0% -79.33% 🍏
AppDataManager.java 0% -3.96% 🍏
NoteDragCallback.java 0% 🍏
NotesGridLayoutManager.java 0% 🍏
NotesItemAnimator.java 0% 🍏
SwipeToListNotesCallback.java 0% -50% 🍏
NoteBindingAdapters.java 0% -4.91% 🍏
SelectionController.java 0% -19.43% 🍏
AppDbHelper.java 0% -14.09% 🍏
AppDatabase.java 0% -5.71% 🍏
InteractionSettingsFragment.java 0% -65.39% 🍏
NoteActivity.java 0% -63.36% 🍏
BaseNoteEditorActivity.java 0% -46.66% 🍏
NoteExtendedEditorActivity.java 0% -61.74% 🍏
RoomSyncStore.java 0% -8.01% 🍏
EditorKeyboardBar.java 0% 🍏
QuickActionsRow.java 0% 🍏
ItemTouchHelperAccess.java 0% 🍏
ChoiceDialog.java 0% 🍏
SortParam.java 0% 🍏
AndroidEnclyInspector.java 0% 🍏
EnclyMigrationRepository.java 0% 🍏
EditableLinkMovementMethod.java 0% 🍏
NoteViewStateStore.java 0% 🍏
NoteOpeningPreferences.java 0% 🍏
NavigationController.java 0% -9.04% 🍏
AppUpdateController.java 0% -54.79% 🍏
ViewFader.java 0% 🍏
MainRenderListsController.java 0% -88.58% 🍏
ViewOptionsDialog.java 0% 🍏
RepeatRuleFormatter.java 0% 🍏
ReminderDraft.java 0% 🍏
ReminderRescheduler.java 0% 🍏
TaskReminderManager.java 0% -9.68% 🍏
ReminderManager.java 0% -89.85% 🍏
SnoozeStore.java 0% 🍏
NoteVersionText.java 0% 🍏
NoteVersionAdapter.java 0% 🍏
NoteEditorView.java 0% -60.69% 🍏
ListUtilsModule.java 0% -15.63% 🍏
ApplicationModule.java 0% 🍏
SearchNotesAdapter.java 0% -61.58% 🍏
NotePayloads.java 0% 🍏
DatabaseConstants.java 0% 🍏
NoteAdapter.java 0% -76.3% 🍏
debug NoteVersionEntity.java 100% 🍏
AppDatabase.java 82.66% 🍏
RoomSyncStore.java 81.67% -0.83% 🍏
PreferencesConfig.java 81.25% 🍏
AttachmentCleaner.java 75% -5.67% 🍏
ApplicationModule.java 68.83% 🍏
BootReceiver.java 68.09% -31.91% 🍏
SyncMetadata.java 55.56% 🍏
NoteVersionReason.java 50% -50% 🍏
ReminderRescheduler.java 49.74% -50.26% 🍏
Note.java 46.71% -1.25% 🍏
NoteHistory.java 41.01% -58.99% 🍏
EditorAttachmentBlocks.java 38.33% -39.87% 🍏
RecentAttachmentUploads.java 36.76% -63.24% 🍏
SnoozeStore.java 26.19% -73.81% 🍏
ReminderManager.java 25.19% -67.29% 🍏
AttachmentStorage.java 13.71% -21.52% 🍏
NoteViewStateStore.java 12.79% -87.21% 🍏
TaskReminderManager.java 7.53% -9.68% 🍏
CustomOrder.java 6.73% -93.27% 🍏
MainViewState.java 0% -83.41% 🍏
RepeatRule.java 0% 🍏
ReminderRepeat.java 0% -9.76% 🍏
NoteHistoryActivity.java 0% 🍏
ReminderTapActivity.java 0% -19.35% 🍏
MeetEnclyActivity.java 0% 🍏
SnoozeActivity.java 0% -8.33% 🍏
MainActivity.java 0% -57.47% 🍏
ReminderReceiver.java 0% -59.71% 🍏
UpdateChecker.java 0% -17.86% 🍏
MeetEnclyDialog.java 0% 🍏
NoteVersionSheet.java 0% 🍏
EditHistoryRows.java 0% 🍏
RestoreVersionDialog.java 0% 🍏
MoreNoteDialog.java 0% -14.67% 🍏
CustomRepeatDialog.java 0% 🍏
ReminderPickerBottomSheet.java 0% -51.08% 🍏
ExtendedViewStateJson.java 0% 🍏
EditorJSInterface.java 0% -79.33% 🍏
AppDataManager.java 0% -3.96% 🍏
AppPreferencesHelper.java 0% -0.72% 🍏
NoteVersionRestore.java 0% 🍏
NoteDragCallback.java 0% 🍏
NotesGridLayoutManager.java 0% 🍏
NoteListTransition.java 0% 🍏
DragTarget.java 0% 🍏
NotesItemAnimator.java 0% 🍏
SwipeToListNotesCallback.java 0% -50% 🍏
SearchText.java 0% 🍏
NoteSearchRanker.java 0% 🍏
SearchHit.java 0% 🍏
SearchHintFitter.java 0% 🍏
NoteBindingAdapters.java 0% -4.91% 🍏
SelectionController.java 0% -19.43% 🍏
AppDbHelper.java 0% -14.09% 🍏
MainPresenter.java 0% -30.51% 🍏
NotePresenter.java 0% -29.35% 🍏
InteractionSettingsFragment.java 0% -65.39% 🍏
NoteDiff.java 0% -42% 🍏
NoteActivity.java 0% -63.36% 🍏
BaseNoteEditorActivity.java 0% -46.66% 🍏
NoteExtendedEditorActivity.java 0% -61.74% 🍏
SyncMutationCoordinator.java 0% -20.05% 🍏
EditorKeyboardBar.java 0% 🍏
QuickActionsRow.java 0% 🍏
ItemTouchHelperAccess.java 0% 🍏
ChoiceDialog.java 0% 🍏
SortParam.java 0% 🍏
EnclyReceiverCheck.java 0% 🍏
HandoffPayloadBuilder.java 0% 🍏
HandoffResult.java 0% 🍏
AndroidEnclyInspector.java 0% 🍏
HandoffArchive.java 0% 🍏
EnclyMigrationRepository.java 0% 🍏
RetainedEditHistory.java 0% 🍏
KeyboardBarState.java 0% 🍏
KeyboardRequest.java 0% 🍏
EditableLinkMovementMethod.java 0% 🍏
TextEditHistory.java 0% 🍏
EditorCursor.java 0% 🍏
PositionRestorer.java 0% 🍏
NoteViewState.java 0% 🍏
AppPreferencesCache.java 0% -32.98% 🍏
NoteOpeningPreferences.java 0% 🍏
NavigationController.java 0% -9.04% 🍏
AppUpdateController.java 0% -54.79% 🍏
ViewFader.java 0% 🍏
MainRenderListsController.java 0% -88.58% 🍏
PickedFile.java 0% 🍏
ViewOptionsDialog.java 0% 🍏
SyncConflictPresentation.java 0% -12.75% 🍏
NoteNavigator.java 0% -34% 🍏
FormatListTool.java 0% -86.11% 🍏
RepeatRuleFormatter.java 0% 🍏
ReminderDraft.java 0% 🍏
NoteVersionText.java 0% 🍏
NoteVersionAdapter.java 0% 🍏
NoteEditorView.java 0% -60.69% 🍏
ListUtilsModule.java 0% -15.63% 🍏
SearchNotesAdapter.java 0% -61.58% 🍏
NotePayloads.java 0% 🍏
DatabaseConstants.java 0% 🍏
NoteAdapter.java 0% -76.3% 🍏

After a Google Drive sync the main screen could go blank or show an empty
category until the app was restarted, and the list animations often froze
or drew one card on top of another.

The selected category was a snapshot filtered by name, so a tag renamed,
merged or deleted by sync emptied the page. The presenter now re-resolves
the selection against every fresh tags list (by id, then by name, else All
notes), stores the result and marks the chip from it. Tags and notes
emissions are folded with a 50 ms debounce, and the view state compares
by content, so a sync that rewrites rows without changing them no longer
re-renders the list.

The list, the empty state and the tags row are now driven toward a target
state that cancels whatever animation is running, so an empty-then-filled
update inside the hide animation can no longer leave the list invisible.
States that arrive in selection mode are held and applied when it ends
(dropping selected notes that sync removed), and states that arrive right
after returning from the editor wait for the return transition.

Card overlap came from change cross-fades and per-update span
reassignment in the staggered grid. Change animations are off, spans are
only rebuilt while the list is hidden, and a reorder (or an insert/remove
in a multi-column grid) is shown as one short cross-fade of the list
instead of cards sliding across columns. Item animations are skipped when
the screen is not visible or system animations are off.

Refs: #170
Every sync applies the whole merged snapshot, and the apply re-wrote every
known record: notes with a REPLACE insert, tags, tasks and task lists with
an update, plus two sync_metadata updates each. Room invalidates a query on
any write to its tables, so even a sync with nothing new re-emitted the
notes, tags and tasks lists and the main screen redrew and re-animated.

The apply now builds the row a version would produce and writes it only
when it differs from the stored row. The comparison is made on the row, not
on the version hash, because a note whose attachment references are moved
to this device's files on apply hashes the same before and after; that
first normalising apply is still written. A note with no attachments counts
as unchanged whether it stores null or "[]". Version bookkeeping is re-read
inside the transaction and only the columns that differ are written, for
live records and tombstones alike. Changed records, revivals and deletions
are applied exactly as before, and the wire format is unchanged.

New Robolectric tests count row writes with SQLite triggers on the watched
tables: a repeated sync of an unchanged library writes no rows and does not
re-emit the notes Flowable, while a changed note or task and a tombstone are
still applied and written. An instrumented case covers a note with an
attachment.

Refs: #170
Search only knew one rule: a title equal to the query came first and
everything else followed by date. The check compared the untrimmed query,
so a trailing space defeated it, tags were never searched, accents and
repeated spaces made otherwise equal text miss, and queries shorter than
two characters returned nothing at all.

A new pure-Java NoteSearchRanker folds every note once per notes list
(NFKD, diacritics removed, locale-independent lower case, whitespace
collapsed) and sorts matches into buckets: exact title, title prefix,
whole word in the title, title contains, tag name, then note text. Inside
a bucket pinned notes come first, then the newest. A one-character query
searches titles only; tags and text are searched from two characters.
The ranking now runs after the debounce on a worker thread instead of on
the main thread for every keystroke, and the view learns from the result
whether an empty list should ask the user to keep typing.

Search results highlight the matched part of the title and preview with
the theme's primary container colours, mapped back through the folding so
accented and decomposed text is marked at the right characters.

SearchFilterTest now exercises the production ranker instead of a copy of
the filter: every bucket in order, diacritics, Cyrillic, trailing and
inner spaces, one-character queries, the pinned tie-break, the tag filter
and a 5,000-note timing smoke test. MainPresenterTest covers the debounced
search stream end to end.

Refs: #175
The main search bar carried two separate actions, Sort and Format, with
vague labels and a layout icon that showed the opposite layout at launch
because FormatListTool.init() was never called. They are now one View
options action that opens a bottom sheet with the sort order (radio
group) and a List/Grid segmented control. Choices apply at once and the
sheet stays open; the activity saves and applies them, so the sheet
survives recreation. The action's icon and TalkBack label show the
current layout. Switching the layout now goes through the list's fade
swap, so cards no longer slide over one another when the column count
changes.

The search hint lost its trailing space and falls back to a short
"Search" when the full hint does not fit, so large text and long
languages no longer cut it off. The drawer lists Tags and Trash first,
then Tasks, and the stats card no longer duplicates the Support button;
its decorative icons are hidden from TalkBack so rows are read once.

In the note editors the toolbar keeps Back, the save status and More.
The reminder bell, which duplicated Reminder in More, is replaced by a
chip at the top of the note showing the upcoming reminder's day and
time; tapping it opens the reminder picker and it is hidden when there
is no reminder. The extended editor's reading-mode action has a
localized title that follows its state.

Strings are added in all shipped locales and the help texts describe
the new places for sorting, layout and reminders.

Refs: #180
The simple editor saved the scroll position when the keyboard opened and
jumped back to it when the keyboard closed, so text edited in between was
scrolled away. That restore is gone: opening the keyboard brings the caret
into view and closing it moves nothing. Tapping Edit no longer forces the
caret to the end of the note; it goes to the first visible line of a long
note, or to the end when the end is on screen, and a selection restored
after a rotation wins over both.

The body field used a LinkMovementMethod subclass, which clears the
selection whenever the field takes focus, so the caret fell back to offset
0 and typing landed in the first line. It is replaced by an
ArrowKeyMovementMethod that opens a link only on a short tap on it, and not
when the caret already sits inside the link.

Copying the body, counting words and comparing it with the note ran on
every keystroke; they now run once typing pauses for 250 ms. The extended
editor no longer parses the whole Editor.js document on the UI thread for
every change: the presenter stores the JSON and derives the plain text and
attachment list when the note is saved or read.

Both editors call the new NotePresenter.flushPending() from onStop, so the
edits still inside the two-second autosave debounce are written when the
screen goes away; the extended editor also asks the page for its document
without waiting for Editor.js change batching. The write is not tied to the
presenter's CompositeDisposable, so a destroy right after onStop cannot
cancel it. A save of a note with no meaningful content no longer reports
success, which used to run the attachment cleanup against a list that was
never written.

The extended editor pads its root by the keyboard or navigation bar, as it
draws edge to edge and was not resized by the keyboard, and the page keeps
the caret visible when its height changes. Both editors keep their reading
position across a rotation (a text offset for the simple editor, the top
visible block for the extended one), plus the selection, edit mode and any
draft whose save is still in flight.

Refs: #171
The image tool uploaded through a synchronous bridge call that carried the
whole file as base64 and returned only after Android had decoded,
optimized and written it, so the editor's JS thread was blocked for the
length of the save and the page jumped when it caught up. Uploads are now
asynchronous: the page sends a request id, Android stores the file on a
background thread and resolves the page's promise through
evaluateJavascript. For a file from the system picker Android keeps the
content URI from the chooser result and reads it itself, so the bytes never
cross the bridge; pasted or dropped files still send base64, but the call
returns at once. Attachments use the same path.

The stored image's width and height are returned with the upload and kept
in the block data. The image tool reserves the picture's aspect ratio from
the preview while it uploads, from the stored size once it is saved, and on
every later render, so inserting or loading an image no longer changes the
height of the note around the caret. The uploading placeholder used a
hard-coded white background; it and the page background now follow the
theme, and the WebView paints the theme surface before the page loads, so
the editor no longer flashes white in the dark theme.

AttachmentCleaner deleted any file not referenced by the saved note,
including an image whose block had not been saved yet. Files uploaded in
this process are now protected until a saved note references them, and
files younger than ten minutes are never treated as orphans, which covers a
process that died before the save.

When the screen is recreated while the picker is open, the result reached a
page that no longer existed and the picked file was dropped. The picker's
kind and the index of the block that opened it are kept in the saved
state; a result that comes back without a waiting page is stored and its
block is inserted at that index once the note has rendered again. Recent
edits are already written on onStop by the previous change.

Refs: #172
Both editors now remember, per note and on this device only, where the
note was being read or edited, and go back there the next time it is
opened, including after an app restart.

The simple editor keeps the selection (when the note was left while
editing), the offset at the top of the viewport, the scroll position,
and the length and a hash of the text, plus a short piece of text on
each side of the caret and of the top line. PositionRestorer, a pure
Java class with its own tests, uses the exact offsets when the text is
unchanged, finds them again by that context when the note was edited
here or on another device, maps them proportionally after a modest
change with no recognisable context left, and opens at the top when the
note was emptied or largely rewritten. Every offset it returns is
inside the current text, so no selection can go out of range. The
restored caret is used when editing starts while it is still on
screen.

The extended editor reports the block at the top of the viewport and
the caret's block, input and character offset once scrolling or the
caret settles. Editor.js keeps block ids across saves, so the position
is found by id even after blocks moved; when every saved block is gone
only the block index is kept, clamped. After render the page puts the
caret back through editor.caret.setToBlock and scrolls the saved block
into place, and autofocus is turned off when a position is about to be
restored so the first block does not take the caret first.

Positions live in their own preferences file (NoteViewStateStore), not
on the Note entity and not in backups or sync, limited to the 200 most
recently used notes. They are written in onPause and removed when a
note is deleted for good: from the trash, by clearing the trash, by a
sync deletion from another device, or by clearing the library after
the move to Encly. A rotation keeps using the existing in-memory
anchor.

Refs: #176
Settings > Interaction gains three options, all off-sync and kept in a
preferences file of their own (NoteOpeningPreferences) rather than in
PreferencesBackup, so an older app version restoring or syncing
settings cannot reset them.

Open notes in: Automatic (the default, and the behaviour so far:
reading in the simple editor, editing in the extended one), Reading
mode or Editing mode. New notes always open for editing. Opening an
existing note in editing mode waits for the first layout, applies the
saved position, and then places the caret by the rules editing already
uses: the restored caret if it is on screen, else the first visible
line, or the end of a note that fits on screen. The keyboard opens in
both editors; Automatic keeps the extended editor's keyboard closed as
before.

Open notes at: Where you left off (the default, using the positions
from the previous commit) or The beginning.

Double-tap to edit, off by default: in reading mode a double tap on the
text starts editing there. The simple editor watches the gesture in
dispatchTouchEvent, since the body is disabled while reading, and maps
the point with getOffsetForPosition. The extended editor handles
dblclick in the page, finds the block, input and offset with
caretRangeFromPoint, leaves reading mode and puts the caret there.

The extended editor now starts read-only through Editor.js's own
readOnly option instead of toggling after load, and its reading mode
also locks the title, which stayed editable before. Switching modes
keeps the block at the top of the viewport in place, the page reports
every switch to Android, and the toolbar action shows the right icon
from the start and after a double tap. The mode survives a rotation.

Reminder notifications always opened NoteActivity, so notes with
attachments, or every note with the extended editor switched on, opened
in the wrong editor. The receiver now looks the note up off the main
thread and both it and ReminderTapActivity build the intent through
NoteNavigator.

The two choices use a new settings choice dialog in the style of the
theme mode dialog, with a line explaining each option and the current
one announced as selected. The forward chevron used by settings rows
now mirrors in right-to-left layouts.

Refs: #177
… or years

The reminder picker gets a Custom chip next to Daily, Weekly and Monthly. It
opens a dialog for a number from 1 to 999 and a unit, and the chosen rule is
shown as "Every 3 hours" on the chip, the note card, the reminder chip at the
top of the note and in More, with plurals in every shipped language.

The rule is a RepeatRule kept in the existing reminderRepeat column and sync
field, so there is no migration. Daily, weekly and monthly are still written
as DAILY, WEEKLY and MONTHLY whenever an older version can follow them; other
rules are stored as EVERY:<n>:<UNIT>@<anchor>, which an older version reads as
a one-time reminder. Every occurrence is computed from the anchor instead of
from "now", so reminders no longer drift later, a monthly reminder on the 31st
returns to the 31st after a short month, a yearly one on 29 February falls on
28 February in other years, hours are real elapsed time and days keep the
wall-clock time across daylight-saving changes.

Scheduling was reworked so the schedule survives everything around it. The
"repeat notification" nag and snooze get their own alarms and no longer
overwrite the next occurrence; snoozes are remembered on the device. A
ReminderRescheduler re-arms reminders at boot, after an app update, when the
exact-alarm permission is granted, at app start and after a sync applies
changed or deleted notes, so a reminder edited on another device is armed or
cancelled here at once. A missed occurrence fires once and then rolls forward,
instead of being dropped. Without the exact-alarm permission reminders are
armed inexactly rather than skipped, and the picker and the main screen offer
to turn the permission on. Task reminders get the same inexact fallback.

Refs: #179
Autosave overwrote a note in place, so text removed by accident and then saved
was gone for good. Each note now keeps up to 20 earlier versions in a new
note_versions table on this device. The editor's save keeps the text it
replaces at most once every ten minutes, or at once when it removes at least
200 characters; a version applied by a sync, the side picked in a conflict and
a restore always keep the text they replace first. Duplicates of the latest
version are skipped and the oldest versions are pruned in the same transaction.

The table has no foreign key on purpose: notes are written with REPLACE
inserts, which SQLite performs as delete plus insert, so a cascade would wipe
the history on every sync. History is deleted explicitly when a note is
deleted for good, when the trash is emptied, when all data is cleared and when
another device deletes the note, and a sweep after each sync removes anything
left without its note. It stays while the note is in the trash, never syncs
and is not part of backups.

More -> Version history, in the editor and on the notes list, opens a new
screen listing the versions with time, reason and the start of the text. A
version opens in a sheet next to the current note with the differing part
highlighted in both, using the comparison extracted from the sync conflict
presentation. Restoring asks for confirmation, keeps the current text as a
version, and writes the version as an ordinary edit that syncs like any
other. Only text comes back: attachment blocks whose files the note no longer
holds are left out, and the sheet says so. The editor saves pending edits
before opening the history and reloads the note after a restore.

Migration 22 -> 23 also adds notes.customPosition for the custom order in
#178. It is seeded newest edit first, rides in local JSON backups as "n",
is stripped from sync payloads so hashes and older clients are unaffected, and
is preserved when a sync rewrites the note row.

Refs: #174
View options gains a Custom order next to the two date orders. In it, press
and hold a note and drag it, in the list or in the two-column grid; letting
go without moving opens the note's menu as before, and TalkBack users get
Move up and Move down actions on every card instead. Pinned notes keep their
own section above the others and are arranged among themselves.

Order is notes.customPosition, larger first, handed out 1024 apart so a drop
takes the midpoint between its neighbours and writes one row; only when two
neighbours touch is the whole order renumbered. A move updates that column
alone, without the note's date or sync timestamp, so it is not an edit and
is never published. New notes, copies, notes from another device and notes
restored from a backup that predates the order start at the top; a local JSON
backup keeps the position.

The drag runs on the adapter's own working copy of the list, and the dropped
order is handed to the differ with its updates muted, since the cards are
already where it says they are; dispatching the moves a second time is what
makes cards slide over each other. States that arrive mid-drag, such as a
sync, are held until the drop has settled. NoteAdapter now wraps an
AsyncListDiffer directly to allow this. A dragged card is no longer faded by
the swipe fade, which only applies to swipes now.

The Custom choice is stored in a device-local flag. The synced and backed-up
sort setting keeps DataSort or DataReserve, which older versions read
correctly, and applying synced settings leaves the flag alone.

Refs: #178
Undo and Redo appear in the note toolbar while a note is being edited,
after the save status so its coming and going never moves them, and are
dimmed and disabled when there is nothing to take back or bring back.
All toolbar actions are now shown as "always": with "ifRoom" a phone
narrower than 500dp shows at most three actions and More would fall into
an overflow menu. Ctrl+Z, Ctrl+Shift+Z and Ctrl+Y work on a hardware
keyboard.

The simple editor records every change to the title and the body as a
diff in TextEditHistory, a plain Java class. Letters typed in a row, a
keyboard rewriting the word it composes, and backspace through a typo
join one step while they follow closely in time and place; a new word, a
paste and typing after a deletion start new steps. Each step keeps the
selection it replaced. Steps are bounded in count and characters, changes
the app makes itself (loading the note, applying an undo) are not
recorded, and an undo is applied as an ordinary edit so it is autosaved.
The title's line-break handling moved from rewriting the title after the
fact to an input filter, so the history only sees the title as it is.
The most recent steps, up to 32K characters, survive a recreation and are
put back only for the same note holding exactly the same text.

The extended editor keeps its history in the page: whole document states
with the caret, batched on a pause, a finished word or line, a block
added, removed or moved, or after a few seconds of typing. Undo applies a
state with editor.blocks.update for blocks whose content changed and
renders again only when the list of blocks changed, then puts the caret
and scroll back with the existing view-state helpers. The result is sent
to Android like any edit. The browser's own undo is replaced, a block
deleted together with its file is taken out of every state, and files an
undoable step removed are kept from the orphan cleanup so an undo never
brings back a block without its file.

The presenter starts the history again whenever a note is put into the
editor (load, copy, a restored version) and never on save, so autosave
leaves it intact and no undo reaches into another note.

Refs: #173
A full list swap fades the notes out, then submits the new list from a
callback. A second swap requested while the first was still fading
replaced that callback, and a plain update arriving in the same window
submitted its list straight away while the older swap's list was still
pending, so the older list could land last. A sync finishing right after
a category switch, or a layout change made mid-fade, could leave the
screen on stale notes or keep the previous column count.

The swap now reads what it applies when it runs: the newest notes, their
category, the saved layout and whether to start at the top are held on
the activity and committed together, and any state that arrives while a
swap is in progress joins it instead of racing it.

Refs: #170
The page now opens on a clear hero: Encly's real icon, a "from the author
of My Notes" label, the title and a one-sentence promise. Four benefit rows
with tonal icons follow (encrypted, offline with no account, PIN and
fingerprint lock, free and open source), then two cards that say what
moves to Encly and what stays in My Notes, each as a label with its count.
Rows with nothing to move or leave behind are hidden, and when nothing
stays behind the card says so.

Each state has one full-width primary action and its own helper text; the
update and wrong-certificate states explain themselves in a tinted card,
and the button reads "Preparing your notes" while the hand-off is being
built. The result is a card with a success or failure badge, the counts
Encly reported, and clearing My Notes as a secondary, red text action
that still asks for confirmation. Content keeps a 600 dp maximum width on
tablets, count rows are read by TalkBack as one item, and headings are
marked as such.

The one-time bottom sheet uses the same hero and three benefit rows with
a filled primary button and "Later". Opening the page now also marks that
introduction as seen, so it can never appear after the user has already
visited the page; a Robolectric test pins the shown-once rule across
restarts and later updates.

All meet_encly strings are rewritten to be shorter and plainer, in all
eleven locales.

Refs: #167
Switching from a tag with no notes back to All notes (or to another tag)
left the list blank: the notes were there, but none was laid out until
the app was restarted. On a device the list had all its items and no
child views, and a forced layout pass did not help.

When a swap brought in an empty list, the grid's columns were reset and
it was scrolled to the top while it had no items. That left the
StaggeredGridLayoutManager anchored wrong, so the next non-empty list was
laid out below the screen. The column reset and the jump to the top now
happen only when there are notes to show, also when the layout switches
between list and grid on an empty tag.

Refs: #170
Undo and Redo took two of the few places in the note toolbar, and in the
extended editor they crowded Back, the title, the save status, Read and
More. They now sit on a slim bar docked on top of the soft keyboard, the
same EditorKeyboardBar in both editors: Undo and Redo at the start, Hide
keyboard at the end, and room between them for formatting actions later.
The toolbar is back to Back, the title, the save status, Read in the
extended editor, and More.

The bar is shown only while a note is edited and the on-screen keyboard
is up, so it is absent in reading mode and with a hardware keyboard,
where Ctrl+Z, Ctrl+Shift+Z and Ctrl+Y keep working. It is a 48dp row of
48dp icon buttons under a 1dp hairline in colorOutlineVariant, on
colorSurfaceContainer; icons are colorOnSurfaceVariant and colorOnSurface
at 38% when there is nothing to undo or redo. The buttons carry content
descriptions and tooltips, and the arrows are auto-mirrored.

The bar is laid out where the keyboard ends up and translated frame by
frame with a WindowInsetsAnimationCompat callback, so it rides on the
keyboard as it slides in and out instead of jumping ahead of it; with
animations off the keyboard and the bar simply appear. Both editors end
their content above the keyboard plus the bar: the simple editor's
scroll view and caret scrolling, and the extended editor's WebView,
which is now sized by a bottom margin instead of padding the root, so
the page's own resize handling keeps the caret line above the bar. Side
system bars and cutouts in landscape keep the buttons clear of them.

Without the keyboard, Undo and Redo are in More while a note is edited,
dimmed and disabled like the bar; the sheet stays open and follows the
history, so several steps can be taken back in a row.

Refs: #173
@pasichDev pasichDev changed the title feat: introduce Encly and move the library to it in one tap feat: Meet Encly, and issues #170–#180 (list, search, toolbars, editors, reminders, history, order, undo) Oct 5, 2026
Moving the top note to the trash left All notes with an empty list until
another tag was chosen, and after editing, pinning or restoring a note,
creating one or switching to the custom order the list often stayed
scrolled down, with the new top note out of sight.

The jump to the top was requested from the commit callback of the new
list, before the list had laid out the change. StaggeredGridLayoutManager
resolves a pending scroll against the children of the previous layout and
takes the position of its first child as the anchor. When that child was
the removed top note the anchor was -1 and nothing was laid out; when the
new top note was a visible card moved up from below, the offset was
measured from where that card used to be and the list landed at a wrong
place.

The jump now waits until the change has been laid out and is applied
before the next frame is drawn, and it expands the search bar and tags
again so the top note is fully visible. A note created while the editor
was in front jumps to the top instead of starting a smooth scroll on a
list that is not on screen.

Refs: #170
In the extended editor, a note opened in reading mode and then switched
to editing (the Edit action or a double tap) never enabled Undo or Redo,
however much was typed.

The page starts its undo history by saving the document it has just
rendered. Editor.js refuses to save while it is read-only, so for a note
shown in reading mode that first save failed and the history was never
started; every later edit was then ignored. The history now starts when
editing does, from the document as it is at that moment, and change
reports Editor.js sends late about its own re-render are not taken for
edits.

The editor page gets a small Node test setup (npm test) that runs the
page runtime against a stand-in for Editor.js.

Refs: #173
…closes

Leaving the extended editor logged "Application attempted to call on a
destroyed WebView": the flush asked for in onStop was posted to the
WebView and ran after onDestroy had already destroyed it. The page
answers a flush asynchronously as well, so an answer still on its way
when the WebView was destroyed was lost, and with it the last edit.

The WebView is now kept until the page has handed over its last edit,
for 1.5 s at most, and only then destroyed. Every script sent to the page
goes through one path that runs at once on the main thread and drops
anything that would reach a destroyed WebView.

Rotating the screen also started the undo history again. When the
screen is only being recreated, the finishing page now hands its history
over as well, bounded to 1.5 million characters of document (the oldest
undo and farthest redo steps go first). It is kept in memory, not in the
saved state, which is far too small for it, and the new page takes it
over when it shows the same document; otherwise it starts empty as
before.

Refs: #173
With "Open in editing mode" the extended editor opened in editing mode
without a caret or a keyboard, and typing went nowhere. A new note did
not get a keyboard either, and its title hint read "Title..." in every
language.

The keyboard was asked for once, as soon as the page reported the note
rendered. That is usually while the screen is still opening, before the
window has focus, or before the WebView is connected to the input
method, and the system drops such a request silently. The request now
waits for window focus and is repeated for up to a second until the
keyboard is actually visible, and the page is asked to hold a caret
first (the start of the first block, without moving the page) when it
has none. A new note asks for the keyboard in every opening mode, as in
the simple editor.

The page's runtime set the title hint itself, hard-coded in English, on
every note it loaded, over the localized one. It now uses the hint for
the page's language, the page gets the app's language rather than the
JVM default, and the editor's texts are translated into Belarusian,
Italian and Kazakh too.

Refs: #177
… title

With "where I left off", a note read in the extended editor often opened
at its end or at an older place; only a note left at the very top was
right. With "at the start" the title was scrolled out of sight.

The WebView stayed hidden (GONE) until the note had been shown, so the
page placed the caret and applied the saved position in a viewport of no
size; Editor.js scrolled the caret "into view" past the title, and a
restored scroll position was clamped or measured against a layout the
page never had on screen. The WebView is now laid out at its full size
from the start and is only transparent until it fades in. A note opened
with no saved position also scrolls back to its title after the caret
is placed.

The position kept when leaving was whatever the page had reported last,
once scrolling had settled for 200 ms. In reading mode nothing else
reports it, so leaving in the middle of a fling, or right after it,
kept an older place. The page now also reports while a scroll goes on
and when it is hidden, and on pause Android asks the page for its
position right then and saves that answer over the last report.

Refs: #176
In the custom order a card dragged in the grid often came back to where
it was, its menu opened and a gap was left. In the list, dragging onto
the first card usually opened the menu, and when the move did happen the
list scrolled so the moved card was out of sight.

ItemTouchHelper only trades places once the dragged card's edge has
passed the other card's edge; onto the first card that means pushing it
above the top of the list, so the card was let go without a move and its
menu opened. A card now trades places once its middle is over the other
card and past that card's middle, which also keeps cards of different
heights from swapping back and forth.

After a move StaggeredGridLayoutManager lays the list out again from the
first card it had on screen, so a card moved in front of it landed above
the screen; its view left the list and the drag ended there. The
fallback ItemTouchHelper uses for layout managers without
prepareForDrop asked for a scroll to the new position, which this
layout manager resolves against the layout before the move. The notes
now use a layout manager that, once a move is laid out, scrolls only as
far as needed to keep the dragged card where it was dropped, and the
fallback scroll is gone.

The drop is stored for the dragged note found in the order on screen,
not for its view's position, which may still be unknown right after the
last move, and the grid's columns are laid out again after a drop so no
gap is left. The drag handling moved out of MainActivity into
NoteDragCallback so it can be tested with real touch events.

Refs: #178
…hen a time has passed

Rotating the screen with the reminder sheet open lost the chosen date
and time and a custom repeat: nothing the sheet held was in its saved
state, and the note loaded again put the stored reminder back over it.
A date or time picker open during the rotation came back without its
listener, so its answer went nowhere. Choosing a time that had already
passed only flashed a short toast over the time picker, easy to miss,
and the choice simply did not appear.

The time, the repeat (with the custom rule), the repeated notification
and a picked day whose time is still being chosen are now kept in the
saved state and put back, the stored reminder no longer overwrites
them, and open pickers get their listeners again. A time in the past is
reported in the sheet itself, with the time chosen, until a later one
is picked; this also covers a time that passes while the sheet is open.
A picked day is read as that day in the local time zone; west of UTC it
was taken for the day before.

Refs: #179
…gh a rotation

Rotating the screen closed the preview of a version and the "Restore this
version?" question.

The preview is recreated with the screen, but the recreated screen loads
the note and its versions again in the background; the preview asked for
them at once, found nothing yet and closed itself as if its version had
been deleted. It now waits until the screen has loaded them and closes
only when the version is really gone. The question was a plain dialog,
which does not survive a recreation; it is now a dialog fragment whose
answer reaches the recreated screen.

Refs: #174
…s text sizes

A version kept less than a minute ago read "0 minutes ago": the system's
relative time counts in whole minutes. It now reads "Just now" with the
time, and older versions keep the system's wording.

The highlighted difference started and ended at the first and last
character that differed, so a change from 1200 to 1500 marked only the
"2" and the "5", cutting through numbers and words. The highlight is now
widened to whole words and numbers.

The version's text in the preview used a larger type than the current
note's; both now use the same body style.

Refs: #174
TalkBack read the simple editor's edit button as "EditNote", an
untranslated identifier, and the first button of the selection bar as
"Tag options". On the notes list that button changes the tag of the
selected notes; in the trash it was shown too, although it does nothing
there.

The edit button now reads "Edit note" and the selection bar's tag
button "Change tag", in every language, and the tag button is no longer
shown for notes in the trash.

Refs: #170
…being cut off

At the default font size and at 1.3x, "Редагувати копію нотатки" in the
More sheet's quick actions read "Редагувати к…" and "Сьогодні ввечері"
in the reminder sheet read "Сьогодні вве…": both labels were held to a
single line. They may now take a second line, the presets with a little
less side padding.

Refs: #179
Under "Stays in My Notes" the Meet Encly page listed "Pinned notes", then
said they move. The line now names what actually stays behind, the pin
on the notes, and says the notes themselves move unpinned, in every
language.

Refs: #167
Coming back from Settings, Trash, Tasks or any other screen opened from
the navigation drawer, the drawer was still open: its header buttons
opened the screen but never closed it. The drawer now closes, without
animation, once the notes screen is out of sight, so it is closed on the
way back and is not seen sliding shut as the other screen opens.

Refs: #170
The Undo and Redo entry said the history survives turning the screen in
the simple editor only; the extended editor keeps it too now.

Refs: #173
The screenshots showed an older look of the app. They are replaced with four
new shots taken on a Pixel 7a with demo content: the note list with tags and a
pinned note, the editor with the undo and redo bar above the keyboard, version
history with a preview of what changed, and the Meet Encly page.

The features list now also names undo and redo, version history, custom
order, custom reminder repeats and moving notes to Encly.

Refs: #167
A new note, most often one with only a body, came back partly or wholly
hidden above the list, under the tags. The note is added to the list as an
empty card while the list is still on screen, and that state started a
smooth scroll to it. The editor opens over the list 80 ms later and the
scroll stopped part-way, so the list kept an offset from where the scroll
was cut off. Whether a note was shown right depended on which of two near
simultaneous paths delivered the new list first, not on the title.

The new note is now put at the top with the same jump that is used for any
other new top note: it waits for the list to be laid out and does not depend
on the list staying in front.

Refs: #170
With a pinned note at the top, editing another note and going back left the
list scrolled so that the edited note was the first card, with the pinned
note, the search bar and the tags out of sight. The edited note moves up
under the pinned one, but the first note does not change, and the list was
only put back at the top when the first note changed. The reordered list is
laid out on the way back, while the return transition runs, and nothing
corrected where it landed.

The screen now remembers whether the list was at its top when it was left.
If it was, every list applied meanwhile, and the end of the return, put the
first note back at the top with the search bar and tags expanded. A list
the user had scrolled down stays where it was.

Refs: #170
…ld has focus

Now and then, most often on the first open after the open mode was changed,
the extended editor opened in editing mode with the keyboard on screen but
attached to nothing: no caret, and typing went nowhere. The page was asked
to focus its caret and the keyboard was asked for right after, before the
WebView itself had the focus and before the page's field held it. The
keyboard then came up for an input of no type, and because it was visible
the request counted as done.

The WebView is now focused first and then the page's caret, and the
keyboard is asked for only once the WebView reports a focused editable
field. At that point the input method is connected to the field afresh, so
a keyboard that came up early is attached to it, and the request ends only
when the keyboard is up after that.

Refs: #177
In the list, swiping a card to the left started selection as expected, but
the card stayed swiped off the screen, leaving a blank place in the list.
The swipe helper keeps drawing a swiped card where the swipe left it until
the card's view leaves the list. That used to happen when the card was
rebound, because the change was shown with a second copy of the card.
Since change animations were turned off to stop cards being drawn over one
another, the same view is rebound in place and was never let go.

After a swipe to the left the helper now lets go of the card and the card
slides back into place with its own animation, which the list's item
animator does not cancel when the card shows that it is selected. A swipe
to the right still moves the note to the trash, and a swipe that is not
finished still springs back as before.

Refs: #170
…of any height

Holding a short card at the bottom of the screen did not scroll the list,
while a tall card did. The drag helper scrolls only once the card crosses
the list's bottom, and with the search bar expanded the list reaches below
the screen by the height of the bar: a short card held under the finger at
the bottom edge never got there.

The list now scrolls when the card enters a band at the visible top or
bottom of the list, whatever its height, faster the closer it gets to the
edge, and the cards it passes are swapped even while the finger is held
still. A card dropped first in the list, often after scrolling up past the
top edge, is shown at the top with the search bar and tags expanded again.

Refs: #178
In the preview of a version of a long note, the whole text was marked as
changed in both boxes when a few lines far apart had changed. The two texts
were compared as one block: everything from the first difference to the
last was marked, and with changes near the start and the end that was the
whole note.

The lines of the two texts are now matched first, so lines that are the
same stay unmarked, and in each run of changed lines only the changed words
are marked. A text too long to show is cut around its first change. Notes
with very many lines still fall back to the single range.

Refs: #174
… does not fit

The quick action labels in the More sheet were still cut off at the default
font size ("Редагувати копію нота…", "Зробити переклад н…"), and at 1.3x
they broke inside words ("Редагува/ти", "Поділити/ся"). Four actions side
by side leave each label too narrow for these words.

The labels now break only between words, in at most two lines, with less
side padding around each action. When a label still does not fit, or one
of its words is wider than its place, the actions go two to a row, so no
label is cut off or split inside a word. The translations are unchanged.

Refs: #179
…les in it

Notes can be dragged only in the custom order, and nothing said so: a long
press in any other order opened the note's menu and that was all.

The first time a note is long-pressed outside the custom order, once its
menu is closed, a message says how to reorder notes by dragging, with a
button that turns the custom order on, the same as in View options. It is
shown once ever, and not while notes are selected or searched. The long
press still opens the menu as before.

In the custom order, the cards that can be dragged carry a small drag
handle at the top end; pinned cards, which stay above the others, do not.
The first line of text keeps clear of it. The handle is not read out:
the cards' move up and move down actions already serve screen readers.

The message is translated into all languages of the app.

Refs: #178
The unreleased section had grown entry by entry into long, overlapping
paragraphs, some of them describing problems in features that were never
released. It is now grouped by what people will notice: Meet Encly, Undo
and Redo, version history, the custom order and how to turn it on, custom
reminder repeats and how notes open, then search and the simpler toolbars,
and finally the fixes to the editor, the note list, sync and reminders.
Each entry is shorter and leads with a bold phrase like the earlier ones.

Refs: #167
Meet Encly and the move to it, Undo and Redo above the keyboard, version
history, a custom note order, custom reminder repeats, notes that reopen
where they were left, better search, simpler toolbars, and a steadier
editor and note list.
@pasichDev
pasichDev marked this pull request as ready for review October 5, 2026 17:09
Since images insert without flicker, the attachment cleanup keeps files
changed in the last ten minutes, so an image saved a moment before its
block reaches the note is not deleted. The instrumented test still made
a brand-new orphan file and expected it gone, and failed on CI. It now
ages the file past the grace period first, and a second test checks
that a fresh orphan file is kept.

Refs: #172
@pasichDev
pasichDev merged commit ab676a5 into master Oct 5, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment