Repository navigation
feat: Meet Encly, and issues #170–#180 (list, search, toolbars, editors, reminders, history, order, undo) - #169
Merged
Merged
Conversation
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
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
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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
note_versions,notes.customPosition).Testing
./gradlew :app:assembleDebug :app:test :app:lintDebug :app:spotlessCheck: pass on the branch head.Not verified:
MigrationTest22→23 (instrumented, not run); RTL (the app hassupportsRtloff).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