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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ and `@zennotes/shared-domain` packages. The exact archives are vendored under
`vendor/zennotes/` with their source identity and checksums (`manifest.json`),
and `package-lock.json` pins the complete install. No source checkout is used.
The vendored set is the core release
[core-2.80.0-core.hc5a4589d040cd0da](https://github.com/ZenNotes/zennotes/releases/tag/core-2.80.0-core.hc5a4589d040cd0da)
(desktop commit `31520814`, clean tree). Run `npm run
[core-2.81.0-core.h4e6a38a3361b4748](https://github.com/ZenNotes/zennotes/releases/tag/core-2.81.0-core.h4e6a38a3361b4748)
(desktop commit `bfab9fdc`, clean tree). Run `npm run
boundaries:check` to verify archives, installed versions, singleton
editor/React peers, and imports; it refuses an archive built from a dirty
upstream tree unless `ZEN_ALLOW_DIRTY_CORE=1` is set for a local try-out.
Expand Down
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 54
versionName "1.1.51"
versionCode 55
versionName "1.1.52"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
Expand Down
34 changes: 17 additions & 17 deletions package-lock.json

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

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "zennotes-android",
"private": true,
"version": "1.1.51",
"version": "1.1.52",
"type": "module",
"description": "ZenNotes for Android — Capacitor shell over the ZenNotes app core",
"homepage": "https://zennotes.org",
Expand Down Expand Up @@ -43,9 +43,9 @@
"@lezer/highlight": "^1.2.5",
"@replit/codemirror-vim": "^6.3.0",
"@xyflow/react": "^12.12.0",
"@zennotes/app-core": "file:vendor/zennotes/zennotes-app-core-2.80.0-core.hc5a4589d040cd0da.tgz",
"@zennotes/bridge-contract": "file:vendor/zennotes/zennotes-bridge-contract-2.80.0-boundaries.hc07ef9e2bef31a64.tgz",
"@zennotes/shared-domain": "file:vendor/zennotes/zennotes-shared-domain-2.80.0-boundaries.hc07ef9e2bef31a64.tgz",
"@zennotes/app-core": "file:vendor/zennotes/zennotes-app-core-2.81.0-core.h4e6a38a3361b4748.tgz",
"@zennotes/bridge-contract": "file:vendor/zennotes/zennotes-bridge-contract-2.81.0-boundaries.h697bb3192aa5970b.tgz",
"@zennotes/shared-domain": "file:vendor/zennotes/zennotes-shared-domain-2.81.0-boundaries.h697bb3192aa5970b.tgz",
"codemirror": "^6.0.1",
"dompurify": "^3.4.17",
"function-plot": "^1.26.0",
Expand Down
42 changes: 42 additions & 0 deletions src/bridge/database-open.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { describe, it } from 'node:test'
import { missingDatabaseAsNull } from './database-open.ts'

const doc = { relPath: 'inbox/Board.base/data.csv' } as never

describe('Opening a database whose folder is gone', () => {
it('answers null for a missing database, so core forgets it and closes its tab', async () => {
const open = missingDatabaseAsNull(async (rel) => {
throw new Error(`Database not found: ${rel}`)
})
assert.equal(await open('inbox/Projects/Untitled Kanban.base/data.csv'), null)
})

it('passes a database through, and every other failure still throws', async () => {
assert.equal(await missingDatabaseAsNull(async () => doc)('x'), doc)
const unreadable = missingDatabaseAsNull(async () => {
throw new Error('EACCES: permission denied')
})
await assert.rejects(unreadable('x'), /permission denied/)
})

it('is what the bridge hands core', () => {
const bridge = readFileSync(new URL('./mobile-bridge.ts', import.meta.url), 'utf8')
assert.match(bridge, /openDatabase: openDatabaseOrThrow,/)
assert.match(bridge, /const openDatabase = missingDatabaseAsNull\(openDatabaseOrThrow\)/)
})

it('reads the contract core and the shared ops keep', () => {
const pkg = (path: string): string =>
readFileSync(new URL(`../../node_modules/@zennotes/${path}`, import.meta.url), 'utf8')
// The shared ops throw for an absent data.csv ...
assert.match(pkg('shared-domain/dist/database-ops.js'), /if \(csvText === null\)\n\s+throw new Error\(`Database not found: \$\{rel\}`\);/)
// ... core forgets a database the bridge answers null for, and toasts a
// throw once, recording it so the grid does not ask again (core 2.81.0).
const store = pkg('app-core/dist/store.js')
assert.match(store, /if \(!doc\) \{[\s\S]{0,240}await get\(\)\.forgetDatabase\(csvPath\);/)
assert.match(store, /const message = humanIpcError\(err, 'Could not open database'\);[\s\S]{0,240}databasesLoadError[\s\S]{0,240}\.addToast\(message, 'error'\);/)
assert.match(pkg('bridge-contract/dist/bridge.d.ts'), /openDatabase\(relPath: string\): Promise<DatabaseDoc \| null>;/)
})
})
26 changes: 26 additions & 0 deletions src/bridge/database-open.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { DatabaseDoc } from '@zennotes/bridge-contract/databases'

/**
* A database's tab can outlive its folder: deleted in a file manager, by sync
* from another device, or by another app sharing the vault. The bridge
* contract answers null for a missing database, as desktop's main process
* does, and core then forgets the database and closes its tab.
*
* The shared database ops throw "Database not found" for it instead, and
* core's grid toasted that error and asked again on its next render: one
* relaunch with such a tab open stacked 780 "Database not found" toasts over
* the app. Only that error becomes null; anything else (a file that cannot
* be read, a schema that cannot be parsed) still throws.
*/
export function missingDatabaseAsNull(
open: (relPath: string) => Promise<DatabaseDoc>
): (relPath: string) => Promise<DatabaseDoc | null> {
return async (relPath) => {
try {
return await open(relPath)
} catch (err) {
if (err instanceof Error && err.message.startsWith('Database not found')) return null
throw err
}
}
}
7 changes: 5 additions & 2 deletions src/bridge/mobile-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import type {
VaultTextSearchMatch
} from '@zennotes/shared-domain/ipc'
import { createDatabaseOps } from '@zennotes/shared-domain/database-ops'
import { missingDatabaseAsNull } from './database-open'
import { normalizeVaultDisplayName, resolveVaultName } from '@zennotes/shared-domain/vault-display-name'
import type {
CustomCodeLanguage,
Expand Down Expand Up @@ -157,7 +158,7 @@ import {
import { folderForRelativePath, posixNormalize, sanitizeNoteTitle } from './vault-core'
import { isPhoneViewport } from '../viewport'

let appVersion = '1.1.51'
let appVersion = '1.1.52'

export async function loadNativeAppVersion(): Promise<string> {
try {
Expand Down Expand Up @@ -737,14 +738,16 @@ const dbOps = createDatabaseOps({
})

const {
openDatabase,
openDatabase: openDatabaseOrThrow,
writeDatabaseRows,
writeDatabaseSchema,
createDatabase,
createRecordPage,
renameDatabase,
listDatabases
} = dbOps
// A tab whose database is gone answers null (database-open.ts).
const openDatabase = missingDatabaseAsNull(openDatabaseOrThrow)

// --------------------------------------------------------------------
// Asset URL resolution (WebView-loadable file URLs)
Expand Down
14 changes: 12 additions & 2 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { installHomeGuard } from './ui-mobile/nav'
import { refreshVault } from './ui-mobile/refresh'
import { isPhoneViewport, watchPhoneClass } from './viewport'
import { waitAtBoot } from './boot-wait'
import { KeyboardLift } from './ui-mobile/keyboard-lift'
import './ui-mobile/mobile.css'

function wireKeyboard(): void {
Expand Down Expand Up @@ -58,13 +59,22 @@ function wireKeyboard(): void {
// innerWidth-at-boot check did.
watchPhoneClass(applyResizeMode)
applyResizeMode(isPhoneViewport())
// The tablet lift: only the part of the keyboard the viewport has not
// already given up, since the Android plugin cannot actually turn the
// resize off (keyboard-lift.ts). Phones never read it.
const lift = new KeyboardLift(window.innerWidth, window.innerHeight)
const setLift = (px: number): void => html.style.setProperty('--zn-kb-height', `${px}px`)
window.addEventListener('resize', () => setLift(lift.resized(window.innerWidth, window.innerHeight)))
void Keyboard.addListener('keyboardWillShow', (info) => {
html.classList.add('zn-kb-open')
html.style.setProperty('--zn-kb-height', `${info.keyboardHeight}px`)
setLift(lift.shown(info.keyboardHeight, window.innerHeight))
}).catch(() => {})
void Keyboard.addListener('keyboardDidShow', (info) => {
setLift(lift.shown(info.keyboardHeight, window.innerHeight))
}).catch(() => {})
void Keyboard.addListener('keyboardWillHide', () => {
html.classList.remove('zn-kb-open')
html.style.setProperty('--zn-kb-height', '0px')
setLift(lift.hidden())
}).catch(() => {})
}

Expand Down
37 changes: 36 additions & 1 deletion src/ui-mobile/MobileDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import ReactDOM from 'react-dom/client'
import { getShellSnapshot, useShellSnapshot, setNoteSortOrder, type NoteSortOrder } from '@zennotes/app-core/shell'
import { getBrowseSnapshot, useBrowseSnapshot, getBrowseDirectory, requestCreateBrowseFolder,
requestCreateBrowseNote, requestRenameBrowseFolder, requestRenameBrowseDatabase, requestMoveBrowseDirectory,
requestDeleteBrowseDirectory, openBrowseFolderGallery } from '@zennotes/app-core/browse'
requestDeleteBrowseDirectory, openBrowseFolderGallery, createBrowseKanban } from '@zennotes/app-core/browse'
import { useWorkspaceSnapshot, openLocalVault, pickLocalVault, refreshRemoteProfiles, connectRemoteWorkspace,
connectRemoteProfile, changeRemoteVaultPath, deleteRemoteProfile } from '@zennotes/app-core/workspace'
import { openNote, openAppPage } from '@zennotes/app-core/navigation'
Expand Down Expand Up @@ -86,6 +86,8 @@ const D = {
move: 'M5 8V6a2 2 0 012-2h3l2 2h7a2 2 0 012 2v10a2 2 0 01-2 2H7a2 2 0 01-2-2v-4M2 13h9m0 0l-3-3m3 3l-3 3',
rename: 'M12 20h9M16.5 3.5a2.1 2.1 0 013 3L7 19l-4 1 1-4z',
gallery: 'M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z',
// The New sheet's New Kanban glyph (MobileShell ICONS.tabs).
kanban: 'M4 6h16M4 6v12h16V6M9 6v12',
link: 'M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71',
files:
'M19 3H5a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2V5a2 2 0 00-2-2zM8.5 10a1.5 1.5 0 100-3 1.5 1.5 0 000 3zM21 15l-5-5L5 21',
Expand Down Expand Up @@ -1005,6 +1007,20 @@ function MobileDrawerBody(props: {
}
}, [refreshing])

// System back closes the folder sheet or the sort menu before the drawer
// under it (MobileShell's useSystemBackClose). Neither has an Escape of its
// own, so the cascade's menu step left them open.
useEffect(() => {
const onBack = (e: Event): void => {
if (folderMenu) setFolderMenu(null)
else if (sortOpen) setSortOpen(false)
else return
e.preventDefault()
}
window.addEventListener('zn:android-back', onBack)
return () => window.removeEventListener('zn:android-back', onBack)
}, [folderMenu, sortOpen])

const renameFolderFromDrawer = (subpath: string, _name: string): void => {
const host = folderMenu?.host ?? captureMobileWorkspace()
const rename = folderMenu?.kind === 'database' ? requestRenameBrowseDatabase : requestRenameBrowseFolder
Expand Down Expand Up @@ -1038,6 +1054,15 @@ function MobileDrawerBody(props: {
setFolderMenu(null)
go(() => openBrowseFolderGallery(host, subpath).catch(reportActionError))
}
// The desktop folder menu's New Kanban (core 2.81's createBrowseKanban): a
// board seeded with To do, Doing and Done lands in this folder and opens on
// its Board. The New sheet's row puts one where new databases go instead.
// Core refuses a folder inside a database, which the drawer never lists.
const newKanbanIn = (subpath: string): void => {
const host = folderMenu?.host ?? captureMobileWorkspace()
setFolderMenu(null)
go(() => createBrowseKanban(host, subpath).catch(reportActionError))
}
const deleteFolder = (subpath: string, _name: string): void => {
const host = folderMenu?.host ?? captureMobileWorkspace()
void requestDeleteBrowseDirectory(host, subpath).catch(reportActionError)
Expand Down Expand Up @@ -1327,6 +1352,16 @@ function MobileDrawerBody(props: {
Open as Gallery
</button>
)}
{folderMenu.kind === 'folder' && (
<button
type="button"
className="zn-mobile-sheet-row"
onClick={() => newKanbanIn(folderMenu.subpath)}
>
<Icon d={D.kanban} />
New Kanban
</button>
)}
<button
type="button"
className="zn-mobile-sheet-row"
Expand Down
Loading
Loading