Skip to content

Commit 3d454a5

Browse files
authored
Merge pull request #79 from ZenNotes/release/1.1.24
Release 1.1.24: Favorites on Home, gestures on every page, folder-vault moves, and the shared 2.53.0 core
2 parents 7e2474c + bbeb8e9 commit 3d454a5

21 files changed

Lines changed: 390 additions & 121 deletions

‎README.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ and `@zennotes/shared-domain` packages. The exact archives are vendored under
1313
`vendor/zennotes/` with their source identity and checksums (`manifest.json`),
1414
and `package-lock.json` pins the complete install. No source checkout is used.
1515
The vendored set is the published desktop release
16-
[core-2.51.0-core.h49d73b531d346192](https://github.com/ZenNotes/zennotes/releases/tag/core-2.51.0-core.h49d73b531d346192)
17-
(desktop commit `8ff2cb86`, tag v2.51.0, clean tree). Run `npm run
16+
[core-2.53.0-core.h598c8d004c9228a3](https://github.com/ZenNotes/zennotes/releases/tag/core-2.53.0-core.h598c8d004c9228a3)
17+
(desktop commit `3a622639`, tag v2.53.0, clean tree). Run `npm run
1818
boundaries:check` to verify archives, installed versions, singleton
19-
editor/React peers, and imports.
19+
editor/React peers, and imports; it refuses an archive built from a dirty
20+
upstream tree unless `ZEN_ALLOW_DIRTY_CORE=1` is set for a local try-out.
2021

2122
## Architecture
2223

‎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 26
19-
versionName "1.1.23"
18+
versionCode 27
19+
versionName "1.1.24"
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.

‎android/app/src/main/java/md/zennotes/SafFsPlugin.java‎

Lines changed: 112 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -382,40 +382,17 @@ public void rename(PluginCall call) {
382382
int ti = to.lastIndexOf('/');
383383
String fromDir = fi == -1 ? "" : from.substring(0, fi);
384384
String toDir = ti == -1 ? "" : to.substring(0, ti);
385+
String fromName = baseName(from);
386+
String targetName = baseName(to);
385387
if (fromDir.equals(toDir)) {
386-
Uri renamed = DocumentsContract.renameDocument(
387-
resolver(), docUri(tree, src.docId), baseName(to)
388-
);
389-
if (renamed == null) throw new Exception("Rename refused");
388+
renameExactly(tree, src.docId, fromName, targetName);
390389
} else {
391390
Entry fromParent = resolve(tree, fromDir);
392391
String toParentId = ensureParentDirs(tree, to);
393-
Uri moved = DocumentsContract.moveDocument(
394-
resolver(),
395-
docUri(tree, src.docId),
396-
docUri(tree, fromParent.docId),
397-
docUri(tree, toParentId)
398-
);
399-
if (moved == null) throw new Exception("Move refused");
400-
String movedId = DocumentsContract.getDocumentId(moved);
401-
String targetName = baseName(to);
402-
if (!baseName(from).equals(targetName)) {
403-
try {
404-
if (DocumentsContract.renameDocument(resolver(), docUri(tree, movedId), targetName) == null) {
405-
throw new Exception("Rename after move refused");
406-
}
407-
} catch (Exception renameError) {
408-
// A rename promise must not reject after silently changing parents.
409-
try {
410-
Uri restored = DocumentsContract.moveDocument(resolver(), docUri(tree, movedId),
411-
docUri(tree, toParentId), docUri(tree, fromParent.docId));
412-
if (restored == null) throw new Exception("Rollback move refused");
413-
} catch (Exception rollbackError) {
414-
throw new Exception("FOLDER_STATE_UNCERTAIN: Rename failed and could not be restored: "
415-
+ rollbackError.getMessage(), renameError);
416-
}
417-
throw renameError;
418-
}
392+
if (fromName.equals(targetName)) {
393+
moveDocument(tree, src.docId, fromParent.docId, toParentId);
394+
} else {
395+
moveRenamed(tree, src.docId, fromParent.docId, toParentId, fromName, targetName);
419396
}
420397
}
421398
invalidateParent(tree, from);
@@ -428,6 +405,111 @@ public void rename(PluginCall call) {
428405
}
429406
}
430407

408+
/** One provider move. The document keeps its display name. */
409+
private String moveDocument(Uri tree, String docId, String fromParentId, String toParentId) throws Exception {
410+
Uri moved = DocumentsContract.moveDocument(
411+
resolver(), docUri(tree, docId), docUri(tree, fromParentId), docUri(tree, toParentId)
412+
);
413+
if (moved == null) throw new Exception("Move refused");
414+
return DocumentsContract.getDocumentId(moved);
415+
}
416+
417+
/**
418+
* Rename and insist on the exact name. The platform file provider does not
419+
* refuse a taken name: it quietly lands on a "name (1)" variant, so the
420+
* caller would believe the rename succeeded while the file sits at a path
421+
* nobody asked for. Such a rename is undone and reported instead.
422+
*/
423+
private String renameExactly(Uri tree, String docId, String currentName, String name) throws Exception {
424+
Uri renamed = DocumentsContract.renameDocument(resolver(), docUri(tree, docId), name);
425+
if (renamed == null) throw new Exception("Rename refused");
426+
String renamedId = DocumentsContract.getDocumentId(renamed);
427+
String actual = displayName(renamed);
428+
if (actual == null || actual.equals(name)) return renamedId;
429+
try {
430+
if (DocumentsContract.renameDocument(resolver(), docUri(tree, renamedId), currentName) == null) {
431+
throw new Exception("Rollback rename refused");
432+
}
433+
} catch (Exception rollbackError) {
434+
throw new Exception("FOLDER_STATE_UNCERTAIN: \"" + name + "\" is taken and the file is now named \""
435+
+ actual + "\": " + rollbackError.getMessage());
436+
}
437+
throw new Exception("\"" + name + "\" already exists");
438+
}
439+
440+
/**
441+
* A move that also changes the name. moveDocument keeps the display name,
442+
* so moving first parks the file at targetDir/fromName and fails with
443+
* "Already exists" whenever an unrelated file holds that name there, even
444+
* though the destination itself is free. Cloud sync hit this on every
445+
* retry when the desktop trashed Untitled.md as "trash/Untitled 2.md" and
446+
* the phone's trash still held an older Untitled.md (desktop #813). Take
447+
* the name first, inside the source directory, then cross directories: the
448+
* only path that has to be free is the one the caller asked for. When the
449+
* source directory already holds the target name (in any letter case, the
450+
* storage may fold case), travel under a hidden temporary name and take the
451+
* final name after the move.
452+
*/
453+
private void moveRenamed(Uri tree, String docId, String fromParentId, String toParentId,
454+
String fromName, String targetName) throws Exception {
455+
Map<String, Entry> siblings = listings.get(cacheKey(tree, fromParentId));
456+
if (siblings == null) siblings = listChildren(tree, fromParentId);
457+
boolean targetNameTaken = false;
458+
for (String sibling : siblings.keySet()) {
459+
if (sibling.equalsIgnoreCase(targetName)) {
460+
targetNameTaken = true;
461+
break;
462+
}
463+
}
464+
String travelName = targetNameTaken ? temporaryName(targetName) : targetName;
465+
String travelId = renameExactly(tree, docId, fromName, travelName);
466+
String movedId;
467+
try {
468+
movedId = moveDocument(tree, travelId, fromParentId, toParentId);
469+
} catch (Exception moveError) {
470+
// A rename promise must not reject after silently changing the name.
471+
try {
472+
if (DocumentsContract.renameDocument(resolver(), docUri(tree, travelId), fromName) == null) {
473+
throw new Exception("Rollback rename refused");
474+
}
475+
} catch (Exception rollbackError) {
476+
throw new Exception("FOLDER_STATE_UNCERTAIN: Move failed and the name could not be restored: "
477+
+ rollbackError.getMessage(), moveError);
478+
}
479+
throw moveError;
480+
}
481+
if (travelName.equals(targetName)) return;
482+
try {
483+
renameExactly(tree, movedId, travelName, targetName);
484+
} catch (Exception renameError) {
485+
// A rename promise must not reject after silently changing parents.
486+
try {
487+
String restoredId = moveDocument(tree, movedId, toParentId, fromParentId);
488+
if (DocumentsContract.renameDocument(resolver(), docUri(tree, restoredId), fromName) == null) {
489+
throw new Exception("Rollback rename refused");
490+
}
491+
} catch (Exception rollbackError) {
492+
throw new Exception("FOLDER_STATE_UNCERTAIN: Rename failed and could not be restored: "
493+
+ rollbackError.getMessage(), renameError);
494+
}
495+
throw renameError;
496+
}
497+
}
498+
499+
private static String temporaryName(String name) {
500+
return ".zn-move-" + Long.toHexString(System.nanoTime()) + "-" + name;
501+
}
502+
503+
/** The provider's current display name for a document, or null when it cannot be read. */
504+
private String displayName(Uri doc) {
505+
try (Cursor c = resolver().query(doc, new String[] { Document.COLUMN_DISPLAY_NAME }, null, null, null)) {
506+
if (c != null && c.moveToFirst() && !c.isNull(0)) return c.getString(0);
507+
} catch (Exception ignored) {
508+
// An unverifiable rename is taken at its word rather than failed.
509+
}
510+
return null;
511+
}
512+
431513
@PluginMethod
432514
public void copy(PluginCall call) {
433515
Uri tree = requireTree(call);

‎package-lock.json‎

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

‎package.json‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "zennotes-android",
33
"private": true,
4-
"version": "1.1.23",
4+
"version": "1.1.24",
55
"type": "module",
66
"description": "ZenNotes for Android — Capacitor shell over the ZenNotes app core",
77
"homepage": "https://zennotes.org",
@@ -66,9 +66,9 @@
6666
"vscode-oniguruma": "^2.0.1",
6767
"vscode-textmate": "^9.3.2",
6868
"zustand": "^5.0.2",
69-
"@zennotes/app-core": "file:vendor/zennotes/zennotes-app-core-2.51.0-core.h49d73b531d346192.tgz",
70-
"@zennotes/bridge-contract": "file:vendor/zennotes/zennotes-bridge-contract-2.51.0-boundaries.h18d39d9887df8897.tgz",
71-
"@zennotes/shared-domain": "file:vendor/zennotes/zennotes-shared-domain-2.51.0-boundaries.h18d39d9887df8897.tgz",
69+
"@zennotes/app-core": "file:vendor/zennotes/zennotes-app-core-2.53.0-core.h598c8d004c9228a3.tgz",
70+
"@zennotes/bridge-contract": "file:vendor/zennotes/zennotes-bridge-contract-2.53.0-boundaries.h193dbe157c4e64d2.tgz",
71+
"@zennotes/shared-domain": "file:vendor/zennotes/zennotes-shared-domain-2.53.0-boundaries.h193dbe157c4e64d2.tgz",
7272
"@lezer/common": "^1.5.2"
7373
},
7474
"devDependencies": {

‎src/bridge/mobile-bridge.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ import {
132132
import { folderForRelativePath, posixNormalize, sanitizeNoteTitle } from './vault-core'
133133
import { isPhoneViewport } from '../viewport'
134134

135-
let appVersion = '1.1.23'
135+
let appVersion = '1.1.24'
136136

137137
export async function loadNativeAppVersion(): Promise<string> {
138138
try {
@@ -380,7 +380,13 @@ function mobileAppInfo(): ZenAppInfo {
380380
description: 'ZenNotes for Android',
381381
homepage: 'https://zennotes.org',
382382
runtime: 'web',
383-
hostKind: 'android'
383+
hostKind: 'android',
384+
// The WebView's user agent names the Android version, the device model
385+
// and the Chrome build, the lines a bug report from a phone needs beside
386+
// the app version (#814); nothing else here is guessed.
387+
...(typeof navigator !== 'undefined' && navigator.userAgent
388+
? { engine: navigator.userAgent }
389+
: {})
384390
}
385391
}
386392

‎src/bridge/mobile-cloud-sync.ts‎

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,18 @@ export async function getMobileCloudSettingsConflict(
173173
): Promise<CloudSyncSettingsConflict | null> {
174174
const parked = await vault.fs.statOrNull(CLOUD_SYNC_SETTINGS_CONFLICT_PATH)
175175
if (parked?.type !== 'file') return null
176+
const raw = await vault.fs.readTextOrNull(CLOUD_SYNC_SETTINGS_CONFLICT_PATH)
177+
if (raw === null) return null
178+
// The parsed copy lets the app show what differs and offer a per-section
179+
// answer (desktop parity, #816). A copy that does not parse is still a
180+
// pending question (the file is there, and sync will not touch vault.json
181+
// until it is gone), so it is reported without the contents and the app
182+
// asks whole-file.
183+
const cloudSettings = parseParkedSettings(raw)
176184
return {
177185
path: CLOUD_SYNC_VAULT_SETTINGS_PATH,
178-
cloud_path: CLOUD_SYNC_SETTINGS_CONFLICT_PATH
186+
cloud_path: CLOUD_SYNC_SETTINGS_CONFLICT_PATH,
187+
...(cloudSettings ? { cloud_settings: cloudSettings } : {})
179188
}
180189
}
181190

@@ -189,22 +198,28 @@ export async function resolveMobileCloudSettingsConflict(
189198
): Promise<void> {
190199
if (choice === 'cloud') {
191200
const raw = await vault.fs.readTextOrNull(CLOUD_SYNC_SETTINGS_CONFLICT_PATH)
192-
let parsed: unknown = null
193-
if (raw !== null) {
194-
try {
195-
parsed = JSON.parse(raw)
196-
} catch {
197-
parsed = null
198-
}
199-
}
200-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
201+
const parsed = raw === null ? null : parseParkedSettings(raw)
202+
if (!parsed) {
201203
throw new Error('The settings from the cloud could not be read, so nothing was changed.')
202204
}
203-
await vault.setVaultSettings(parsed as Parameters<MobileVault['setVaultSettings']>[0])
205+
await vault.setVaultSettings(
206+
parsed as unknown as Parameters<MobileVault['setVaultSettings']>[0]
207+
)
204208
}
205209
await vault.fs.deleteFile(CLOUD_SYNC_SETTINGS_CONFLICT_PATH).catch(() => {})
206210
}
207211

212+
function parseParkedSettings(raw: string): Record<string, unknown> | null {
213+
let parsed: unknown
214+
try {
215+
parsed = JSON.parse(raw)
216+
} catch {
217+
return null
218+
}
219+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null
220+
return parsed as Record<string, unknown>
221+
}
222+
208223
export async function listMobileCloudBackups(vault: MobileVault): Promise<CloudBackupSnapshot[]> {
209224
return service.listBackups(hostVault(vault))
210225
}

0 commit comments

Comments
 (0)