Skip to content

Commit f46cf33

Browse files
authored
Merge pull request #56 from ZenNotes/release/1.1.16
Release 1.1.16: phone editing, tuned by hand (app core 2.45.0)
2 parents de22fc2 + d04376a commit f46cf33

19 files changed

Lines changed: 1342 additions & 146 deletions

.zennotes-commit

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
f156bb1398b82f7f590d669fce5371783500f061
1+
3301a29d6564c13df4d85a5acb59789c5fc7e200

android/app/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ android {
1515
applicationId "md.zennotes"
1616
minSdkVersion rootProject.ext.minSdkVersion
1717
targetSdkVersion rootProject.ext.targetSdkVersion
18-
versionCode 17
19-
versionName "1.1.15"
18+
versionCode 18
19+
versionName "1.1.16"
2020
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
2121
aaptOptions {
2222
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "zennotes-android",
33
"private": true,
4-
"version": "1.1.15",
4+
"version": "1.1.16",
55
"type": "module",
66
"description": "ZenNotes for Android — Capacitor shell over the ZenNotes app core",
77
"homepage": "https://zennotes.org",

src/bootstrap.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Preferences } from '@capacitor/preferences'
2-
import { GESTURES_KEY, HIDE_STATUS_BAR_KEY, LAYOUT_MODE_KEY } from './viewport'
2+
import { GESTURES_KEY, HIDE_STATUS_BAR_KEY, LAYOUT_MODE_KEY, START_SCREEN_KEY } from './viewport'
33

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

2024
let persistenceQueue = Promise.resolve()
@@ -84,6 +88,14 @@ async function restoreNativePreferences(): Promise<void> {
8488
} else if (webGestures) {
8589
await Preferences.set({ key: NATIVE_GESTURES_KEY, value: webGestures })
8690
}
91+
92+
const nativeStartScreen = await Preferences.get({ key: NATIVE_START_SCREEN_KEY })
93+
const webStartScreen = localStorage.getItem(START_SCREEN_KEY)
94+
if (nativeStartScreen.value) {
95+
localStorage.setItem(START_SCREEN_KEY, nativeStartScreen.value)
96+
} else if (webStartScreen) {
97+
await Preferences.set({ key: NATIVE_START_SCREEN_KEY, value: webStartScreen })
98+
}
8799
} catch {
88100
// Continue with WebView storage when native preferences are unavailable.
89101
}

src/bridge/mobile-bridge.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ import {
130130
import { folderForRelativePath, posixNormalize, sanitizeNoteTitle } from './vault-core'
131131
import { isPhoneViewport } from '../viewport'
132132

133-
let appVersion = '1.1.15'
133+
let appVersion = '1.1.16'
134134

135135
export async function loadNativeAppVersion(): Promise<string> {
136136
try {

src/main.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
import { configureMobileCloudAuth } from './bridge/mobile-cloud-auth'
2121
import { maybeRunFirstRunOnboarding } from './ui-mobile/Onboarding'
2222
import { mountMobileShell } from './ui-mobile/MobileShell'
23+
import { installHomeGuard } from './ui-mobile/nav'
2324
import { refreshVault } from './ui-mobile/refresh'
2425
import { isPhoneViewport, watchPhoneClass } from './viewport'
2526
import './ui-mobile/mobile.css'
@@ -74,6 +75,12 @@ async function boot(): Promise<void> {
7475
const appVersion = await loadNativeAppVersion()
7576
await configureMobileCloudAuth(appVersion)
7677
installMobileBridge()
78+
// FIRST store subscriber, ahead of everything React mounts: the guard
79+
// undoes app-core's null-active-tab fallback inside the same notification
80+
// pass, and zustand notifies in subscription order — installed any later,
81+
// earlier subscribers (the drawer's auto-close on selection, for one) act
82+
// on the transient tab before the guard puts Home back.
83+
installHomeGuard()
7784
wireKeyboard()
7885
wireForegroundRescan()
7986

src/ui-mobile/EditorToolbar.tsx

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,14 @@ import { indentLess, indentMore, redo, undo } from '@codemirror/commands'
1616
import { openSearchPanel } from '@codemirror/search'
1717
import { EditorSelection } from '@codemirror/state'
1818
import { useStore } from '@zennotes/app-core/store'
19-
import { setBlockType, toggleWrap, wrapLink } from '@zennotes/app-core/lib/cm-format'
19+
import {
20+
setBlockType,
21+
toggleWrap,
22+
wrapLink,
23+
type BlockType
24+
} from '@zennotes/app-core/lib/cm-format'
2025
import { promptAttachFiles } from './attach'
26+
import { revealCaretAboveKeyboardSoon } from './editor-keyboard-scroll'
2127

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

48+
/**
49+
* Markers app-core's blockPrefix would put on these block types. app-core's
50+
* setBlockType converts existing lines and deliberately skips blank ones, so
51+
* on a fresh line Bullet / Checkbox / Heading did nothing until something was
52+
* typed (Adib, iPhone device testing 2026-09-08; same code here). Desktop
53+
* users just type the marker; on the phone the button IS the way to start a
54+
* list.
55+
*/
56+
const BLANK_LINE_MARKERS: Partial<Record<BlockType, string>> = {
57+
bullet: '- ',
58+
todo: '- [ ] ',
59+
h1: '# ',
60+
h2: '## ',
61+
h3: '### '
62+
}
63+
64+
/**
65+
* setBlockType, plus the blank-line case it skips: a collapsed cursor on an
66+
* empty (or whitespace-only) line gets the marker inserted after the existing
67+
* indentation, caret after the marker. Selections and non-blank lines go
68+
* through setBlockType unchanged.
69+
*/
70+
function applyBlockType(v: EditorView, type: BlockType): void {
71+
const { from, to } = v.state.selection.main
72+
const line = v.state.doc.lineAt(from)
73+
const marker = BLANK_LINE_MARKERS[type]
74+
if (marker !== undefined && from === to && line.text.trim() === '') {
75+
// line.text is whitespace-only here, so it doubles as the indent.
76+
const insert = line.text + marker
77+
v.dispatch({
78+
changes: { from: line.from, to: line.to, insert },
79+
selection: EditorSelection.cursor(line.from + insert.length)
80+
})
81+
return
82+
}
83+
setBlockType(v, type)
84+
}
85+
4286
/** Cycle the current line's heading level: none → # → ## → ### → none. */
4387
function cycleHeading(v: EditorView): void {
4488
const line = v.state.doc.lineAt(v.state.selection.main.from)
4589
const m = line.text.match(/^(#{1,6})\s/)
4690
const level = m ? m[1]!.length : 0
4791
const next = level >= 3 ? 'paragraph' : (['h1', 'h2', 'h3'] as const)[level]!
48-
setBlockType(v, next)
92+
applyBlockType(v, next)
4993
}
5094

5195
interface ToolButton {
@@ -95,13 +139,13 @@ const BUTTONS: ToolButton[] = [
95139
key: 'todo',
96140
label: 'Checkbox',
97141
d: 'M9 11l3 3L22 4M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11',
98-
run: () => withView((v) => setBlockType(v, 'todo'))
142+
run: () => withView((v) => applyBlockType(v, 'todo'))
99143
},
100144
{
101145
key: 'bullet',
102146
label: 'Bullet list',
103147
d: 'M8 6h13M8 12h13M8 18h13M3.5 6h.01M3.5 12h.01M3.5 18h.01',
104-
run: () => withView((v) => setBlockType(v, 'bullet'))
148+
run: () => withView((v) => applyBlockType(v, 'bullet'))
105149
},
106150
{
107151
key: 'heading',
@@ -223,6 +267,12 @@ export function MobileEditorToolbar(): React.JSX.Element | null {
223267
return () => window.clearTimeout(t)
224268
}, [kbOpen, editing])
225269

270+
// The toolbar overlays the bottom of the editor: once it's in the DOM, make
271+
// sure the caret isn't under it (editor-keyboard-scroll.ts measures it).
272+
useEffect(() => {
273+
if (visible) revealCaretAboveKeyboardSoon()
274+
}, [visible])
275+
226276
if (!visible) return null
227277

228278
return (

src/ui-mobile/MobileDrawer.tsx

Lines changed: 6 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { useStore } from '@zennotes/app-core/store'
1111
import type { NoteSortOrder } from '@zennotes/app-core/store'
1212
import { confirmApp } from '@zennotes/app-core/lib/confirm-requests'
1313
import { promptApp } from '@zennotes/app-core/lib/prompt-requests'
14-
import { buildMoveNotePrompt, parseMoveNoteTarget } from '@zennotes/app-core/lib/move-note'
1514
import { notePathWithinFolder } from '@zennotes/app-core/lib/vault-layout'
1615
import { resolveFolderPath } from '@zennotes/shared-domain/system-folder-paths'
1716
import {
@@ -26,6 +25,7 @@ import { openMobileSheet } from './sheet-state'
2625
import { goHome } from './nav'
2726
import { dirOf, noteComparator, pinnedFirst } from './note-order'
2827
import { usePins, toggleNotePin, toggleFolderPin } from './pins'
28+
import { archiveNote, openNoteMenu, trashNote } from './note-actions'
2929
import { refreshVault } from './refresh'
3030
import { SwipeRow } from './SwipeRow'
3131
import { getStoragePref } from '../bridge/icloud'
@@ -954,11 +954,11 @@ function MobileDrawerBody(props: {
954954
const lp = useLongPress()
955955
const [sortOpen, setSortOpen] = useState(false)
956956
// Long-pressing a row opens its action sheet — the phone's right-click
957-
// (Discord folder feedback). Notes mirror the ••• sheet's actions; folders
958-
// get Rename/Delete. Prompts overlay the open drawer (Modal layers above
957+
// (Discord folder feedback). Notes open the shell-wide note sheet
958+
// (note-actions.tsx, shared with app-core's lists); folders get
959+
// Rename/Delete here. Prompts overlay the open drawer (Modal layers above
959960
// z-49), so the drawer stays put and its list refreshes in place via the
960961
// vault change events.
961-
const [noteMenu, setNoteMenu] = useState<{ path: string; title: string } | null>(null)
962962
const [folderMenu, setFolderMenu] = useState<{ subpath: string; name: string } | null>(null)
963963

964964
const pinNote = (notePath: string): void => {
@@ -1039,46 +1039,6 @@ function MobileDrawerBody(props: {
10391039
}
10401040
}, [refreshing])
10411041

1042-
const moveNoteFromDrawer = (notePath: string): void => {
1043-
setNoteMenu(null)
1044-
void (async () => {
1045-
const st = s()
1046-
const meta = st.notes.find((n) => n.path === notePath)
1047-
if (!meta) return
1048-
const target = await promptApp(buildMoveNotePrompt(meta, st.folders))
1049-
if (!target) return
1050-
const dest = parseMoveNoteTarget(target)
1051-
await s().moveNote(meta.path, dest.folder, dest.subpath)
1052-
})()
1053-
}
1054-
1055-
const renameNoteFromDrawer = (notePath: string, title: string): void => {
1056-
setNoteMenu(null)
1057-
void (async () => {
1058-
const next = await promptApp({
1059-
title: 'Rename note',
1060-
initialValue: title,
1061-
okLabel: 'Rename',
1062-
validate: (v: string) => (/[\\/]/.test(v) ? 'Title cannot contain / or \\' : null)
1063-
})
1064-
if (!next || next === title) return
1065-
await s().renameNote(notePath, next)
1066-
})()
1067-
}
1068-
1069-
const archiveNoteFromDrawer = (notePath: string): void => {
1070-
setNoteMenu(null)
1071-
void (async () => {
1072-
if (!(await s().confirmArchiveNotes([notePath]))) return
1073-
await window.zen.archiveNote(notePath)
1074-
})()
1075-
}
1076-
1077-
const copyWikilinkFromDrawer = (title: string): void => {
1078-
setNoteMenu(null)
1079-
void navigator.clipboard.writeText(`[[${title}]]`).catch(() => {})
1080-
}
1081-
10821042
const renameFolderFromDrawer = (subpath: string, name: string): void => {
10831043
setFolderMenu(null)
10841044
void (async () => {
@@ -1114,18 +1074,6 @@ function MobileDrawerBody(props: {
11141074
})()
11151075
}
11161076

1117-
const trashNote = (notePath: string, title: string): void => {
1118-
void (async () => {
1119-
const ok = await confirmApp({
1120-
title: `Delete "${title}"?`,
1121-
description: 'It will move to the trash.',
1122-
confirmLabel: 'Delete',
1123-
danger: true
1124-
})
1125-
if (ok) await window.zen.moveToTrash(notePath)
1126-
})()
1127-
}
1128-
11291077
const deleteDatabase = (subpath: string, title: string): void => {
11301078
void (async () => {
11311079
const ok = await confirmApp({
@@ -1331,7 +1279,7 @@ function MobileDrawerBody(props: {
13311279
{
13321280
label: 'Archive',
13331281
icon: <Icon d={D.archive} />,
1334-
onAction: () => archiveNoteFromDrawer(n.path)
1282+
onAction: () => archiveNote(n.path)
13351283
},
13361284
{
13371285
label: 'Delete',
@@ -1346,7 +1294,7 @@ function MobileDrawerBody(props: {
13461294
<button
13471295
type="button"
13481296
onClick={() => go(() => s().selectNote(n.path))}
1349-
{...lp(() => setNoteMenu({ path: n.path, title: n.title }))}
1297+
{...lp(() => openNoteMenu({ path: n.path, title: n.title, kind: 'note' }))}
13501298
>
13511299
<Icon d={D.note} />
13521300
<span className="zn-truncate">{n.title}</span>
@@ -1373,79 +1321,6 @@ function MobileDrawerBody(props: {
13731321
</div>
13741322
</div>
13751323

1376-
{noteMenu && (
1377-
<>
1378-
<div
1379-
className="zn-mobile-sheet-backdrop"
1380-
onClick={() => setNoteMenu(null)}
1381-
role="presentation"
1382-
/>
1383-
<div className="zn-mobile-sheet" role="menu" aria-label="Note actions">
1384-
<div className="zn-mobile-sheet-title zn-truncate">{noteMenu.title}</div>
1385-
<div className="zn-mobile-sheet-scroll">
1386-
<div className="zn-mobile-sheet-group">
1387-
<button
1388-
type="button"
1389-
className="zn-mobile-sheet-row"
1390-
onClick={() => {
1391-
const p = noteMenu.path
1392-
setNoteMenu(null)
1393-
pinNote(p)
1394-
}}
1395-
>
1396-
<Icon d={D.pin} />
1397-
{pinnedNotes.includes(noteMenu.path) ? 'Unpin' : 'Pin'}
1398-
</button>
1399-
<button
1400-
type="button"
1401-
className="zn-mobile-sheet-row"
1402-
onClick={() => renameNoteFromDrawer(noteMenu.path, noteMenu.title)}
1403-
>
1404-
<Icon d={D.rename} />
1405-
Rename
1406-
</button>
1407-
<button
1408-
type="button"
1409-
className="zn-mobile-sheet-row"
1410-
onClick={() => moveNoteFromDrawer(noteMenu.path)}
1411-
>
1412-
<Icon d={D.move} />
1413-
Move to…
1414-
</button>
1415-
<button
1416-
type="button"
1417-
className="zn-mobile-sheet-row"
1418-
onClick={() => copyWikilinkFromDrawer(noteMenu.title)}
1419-
>
1420-
<Icon d={D.link} />
1421-
Copy wikilink
1422-
</button>
1423-
<button
1424-
type="button"
1425-
className="zn-mobile-sheet-row"
1426-
onClick={() => archiveNoteFromDrawer(noteMenu.path)}
1427-
>
1428-
<Icon d={D.archive} />
1429-
Archive
1430-
</button>
1431-
<button
1432-
type="button"
1433-
className="zn-mobile-sheet-row zn-danger"
1434-
onClick={() => {
1435-
const { path: p, title } = noteMenu
1436-
setNoteMenu(null)
1437-
trashNote(p, title)
1438-
}}
1439-
>
1440-
<Icon d={D.trash} />
1441-
Delete
1442-
</button>
1443-
</div>
1444-
</div>
1445-
</div>
1446-
</>
1447-
)}
1448-
14491324
{folderMenu && (
14501325
<>
14511326
<div

0 commit comments

Comments
 (0)