Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .zennotes-commit
Original file line number Diff line number Diff line change
@@ -1 +1 @@
f156bb1398b82f7f590d669fce5371783500f061
3301a29d6564c13df4d85a5acb59789c5fc7e200
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ android {
applicationId "md.zennotes"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 17
versionName "1.1.15"
versionCode 18
versionName "1.1.16"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "zennotes-android",
"private": true,
"version": "1.1.15",
"version": "1.1.16",
"type": "module",
"description": "ZenNotes for Android — Capacitor shell over the ZenNotes app core",
"homepage": "https://zennotes.org",
Expand Down
16 changes: 14 additions & 2 deletions src/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Preferences } from '@capacitor/preferences'
import { GESTURES_KEY, HIDE_STATUS_BAR_KEY, LAYOUT_MODE_KEY } from './viewport'
import { GESTURES_KEY, HIDE_STATUS_BAR_KEY, LAYOUT_MODE_KEY, START_SCREEN_KEY } from './viewport'

const WEB_PREFERENCES_KEY = 'zen:prefs:v2'
const NATIVE_PREFERENCES_KEY = 'zn-app-preferences-v2'
Expand All @@ -10,11 +10,15 @@ const NATIVE_LAYOUT_MODE_KEY = 'zn-layout-mode'
const NATIVE_HIDE_STATUS_BAR_KEY = 'zn-hide-status-bar'
// The swipe-gesture assignments (#24) ride along too.
const NATIVE_GESTURES_KEY = 'zn-gestures'
// And the Start screen choice (start-screen.ts): read at cold launch, so it
// must be back in localStorage before main.tsx evaluates.
const NATIVE_START_SCREEN_KEY = 'zn-start-screen'
const MIRRORED_KEYS: Record<string, string> = {
[WEB_PREFERENCES_KEY]: NATIVE_PREFERENCES_KEY,
[LAYOUT_MODE_KEY]: NATIVE_LAYOUT_MODE_KEY,
[HIDE_STATUS_BAR_KEY]: NATIVE_HIDE_STATUS_BAR_KEY,
[GESTURES_KEY]: NATIVE_GESTURES_KEY
[GESTURES_KEY]: NATIVE_GESTURES_KEY,
[START_SCREEN_KEY]: NATIVE_START_SCREEN_KEY
}

let persistenceQueue = Promise.resolve()
Expand Down Expand Up @@ -84,6 +88,14 @@ async function restoreNativePreferences(): Promise<void> {
} else if (webGestures) {
await Preferences.set({ key: NATIVE_GESTURES_KEY, value: webGestures })
}

const nativeStartScreen = await Preferences.get({ key: NATIVE_START_SCREEN_KEY })
const webStartScreen = localStorage.getItem(START_SCREEN_KEY)
if (nativeStartScreen.value) {
localStorage.setItem(START_SCREEN_KEY, nativeStartScreen.value)
} else if (webStartScreen) {
await Preferences.set({ key: NATIVE_START_SCREEN_KEY, value: webStartScreen })
}
} catch {
// Continue with WebView storage when native preferences are unavailable.
}
Expand Down
2 changes: 1 addition & 1 deletion src/bridge/mobile-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ import {
import { folderForRelativePath, posixNormalize, sanitizeNoteTitle } from './vault-core'
import { isPhoneViewport } from '../viewport'

let appVersion = '1.1.15'
let appVersion = '1.1.16'

export async function loadNativeAppVersion(): Promise<string> {
try {
Expand Down
7 changes: 7 additions & 0 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import { configureMobileCloudAuth } from './bridge/mobile-cloud-auth'
import { maybeRunFirstRunOnboarding } from './ui-mobile/Onboarding'
import { mountMobileShell } from './ui-mobile/MobileShell'
import { installHomeGuard } from './ui-mobile/nav'
import { refreshVault } from './ui-mobile/refresh'
import { isPhoneViewport, watchPhoneClass } from './viewport'
import './ui-mobile/mobile.css'
Expand Down Expand Up @@ -74,6 +75,12 @@ async function boot(): Promise<void> {
const appVersion = await loadNativeAppVersion()
await configureMobileCloudAuth(appVersion)
installMobileBridge()
// FIRST store subscriber, ahead of everything React mounts: the guard
// undoes app-core's null-active-tab fallback inside the same notification
// pass, and zustand notifies in subscription order — installed any later,
// earlier subscribers (the drawer's auto-close on selection, for one) act
// on the transient tab before the guard puts Home back.
installHomeGuard()
wireKeyboard()
wireForegroundRescan()

Expand Down
58 changes: 54 additions & 4 deletions src/ui-mobile/EditorToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,14 @@ import { indentLess, indentMore, redo, undo } from '@codemirror/commands'
import { openSearchPanel } from '@codemirror/search'
import { EditorSelection } from '@codemirror/state'
import { useStore } from '@zennotes/app-core/store'
import { setBlockType, toggleWrap, wrapLink } from '@zennotes/app-core/lib/cm-format'
import {
setBlockType,
toggleWrap,
wrapLink,
type BlockType
} from '@zennotes/app-core/lib/cm-format'
import { promptAttachFiles } from './attach'
import { revealCaretAboveKeyboardSoon } from './editor-keyboard-scroll'

function view(): EditorView | null {
return useStore.getState().editorViewRef
Expand All @@ -39,13 +45,51 @@ function insertSnippet(v: EditorView, text: string, caretOffset: number): void {
})
}

/**
* Markers app-core's blockPrefix would put on these block types. app-core's
* setBlockType converts existing lines and deliberately skips blank ones, so
* on a fresh line Bullet / Checkbox / Heading did nothing until something was
* typed (Adib, iPhone device testing 2026-09-08; same code here). Desktop
* users just type the marker; on the phone the button IS the way to start a
* list.
*/
const BLANK_LINE_MARKERS: Partial<Record<BlockType, string>> = {
bullet: '- ',
todo: '- [ ] ',
h1: '# ',
h2: '## ',
h3: '### '
}

/**
* setBlockType, plus the blank-line case it skips: a collapsed cursor on an
* empty (or whitespace-only) line gets the marker inserted after the existing
* indentation, caret after the marker. Selections and non-blank lines go
* through setBlockType unchanged.
*/
function applyBlockType(v: EditorView, type: BlockType): void {
const { from, to } = v.state.selection.main
const line = v.state.doc.lineAt(from)
const marker = BLANK_LINE_MARKERS[type]
if (marker !== undefined && from === to && line.text.trim() === '') {
// line.text is whitespace-only here, so it doubles as the indent.
const insert = line.text + marker
v.dispatch({
changes: { from: line.from, to: line.to, insert },
selection: EditorSelection.cursor(line.from + insert.length)
})
return
}
setBlockType(v, type)
}

/** Cycle the current line's heading level: none → # → ## → ### → none. */
function cycleHeading(v: EditorView): void {
const line = v.state.doc.lineAt(v.state.selection.main.from)
const m = line.text.match(/^(#{1,6})\s/)
const level = m ? m[1]!.length : 0
const next = level >= 3 ? 'paragraph' : (['h1', 'h2', 'h3'] as const)[level]!
setBlockType(v, next)
applyBlockType(v, next)
}

interface ToolButton {
Expand Down Expand Up @@ -95,13 +139,13 @@ const BUTTONS: ToolButton[] = [
key: 'todo',
label: 'Checkbox',
d: 'M9 11l3 3L22 4M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11',
run: () => withView((v) => setBlockType(v, 'todo'))
run: () => withView((v) => applyBlockType(v, 'todo'))
},
{
key: 'bullet',
label: 'Bullet list',
d: 'M8 6h13M8 12h13M8 18h13M3.5 6h.01M3.5 12h.01M3.5 18h.01',
run: () => withView((v) => setBlockType(v, 'bullet'))
run: () => withView((v) => applyBlockType(v, 'bullet'))
},
{
key: 'heading',
Expand Down Expand Up @@ -223,6 +267,12 @@ export function MobileEditorToolbar(): React.JSX.Element | null {
return () => window.clearTimeout(t)
}, [kbOpen, editing])

// The toolbar overlays the bottom of the editor: once it's in the DOM, make
// sure the caret isn't under it (editor-keyboard-scroll.ts measures it).
useEffect(() => {
if (visible) revealCaretAboveKeyboardSoon()
}, [visible])

if (!visible) return null

return (
Expand Down
137 changes: 6 additions & 131 deletions src/ui-mobile/MobileDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { useStore } from '@zennotes/app-core/store'
import type { NoteSortOrder } from '@zennotes/app-core/store'
import { confirmApp } from '@zennotes/app-core/lib/confirm-requests'
import { promptApp } from '@zennotes/app-core/lib/prompt-requests'
import { buildMoveNotePrompt, parseMoveNoteTarget } from '@zennotes/app-core/lib/move-note'
import { notePathWithinFolder } from '@zennotes/app-core/lib/vault-layout'
import { resolveFolderPath } from '@zennotes/shared-domain/system-folder-paths'
import {
Expand All @@ -26,6 +25,7 @@ import { openMobileSheet } from './sheet-state'
import { goHome } from './nav'
import { dirOf, noteComparator, pinnedFirst } from './note-order'
import { usePins, toggleNotePin, toggleFolderPin } from './pins'
import { archiveNote, openNoteMenu, trashNote } from './note-actions'
import { refreshVault } from './refresh'
import { SwipeRow } from './SwipeRow'
import { getStoragePref } from '../bridge/icloud'
Expand Down Expand Up @@ -954,11 +954,11 @@ function MobileDrawerBody(props: {
const lp = useLongPress()
const [sortOpen, setSortOpen] = useState(false)
// Long-pressing a row opens its action sheet — the phone's right-click
// (Discord folder feedback). Notes mirror the ••• sheet's actions; folders
// get Rename/Delete. Prompts overlay the open drawer (Modal layers above
// (Discord folder feedback). Notes open the shell-wide note sheet
// (note-actions.tsx, shared with app-core's lists); folders get
// Rename/Delete here. Prompts overlay the open drawer (Modal layers above
// z-49), so the drawer stays put and its list refreshes in place via the
// vault change events.
const [noteMenu, setNoteMenu] = useState<{ path: string; title: string } | null>(null)
const [folderMenu, setFolderMenu] = useState<{ subpath: string; name: string } | null>(null)

const pinNote = (notePath: string): void => {
Expand Down Expand Up @@ -1039,46 +1039,6 @@ function MobileDrawerBody(props: {
}
}, [refreshing])

const moveNoteFromDrawer = (notePath: string): void => {
setNoteMenu(null)
void (async () => {
const st = s()
const meta = st.notes.find((n) => n.path === notePath)
if (!meta) return
const target = await promptApp(buildMoveNotePrompt(meta, st.folders))
if (!target) return
const dest = parseMoveNoteTarget(target)
await s().moveNote(meta.path, dest.folder, dest.subpath)
})()
}

const renameNoteFromDrawer = (notePath: string, title: string): void => {
setNoteMenu(null)
void (async () => {
const next = await promptApp({
title: 'Rename note',
initialValue: title,
okLabel: 'Rename',
validate: (v: string) => (/[\\/]/.test(v) ? 'Title cannot contain / or \\' : null)
})
if (!next || next === title) return
await s().renameNote(notePath, next)
})()
}

const archiveNoteFromDrawer = (notePath: string): void => {
setNoteMenu(null)
void (async () => {
if (!(await s().confirmArchiveNotes([notePath]))) return
await window.zen.archiveNote(notePath)
})()
}

const copyWikilinkFromDrawer = (title: string): void => {
setNoteMenu(null)
void navigator.clipboard.writeText(`[[${title}]]`).catch(() => {})
}

const renameFolderFromDrawer = (subpath: string, name: string): void => {
setFolderMenu(null)
void (async () => {
Expand Down Expand Up @@ -1114,18 +1074,6 @@ function MobileDrawerBody(props: {
})()
}

const trashNote = (notePath: string, title: string): void => {
void (async () => {
const ok = await confirmApp({
title: `Delete "${title}"?`,
description: 'It will move to the trash.',
confirmLabel: 'Delete',
danger: true
})
if (ok) await window.zen.moveToTrash(notePath)
})()
}

const deleteDatabase = (subpath: string, title: string): void => {
void (async () => {
const ok = await confirmApp({
Expand Down Expand Up @@ -1331,7 +1279,7 @@ function MobileDrawerBody(props: {
{
label: 'Archive',
icon: <Icon d={D.archive} />,
onAction: () => archiveNoteFromDrawer(n.path)
onAction: () => archiveNote(n.path)
},
{
label: 'Delete',
Expand All @@ -1346,7 +1294,7 @@ function MobileDrawerBody(props: {
<button
type="button"
onClick={() => go(() => s().selectNote(n.path))}
{...lp(() => setNoteMenu({ path: n.path, title: n.title }))}
{...lp(() => openNoteMenu({ path: n.path, title: n.title, kind: 'note' }))}
>
<Icon d={D.note} />
<span className="zn-truncate">{n.title}</span>
Expand All @@ -1373,79 +1321,6 @@ function MobileDrawerBody(props: {
</div>
</div>

{noteMenu && (
<>
<div
className="zn-mobile-sheet-backdrop"
onClick={() => setNoteMenu(null)}
role="presentation"
/>
<div className="zn-mobile-sheet" role="menu" aria-label="Note actions">
<div className="zn-mobile-sheet-title zn-truncate">{noteMenu.title}</div>
<div className="zn-mobile-sheet-scroll">
<div className="zn-mobile-sheet-group">
<button
type="button"
className="zn-mobile-sheet-row"
onClick={() => {
const p = noteMenu.path
setNoteMenu(null)
pinNote(p)
}}
>
<Icon d={D.pin} />
{pinnedNotes.includes(noteMenu.path) ? 'Unpin' : 'Pin'}
</button>
<button
type="button"
className="zn-mobile-sheet-row"
onClick={() => renameNoteFromDrawer(noteMenu.path, noteMenu.title)}
>
<Icon d={D.rename} />
Rename
</button>
<button
type="button"
className="zn-mobile-sheet-row"
onClick={() => moveNoteFromDrawer(noteMenu.path)}
>
<Icon d={D.move} />
Move to…
</button>
<button
type="button"
className="zn-mobile-sheet-row"
onClick={() => copyWikilinkFromDrawer(noteMenu.title)}
>
<Icon d={D.link} />
Copy wikilink
</button>
<button
type="button"
className="zn-mobile-sheet-row"
onClick={() => archiveNoteFromDrawer(noteMenu.path)}
>
<Icon d={D.archive} />
Archive
</button>
<button
type="button"
className="zn-mobile-sheet-row zn-danger"
onClick={() => {
const { path: p, title } = noteMenu
setNoteMenu(null)
trashNote(p, title)
}}
>
<Icon d={D.trash} />
Delete
</button>
</div>
</div>
</div>
</>
)}

{folderMenu && (
<>
<div
Expand Down
Loading