From 9284d2415db3574bc67ddc8d463d32ae5cd4eb20 Mon Sep 17 00:00:00 2001 From: Johann Date: Fri, 25 Sep 2026 17:43:16 +0800 Subject: [PATCH] Add update channels to macOS Add an Updates settings tab with Stable and Latest channels plus the startup check toggle. Use the saved channel for both manual and startup checks. Embed the Git commit in Mac builds so Latest can compare builds, and only offer a download when the selected release has a Mac disk image. Update the Mac guide and changelog. Co-Authored-By: OpenAI Codex --- README-macOS.md | 3 +++ apple/shared/AppState.swift | 10 ++++----- apple/shared/CoreEvents.swift | 10 ++++----- core/src/version.cpp | 4 ++++ docs/changelog.txt | 1 + macos/build-core.sh | 4 ++++ macos/project.yml | 14 +++++++++++++ macos/src/AppDelegate.swift | 26 ++++++++++++++++-------- macos/src/SettingsWindowController.swift | 19 ++++++++++++++++- 9 files changed, 70 insertions(+), 21 deletions(-) diff --git a/README-macOS.md b/README-macOS.md index 8a74154..9f4e7d1 100644 --- a/README-macOS.md +++ b/README-macOS.md @@ -198,5 +198,8 @@ versions of FastSM: secondary action, keeping the media player in the background, and moving extra reply mentions to the end. - Advanced — how many pages of posts to fetch per load. +- Updates — choose Stable version releases or every new build from main, and + whether to check automatically at startup. Use Application > Check for Updates + to download the selected Mac disk image. - Confirmation — ask before boosting, unboosting, liking, unliking, clearing a timeline, blocking, unblocking, or deleting a post. diff --git a/apple/shared/AppState.swift b/apple/shared/AppState.swift index 176af96..3e3941e 100644 --- a/apple/shared/AppState.swift +++ b/apple/shared/AppState.swift @@ -472,12 +472,10 @@ final class AppState { } func deleteList(id: String) { client.send("delete_list", ["id": id]) } - // Updates. Mac checks the version-tagged "stable" channel (the commit-based - // "latest" channel needs an embedded build commit, which the Mac build lacks - // — same rationale as Android). - func checkForUpdate(silent: Bool) { - client.send("check_for_update", ["silent": silent, "branch": "stable"]) - } + // The core uses the saved channel; iOS does not invoke this update flow. + func checkForUpdate(silent: Bool) { + client.send("check_for_update", ["silent": silent]) + } func getClientFilter() { client.send("get_client_filter") } func setClientFilter(_ filter: [String: Any]) { client.send("set_client_filter", ["filter": filter]) } diff --git a/apple/shared/CoreEvents.swift b/apple/shared/CoreEvents.swift index b82c69a..9b0cfb8 100644 --- a/apple/shared/CoreEvents.swift +++ b/apple/shared/CoreEvents.swift @@ -487,28 +487,26 @@ struct AccountSettings: Decodable { } } -/// Result of a check_for_update. Mac uses the `dmg_url` (a .dmg release asset); -/// falls back to opening `download_url` if none. +/// Result of a check_for_update. Mac downloads the `dmg_url` asset. struct UpdateStatus: Decodable { var silent = false var available = false + var branch = "stable" var version = "" var notes = "" - var downloadUrl = "" var dmgUrl = "" var error = "" enum CodingKeys: String, CodingKey { - case silent, available, version, notes, error - case downloadUrl = "download_url" + case silent, available, branch, version, notes, error case dmgUrl = "dmg_url" } init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) silent = try c.decodeIfPresent(Bool.self, forKey: .silent) ?? false available = try c.decodeIfPresent(Bool.self, forKey: .available) ?? false + branch = try c.decodeIfPresent(String.self, forKey: .branch) ?? "stable" version = try c.decodeIfPresent(String.self, forKey: .version) ?? "" notes = try c.decodeIfPresent(String.self, forKey: .notes) ?? "" - downloadUrl = try c.decodeIfPresent(String.self, forKey: .downloadUrl) ?? "" dmgUrl = try c.decodeIfPresent(String.self, forKey: .dmgUrl) ?? "" error = try c.decodeIfPresent(String.self, forKey: .error) ?? "" } diff --git a/core/src/version.cpp b/core/src/version.cpp index 035958e..03b11e6 100644 --- a/core/src/version.cpp +++ b/core/src/version.cpp @@ -1,5 +1,9 @@ #include "fastsm/fastsm.hpp" +#if defined(__APPLE__) && __has_include("fastsm_build_commit.h") +#include "fastsm_build_commit.h" +#endif + namespace fastsm { const char* version() { diff --git a/docs/changelog.txt b/docs/changelog.txt index 7571a51..96daade 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -4,6 +4,7 @@ FastSMRW changelog 0.6.0 ----- +- New: on the Mac, choose Stable version releases or every new build from main in Settings, Updates, and choose whether to check at startup. - Fixed: on Mastodon, posting no longer fails with an error sound when you have many timelines open; background refreshing now slows down before it uses up your server's request limit. - Changed: threads and user timelines with nothing new for a day now check for updates every 15 minutes instead of every minute, unless you're viewing them, so keeping many open no longer strains your server's request limit. - New: on iPhone, an Accounts button next to the More button lists your accounts so you can jump straight to one, and add, remove or configure accounts. diff --git a/macos/build-core.sh b/macos/build-core.sh index 689d5d9..444fd4d 100755 --- a/macos/build-core.sh +++ b/macos/build-core.sh @@ -19,6 +19,10 @@ CORE=core/src CXXFLAGS=(-std=c++20 -fexceptions -frtti -O2 -g -I core/include -I deps -I deps/stb_vorbis -Wall -Wno-deprecated-declarations) +COMMIT=$(git rev-parse --short HEAD 2>/dev/null || true) +if [ -n "$COMMIT" ]; then + CXXFLAGS+=("-DFASTSM_BUILD_COMMIT=\"$COMMIT\"") +fi # Portable core sources (mirrors android/app/src/main/cpp/CMakeLists.txt), plus # the C-ABI factory. Windows-only winhttp_client is excluded; the Darwin diff --git a/macos/project.yml b/macos/project.yml index c43ecc1..eecbeac 100644 --- a/macos/project.yml +++ b/macos/project.yml @@ -23,6 +23,19 @@ targets: fastsm_core: type: library.static platform: macOS + preBuildScripts: + - name: Embed build commit + basedOnDependencyAnalysis: false + script: | + COMMIT=$(/usr/bin/git -C "$SRCROOT/.." rev-parse --short HEAD 2>/dev/null || true) + HEADER="$DERIVED_FILE_DIR/fastsm_build_commit.h" + mkdir -p "$DERIVED_FILE_DIR" + printf '#define FASTSM_BUILD_COMMIT "%s"\n' "$COMMIT" > "$HEADER.tmp" + if ! cmp -s "$HEADER.tmp" "$HEADER"; then + mv "$HEADER.tmp" "$HEADER" + else + rm "$HEADER.tmp" + fi sources: # The whole portable core, minus the Windows-only WinHTTP transport. This # is capi/fastsm_core.cpp + net/darwin_http_client.mm + everything the @@ -38,6 +51,7 @@ targets: - ../core/include - ../deps - ../deps/stb_vorbis + - $(DERIVED_FILE_DIR) # stb_vorbis.c is noisy; don't let its warnings clutter the build. GCC_WARN_UNUSED_FUNCTION: NO GCC_WARN_UNUSED_VARIABLE: NO diff --git a/macos/src/AppDelegate.swift b/macos/src/AppDelegate.swift index 839d216..f2c8a7d 100644 --- a/macos/src/AppDelegate.swift +++ b/macos/src/AppDelegate.swift @@ -108,25 +108,35 @@ final class AppDelegate: NSObject, NSApplicationDelegate { if !status.silent { let alert = NSAlert() alert.messageText = "You’re up to date." - alert.informativeText = "FastSMRW \(CoreClient.version) is the latest version." + alert.informativeText = status.branch == "latest" + ? "You’re running the latest build of FastSMRW." + : "FastSMRW \(CoreClient.version) is the latest version." alert.addButton(withTitle: "OK") present(alert, on: window) { _ in } } return } + guard !status.dmgUrl.isEmpty else { + if !status.silent { + ErrorAlert.present("Mac update isn't available yet.", + detail: "The selected release has no Mac download yet. Check again later.", + in: window) + } + return + } let alert = NSAlert() - alert.messageText = "Update available: \(status.version)" - alert.informativeText = status.notes.isEmpty ? "A new version of FastSMRW is available." + alert.messageText = status.branch == "latest" + ? "New build available: \(status.version)" + : "Update available: \(status.version)" + alert.informativeText = status.notes.isEmpty + ? (status.branch == "latest" ? "A new build of FastSMRW is available." + : "A new version of FastSMRW is available.") : status.notes alert.addButton(withTitle: "Download") alert.addButton(withTitle: "Later") present(alert, on: window) { [weak self] response in guard response == .alertFirstButtonReturn else { return } - if !status.dmgUrl.isEmpty { - self?.downloadAndOpenDMG(status.dmgUrl) - } else if let url = URL(string: status.downloadUrl) { - NSWorkspace.shared.open(url) // no Mac disk image; open the release page - } + self?.downloadAndOpenDMG(status.dmgUrl) } } diff --git a/macos/src/SettingsWindowController.swift b/macos/src/SettingsWindowController.swift index ef597ba..ccd107f 100644 --- a/macos/src/SettingsWindowController.swift +++ b/macos/src/SettingsWindowController.swift @@ -3,7 +3,7 @@ // // The preferences window: a toolbar-style tab view mirroring the Windows // settings pages (General, Timelines, Audio, Speech, Behavior, Advanced, -// Confirmation). Each control reads from the core's settings and writes back +// Updates, Confirmation). Each control reads from the core's settings and writes back // through update_settings, which the core applies live. // // Deferred: the per-category reorderable "Speech Details" field lists — those @@ -153,6 +153,17 @@ final class SettingsWindowController: NSWindowController { p.intRow("API pages per fetch:", key: "fetch_pages", default: 3, min: 1, max: 10) } + tab("Updates", "arrow.down.circle") { p in + let channels = [("Stable (version updates)", "stable"), + ("Latest (every build)", "latest")] + p.popup("Update channel:", options: channels, + key: "update_branch", default: "stable") + p.checkbox("Check for updates automatically at startup", + key: "check_updates_on_startup", default: true) + p.label("Stable offers versioned releases. Latest follows new builds from main.") + p.label("Use Application > Check for Updates to check now.") + } + tab("Confirmation", "checkmark.shield") { p in p.checkbox("Confirm boost", key: "confirm_boost", default: false) p.checkbox("Confirm un-boost", key: "confirm_unboost", default: false) @@ -342,6 +353,12 @@ final class SettingsPane: NSViewController { stack.addArrangedSubview(button) } + func label(_ text: String) { + let label = NSTextField(wrappingLabelWithString: text) + label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + stack.addArrangedSubview(label) + } + // MARK: Helpers private func currentValue(key: String, default def: Value) -> Value {