diff --git a/apps/mobile/android/app/build.gradle b/apps/mobile/android/app/build.gradle
index 6e428a4ad..613cd828c 100644
--- a/apps/mobile/android/app/build.gradle
+++ b/apps/mobile/android/app/build.gradle
@@ -140,7 +140,7 @@ android {
if (project.hasProperty("prBuildNumber")) {
versionCode Integer.parseInt(prBuildNumber())
} else {
- versionCode 3109
+ versionCode 3110
}
versionName getNpmVersion()
testBuildType System.getProperty('testBuildType', 'debug')
diff --git a/apps/mobile/ios/build-configs/ios-build.active.xcconfig b/apps/mobile/ios/build-configs/ios-build.active.xcconfig
index c56a81689..e52cde807 100644
--- a/apps/mobile/ios/build-configs/ios-build.active.xcconfig
+++ b/apps/mobile/ios/build-configs/ios-build.active.xcconfig
@@ -1,6 +1,6 @@
// Production iOS build identifiers
-IOS_CURRENT_PROJECT_VERSION = 2187
-IOS_MARKETING_VERSION = 3.4.3
+IOS_CURRENT_PROJECT_VERSION = 2188
+IOS_MARKETING_VERSION = 3.4.4
IOS_MAIN_BUNDLE_ID = org.streetwriters.notesnook
IOS_WIDGET_BUNDLE_ID = org.streetwriters.notesnook.notewidget
IOS_SHARE_BUNDLE_ID = org.streetwriters.notesnook.share
diff --git a/apps/mobile/ios/build-configs/ios-build.production.xcconfig b/apps/mobile/ios/build-configs/ios-build.production.xcconfig
index c56a81689..e52cde807 100644
--- a/apps/mobile/ios/build-configs/ios-build.production.xcconfig
+++ b/apps/mobile/ios/build-configs/ios-build.production.xcconfig
@@ -1,6 +1,6 @@
// Production iOS build identifiers
-IOS_CURRENT_PROJECT_VERSION = 2187
-IOS_MARKETING_VERSION = 3.4.3
+IOS_CURRENT_PROJECT_VERSION = 2188
+IOS_MARKETING_VERSION = 3.4.4
IOS_MAIN_BUNDLE_ID = org.streetwriters.notesnook
IOS_WIDGET_BUNDLE_ID = org.streetwriters.notesnook.notewidget
IOS_SHARE_BUNDLE_ID = org.streetwriters.notesnook.share
diff --git a/apps/mobile/ios/build-configs/ios-build.staging.xcconfig b/apps/mobile/ios/build-configs/ios-build.staging.xcconfig
index 93c4a33cf..50bee7ee8 100644
--- a/apps/mobile/ios/build-configs/ios-build.staging.xcconfig
+++ b/apps/mobile/ios/build-configs/ios-build.staging.xcconfig
@@ -1,6 +1,6 @@
// Staging iOS build identifiers
-IOS_CURRENT_PROJECT_VERSION = 2187
-IOS_MARKETING_VERSION = 3.4.3
+IOS_CURRENT_PROJECT_VERSION = 2188
+IOS_MARKETING_VERSION = 3.4.4
IOS_MAIN_BUNDLE_ID = org.streetwriters.notesnook
IOS_WIDGET_BUNDLE_ID = org.streetwriters.notesnook.notewidget
IOS_SHARE_BUNDLE_ID = org.streetwriters.notesnook.share
diff --git a/apps/mobile/package.json b/apps/mobile/package.json
index 7aef65d4d..265955be3 100644
--- a/apps/mobile/package.json
+++ b/apps/mobile/package.json
@@ -1,6 +1,6 @@
{
"name": "@notesnook/mobile",
- "version": "3.4.3",
+ "version": "3.4.4",
"private": true,
"license": "GPL-3.0-or-later",
"scripts": {
@@ -18,7 +18,7 @@
"e2e-android": "detox test --configuration android.emu.release --detectOpenHandles",
"e2e-android-debug": "detox test --configuration android.emu.debug --detectOpenHandles",
"e2e-ios": "detox test -c ios.sim.release --detectOpenHandles",
- "bump": "npx react-native bump-version --skip-semver-for android",
+ "bump": "node ./scripts/bump-version.mjs",
"release-android": "cd android && GITHUB_RELEASE=true ENVFILE=.env.public ./gradlew assembleRelease --no-daemon",
"release-android-bundle": "cd android && ./gradlew bundleRelease --no-daemon",
"lint": "eslint --ext .js,.jsx,.tsx,.ts ./app"
diff --git a/apps/mobile/scripts/bump-version.mjs b/apps/mobile/scripts/bump-version.mjs
new file mode 100644
index 000000000..fa7ae7d4d
--- /dev/null
+++ b/apps/mobile/scripts/bump-version.mjs
@@ -0,0 +1,194 @@
+/*
+This file is part of the Notesnook project (https://notesnook.com/)
+
+Copyright (C) 2023 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+import { promises as fs } from "fs";
+import path from "path";
+
+const PROJECT_ROOT = path.resolve(
+ new URL(import.meta.url).pathname,
+ "..",
+ ".."
+); // scripts/.. -> mobile
+
+const REPO_ROOT = path.resolve(PROJECT_ROOT, "..", ".."); // mobile/../.. -> repo root
+const IOS_BUILD_CONFIGS_DIR = path.resolve(PROJECT_ROOT, "ios/build-configs");
+const ANDROID_GRADLE = path.resolve(PROJECT_ROOT, "android/app/build.gradle");
+const PACKAGE_JSON = path.resolve(PROJECT_ROOT, "package.json");
+const FDROID_CHANGELOGS_DIR = path.resolve(
+ REPO_ROOT,
+ "fastlane/metadata/android/en-US/changelogs"
+);
+
+async function resolveVersion() {
+ const provided = process.argv[2];
+ if (provided) {
+ if (!/^\d+\.\d+\.\d+$/.test(provided)) {
+ console.error(`Invalid version "${provided}". Expected format: x.y.z`);
+ process.exit(1);
+ }
+ return provided;
+ }
+ // No version provided: bump the current package.json version as a patch.
+ const pkg = JSON.parse(await fs.readFile(PACKAGE_JSON, "utf8"));
+ const match = String(pkg.version).match(/^(\d+)\.(\d+)\.(\d+)$/);
+ if (!match) {
+ console.error(
+ `Cannot auto-bump: package.json version "${pkg.version}" is not x.y.z`
+ );
+ process.exit(1);
+ }
+ const [, major, minor, patch] = match;
+ return `${major}.${minor}.${Number(patch) + 1}`;
+}
+
+const version = await resolveVersion();
+
+async function updatePackageJson() {
+ const raw = await fs.readFile(PACKAGE_JSON, "utf8");
+ const pkg = JSON.parse(raw);
+ const from = pkg.version;
+ pkg.version = version;
+ // Preserve trailing newline if present.
+ const trailingNewline = raw.endsWith("\n") ? "\n" : "";
+ await fs.writeFile(
+ PACKAGE_JSON,
+ JSON.stringify(pkg, null, 2) + trailingNewline
+ );
+ console.log(`package.json: version ${from} -> ${version}`);
+}
+
+async function updateAndroid() {
+ let content = await fs.readFile(ANDROID_GRADLE, "utf8");
+ const match = content.match(/versionCode\s+(\d+)/);
+ if (!match) {
+ throw new Error("Could not find `versionCode ` in build.gradle");
+ }
+ const current = parseInt(match[1], 10);
+ const next = current + 1;
+ content = content.replace(match[0], `versionCode ${next}`);
+ await fs.writeFile(ANDROID_GRADLE, content);
+ console.log(`android/app/build.gradle: versionCode ${current} -> ${next}`);
+ return next;
+}
+
+// Derives the F-Droid release version code from build.gradle. F-Droid builds a
+// separate APK per ABI (see `applicationVariants.all` in build.gradle) using:
+// output.versionCodeOverride = defaultConfig.versionCode * + versionCodes[abi]
+// and publishes the highest of those. So the changelog file is named after
+// `versionCode * + max(versionCodes)`. Both are parsed here rather than
+// hardcoded so this stays in sync with the formula in build.gradle.
+async function computeFdroidVersionCode(versionCode) {
+ const gradle = await fs.readFile(ANDROID_GRADLE, "utf8");
+
+ const multMatch = gradle.match(/defaultConfig\.versionCode\s*\*\s*(\d+)/);
+ if (!multMatch) {
+ throw new Error(
+ "Could not find the F-Droid versionCode multiplier in build.gradle"
+ );
+ }
+ const multiplier = parseInt(multMatch[1], 10);
+
+ const mapMatch = gradle.match(/versionCodes\s*=\s*\[([^\]]*)\]/);
+ if (!mapMatch) {
+ throw new Error("Could not find the ABI versionCodes map in build.gradle");
+ }
+ const abiCodes = [...mapMatch[1].matchAll(/:\s*(\d+)/g)].map((m) =>
+ parseInt(m[1], 10)
+ );
+ if (abiCodes.length === 0) {
+ throw new Error("ABI versionCodes map in build.gradle is empty");
+ }
+ const maxAbiCode = Math.max(...abiCodes);
+
+ return versionCode * multiplier + maxAbiCode;
+}
+
+async function createFdroidChangelog(versionCode) {
+ const fdroidVersionCode = await computeFdroidVersionCode(versionCode);
+ const file = path.resolve(FDROID_CHANGELOGS_DIR, `${fdroidVersionCode}.txt`);
+ const rel = path.relative(REPO_ROOT, file);
+
+ try {
+ await fs.access(file);
+ console.log(`${rel}: already exists, skipped`);
+ return;
+ } catch {
+ // File does not exist yet — create it.
+ }
+
+ const placeholder = "- Minor bug fixes and improvements\n\nThank you for using Notesnook!\n";
+ await fs.mkdir(FDROID_CHANGELOGS_DIR, { recursive: true });
+ await fs.writeFile(file, placeholder);
+ console.log(`${rel}: created (edit with this release's changelog)`);
+}
+
+async function updateIosConfig(file) {
+ let content = await fs.readFile(file, "utf8");
+ const rel = path.relative(PROJECT_ROOT, file);
+
+ const buildMatch = content.match(/IOS_CURRENT_PROJECT_VERSION\s*=\s*(\d+)/);
+ if (!buildMatch) {
+ throw new Error(`Could not find IOS_CURRENT_PROJECT_VERSION in ${rel}`);
+ }
+ const currentBuild = parseInt(buildMatch[1], 10);
+ const nextBuild = currentBuild + 1;
+ content = content.replace(
+ buildMatch[0],
+ `IOS_CURRENT_PROJECT_VERSION = ${nextBuild}`
+ );
+
+ const marketingMatch = content.match(/IOS_MARKETING_VERSION\s*=\s*(\S+)/);
+ if (!marketingMatch) {
+ throw new Error(`Could not find IOS_MARKETING_VERSION in ${rel}`);
+ }
+ const currentMarketing = marketingMatch[1];
+ content = content.replace(
+ marketingMatch[0],
+ `IOS_MARKETING_VERSION = ${version}`
+ );
+
+ await fs.writeFile(file, content);
+ console.log(
+ `${rel}: version ${currentMarketing} -> ${version}, build ${currentBuild} -> ${nextBuild}`
+ );
+}
+
+async function updateIos() {
+ const entries = await fs.readdir(IOS_BUILD_CONFIGS_DIR);
+ const configs = entries.filter((e) => e.endsWith(".xcconfig"));
+ if (configs.length === 0) {
+ throw new Error("No .xcconfig files found in ios/build-configs");
+ }
+ for (const config of configs) {
+ await updateIosConfig(path.resolve(IOS_BUILD_CONFIGS_DIR, config));
+ }
+}
+
+async function run() {
+ console.log(`Bumping to version ${version}...\n`);
+ await updatePackageJson();
+ const androidVersionCode = await updateAndroid();
+ await updateIos();
+ await createFdroidChangelog(androidVersionCode);
+ console.log("\nDone.");
+}
+
+run().catch((err) => {
+ console.error("Failed:", err?.message || err);
+ process.exitCode = 1;
+});
diff --git a/fastlane/metadata/android/en-US/changelogs/15554.txt b/fastlane/metadata/android/en-US/changelogs/15554.txt
new file mode 100644
index 000000000..001b7f88d
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/15554.txt
@@ -0,0 +1,7 @@
+- Inbox API is now in beta. Learn more about it at https://help.notesnook.com/inbox-api/getting-started
+- Added close all tabs button in editor
+- Deeplinking support for nn:// urls
+- Set default image compression mode in Settings -> Behavior
+- Minor bug fixes and performance improvements
+
+Thank you for using Notesnook!