Skip to content

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

Draft
pasichDev wants to merge 18 commits into
masterfrom
feat/meet-encly
Draft

pasichDev wants to merge 18 commits into
masterfrom
feat/meet-encly

Conversation

@pasichDev

@pasichDev pasichDev commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

⏳ Draft: device QA found bugs in the new features; their fixes land on this branch next.

Merge order: Encly 2.1.0 (pasichDev/Encly#56, tag v2.1.0, submitted to Google Play) must be live on every channel before this ships. Otherwise "Move my notes to Encly" points at an Encly that cannot receive.

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, fixes in progress.

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 19.85% -12.03% 🍏
Files changed 33.36% 🍏

File Coverage
ReminderRepeat.java 100% 🍏
CustomOrder.java 100% 🍏
NoteVersionEntity.java 100% 🍏
NoteListTransition.java 100% 🍏
HandoffResult.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% 🍏
FormatListTool.java 97.22% -2.78% 🍏
PositionRestorer.java 96.46% -3.54% 🍏
KeyboardBarState.java 96.43% -3.57% 🍏
ExtendedViewStateJson.java 96.17% -3.83% 🍏
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 43.3% -9.2% 🍏
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% -52.61% 🍏
BootReceiver.java 0% -74.47% 🍏
ReminderReceiver.java 0% -59.71% 🍏
UpdateChecker.java 0% -17.86% 🍏
MeetEnclyDialog.java 0% 🍏
NoteVersionSheet.java 0% 🍏
EditHistoryRows.java 0% 🍏
MoreNoteDialog.java 0% -14.67% 🍏
CustomRepeatDialog.java 0% 🍏
ReminderPickerBottomSheet.java 0% -29.95% 🍏
EditorJSInterface.java 0% -67.36% 🍏
AppDataManager.java 0% -3.96% 🍏
NotesItemAnimator.java 0% 🍏
SwipeToListNotesCallback.java 0% -50% 🍏
NoteBindingAdapters.java 0% -4.91% 🍏
SelectionController.java 0% -17.86% 🍏
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% -58.97% 🍏
RoomSyncStore.java 0% -8.01% 🍏
EditorKeyboardBar.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% -3.84% 🍏
AppUpdateController.java 0% -54.79% 🍏
ViewFader.java 0% 🍏
MainRenderListsController.java 0% -88.76% 🍏
ViewOptionsDialog.java 0% 🍏
RepeatRuleFormatter.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% -50.1% 🍏
ApplicationModule.java 0% 🍏
SearchNotesAdapter.java 0% -61.58% 🍏
NotePayloads.java 0% 🍏
DatabaseConstants.java 0% 🍏
NoteAdapter.java 0% -72.82% 🍏

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment