Skip to content
Closed
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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@
All notable changes to Chorus are documented here. Format loosely follows
[Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [Unreleased]

### Changed

- Chorus tells sites it is Safari 27 rather than 26 unless the service has a custom browser setting.

### Fixed

- Hiding Chorus and switching services happen at once. Chorus gives recent page actions a short time to finish before clearing hover or putting the page aside. This does not confirm a server save: reloading or quitting immediately after a click can still lose an unsent change.
- Service tabs no longer cover the top of the page. Returning from Hide cancels its delayed hover exit, and repeated reload requests share one pending reload.
- Chorus no longer crashes on macOS 14 when a page action returns no result.

## [1.5.28] - 2026-10-11

### Changed
Expand Down
24 changes: 24 additions & 0 deletions Chorus.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
0294214694FE69AB69B03C55 /* WebViewJavaScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8291060BF6847242E1F03C6 /* WebViewJavaScript.swift */; };
03162EF33AE4EEB6E524E5B1 /* CatalogGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F17BFF4DCC563429E130A13 /* CatalogGridView.swift */; };
0413EB0C08636424F9362A1C /* FeatureTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09F8BC3392C6945D38F1AEA7 /* FeatureTips.swift */; };
0551F23E1A9E226486C1A0BB /* AddServiceSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECCC4A3133903E40BFB42AA6 /* AddServiceSheet.swift */; };
Expand Down Expand Up @@ -57,6 +58,7 @@
8F6E2C62BD277E5577FBE480 /* WebContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4617D503719C1701307F28 /* WebContentView.swift */; };
92BF212550953DD2E21A12E7 /* EmojiPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01EC3720B49A4D8B953CDA90 /* EmojiPickerView.swift */; };
9722D48056956FA71E911D98 /* ServicePopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200FB7DAF9279B0E2764474D /* ServicePopup.swift */; };
97D0EDAE4F2712616A66B8C2 /* WebViewPoolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E589AEBC286712A2556141FC /* WebViewPoolTests.swift */; };
9912BDFEE290E93DC57ED3C7 /* DemoControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C2BEF1C847F26AD98EABF0 /* DemoControl.swift */; };
9EDF307D989D199DE994D432 /* EditServiceSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD5F8F7C2450C88FA1A3886 /* EditServiceSheet.swift */; };
9F0C82A9D1AFDF927EC1A59C /* DarkReaderSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6935142E75345640B2C4D7F0 /* DarkReaderSupport.swift */; };
Expand All @@ -69,8 +71,10 @@
BAA5421430BDF6F9B918B7B5 /* WebViewPool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 608B27F538D778C6FC4B1697 /* WebViewPool.swift */; };
BAEE46A4791ABB6963914A5D /* NativeAppSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06F91EDD3C2858890FA0B343 /* NativeAppSupport.swift */; };
BD6B6CD0E903780B610BD6B5 /* StoreRecoveryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E05727F83BFEFB338D97E4DF /* StoreRecoveryView.swift */; };
BEB67E84D36E90ADBC75AEF6 /* WebViewDepartureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F3C6319147E80D9FCD5260E /* WebViewDepartureTests.swift */; };
C14E7A689411E8DCCC20ED74 /* fanboy-annoyance.json in Resources */ = {isa = PBXBuildFile; fileRef = 0D9CBE47687FB4FF2EF6BED5 /* fanboy-annoyance.json */; };
C1E4CE7879BB117DBA721C35 /* SpaceStripView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537DF7ABA3D6DED64273776A /* SpaceStripView.swift */; };
C4603B5D275EEABB2283D850 /* InputSettle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89214AFC85F9FE43143A36EC /* InputSettle.swift */; };
C4ADA7C24E806E00FF4709D1 /* ChorusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBA2B8A19B41207F66E51CC3 /* ChorusTests.swift */; };
C4F49AC21F05DCC84FE0D96F /* SetupTransferSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132D2C07876ECAB3E3073872 /* SetupTransferSection.swift */; };
C65C95AB514FF50CEE9A75D6 /* ServiceCatalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = E30F23AFD0B5D81AF103BF37 /* ServiceCatalog.swift */; };
Expand All @@ -80,10 +84,12 @@
CAD06ACCA8483CC80764D8C8 /* hagezi-light.json in Resources */ = {isa = PBXBuildFile; fileRef = D9E64C2450C98941A48AC7BD /* hagezi-light.json */; };
CF6CC3C4CC580F7733D24212 /* SpaceServiceLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 312995FF05808961FD519D76 /* SpaceServiceLink.swift */; };
D19CD58F64D002620BE6850C /* StoreRepair.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC16234E82B88C1DB338A49 /* StoreRepair.swift */; };
D1C6FFBDDA20E8172D0BC347 /* InputSettleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90198C05F204DF1B75DC485C /* InputSettleTests.swift */; };
D2FF4AA7BA395992C0609D93 /* ChorusAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2E3F3C66C05EA91501A73EF /* ChorusAppDelegate.swift */; };
D341B327E1C20FA11DEC2C2B /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D533B667501DF0913FF62B9A /* AppState.swift */; };
D4721545C5C3DD809AE549A7 /* ChorusStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50260A359445D2B4C556E1E6 /* ChorusStyle.swift */; };
D861B394A60C4CD2F47CF8EA /* ServiceHealth.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD9BB4DBF6536D1AD0CA298 /* ServiceHealth.swift */; };
D887CCC00E8FE67581C132C4 /* WebViewDeparture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FF59EADC9EE4BBF5C9E0B8E /* WebViewDeparture.swift */; };
DB9B6FD9265D63DCA11B6CFC /* AppCapabilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E0D298EDEBDC1F1EB614912 /* AppCapabilities.swift */; };
DF712EA56B5AE10C7BF77176 /* WebViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438D78C9503E732B7792FA10 /* WebViewCoordinator.swift */; };
DFA25C2036755E1DAD624BA5 /* licenses in Resources */ = {isa = PBXBuildFile; fileRef = 7C3DF954AFE066BC42761ABC /* licenses */; };
Expand Down Expand Up @@ -135,6 +141,7 @@
464C332182D96B75A17F363C /* WhatsNew.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhatsNew.swift; sourceTree = "<group>"; };
4666C49AA129F26D936B0715 /* ServiceInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceInstance.swift; sourceTree = "<group>"; };
4D2B28970B7AEF9B0EB53AFD /* UnifiedRailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnifiedRailView.swift; sourceTree = "<group>"; };
4FF59EADC9EE4BBF5C9E0B8E /* WebViewDeparture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewDeparture.swift; sourceTree = "<group>"; };
50260A359445D2B4C556E1E6 /* ChorusStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChorusStyle.swift; sourceTree = "<group>"; };
52D93229EEC510DC0850005E /* SpaceHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceHeaderView.swift; sourceTree = "<group>"; };
537DF7ABA3D6DED64273776A /* SpaceStripView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceStripView.swift; sourceTree = "<group>"; };
Expand All @@ -156,7 +163,10 @@
7E0D298EDEBDC1F1EB614912 /* AppCapabilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCapabilities.swift; sourceTree = "<group>"; };
8223D66D602364046AF98EDC /* Chorus.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Chorus.entitlements; sourceTree = "<group>"; };
88C307C75776869CE8A82618 /* AppPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPreferences.swift; sourceTree = "<group>"; };
89214AFC85F9FE43143A36EC /* InputSettle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputSettle.swift; sourceTree = "<group>"; };
8F17BFF4DCC563429E130A13 /* CatalogGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogGridView.swift; sourceTree = "<group>"; };
8F3C6319147E80D9FCD5260E /* WebViewDepartureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewDepartureTests.swift; sourceTree = "<group>"; };
90198C05F204DF1B75DC485C /* InputSettleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputSettleTests.swift; sourceTree = "<group>"; };
94326A5DEE01135F945E9DF3 /* Chorus.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Chorus.app; sourceTree = BUILT_PRODUCTS_DIR; };
9D811CC9D75730152E17859C /* MenuBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarView.swift; sourceTree = "<group>"; };
9DD5F8F7C2450C88FA1A3886 /* EditServiceSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditServiceSheet.swift; sourceTree = "<group>"; };
Expand All @@ -169,6 +179,7 @@
B44B9AB3EC52D565BFB46183 /* ServiceRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceRowView.swift; sourceTree = "<group>"; };
B5AEA5DCBB5AB96E2C962A90 /* WindowBackdrop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowBackdrop.swift; sourceTree = "<group>"; };
B71CC3F7D7BD76E4100BC126 /* StoreRelocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreRelocation.swift; sourceTree = "<group>"; };
B8291060BF6847242E1F03C6 /* WebViewJavaScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewJavaScript.swift; sourceTree = "<group>"; };
B8E0761BBE5A85E34BC73573 /* ServiceTabs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceTabs.swift; sourceTree = "<group>"; };
BFD9BB4DBF6536D1AD0CA298 /* ServiceHealth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceHealth.swift; sourceTree = "<group>"; };
CA55A0E026084DC20071FBCE /* QuickSwitcherView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickSwitcherView.swift; sourceTree = "<group>"; };
Expand All @@ -185,6 +196,7 @@
E143D686890F381C5B42761F /* ChorusApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChorusApp.swift; sourceTree = "<group>"; };
E1E431CF7CE145D536081A23 /* UserAgentProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAgentProvider.swift; sourceTree = "<group>"; };
E30F23AFD0B5D81AF103BF37 /* ServiceCatalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceCatalog.swift; sourceTree = "<group>"; };
E589AEBC286712A2556141FC /* WebViewPoolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewPoolTests.swift; sourceTree = "<group>"; };
E84C935C8A82FD6B6B969E6B /* ServiceIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceIconView.swift; sourceTree = "<group>"; };
E88CC47CBB66B4749ACDC068 /* AppLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLogger.swift; sourceTree = "<group>"; };
EA17C93CEDF437318D536482 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
Expand Down Expand Up @@ -215,6 +227,7 @@
B8E0761BBE5A85E34BC73573 /* ServiceTabs.swift */,
09D9F34755F65BE5A3C76C88 /* WebViewContainer.swift */,
438D78C9503E732B7792FA10 /* WebViewCoordinator.swift */,
4FF59EADC9EE4BBF5C9E0B8E /* WebViewDeparture.swift */,
608B27F538D778C6FC4B1697 /* WebViewPool.swift */,
192C907C2215D62E02FF8408 /* WebViewState.swift */,
);
Expand Down Expand Up @@ -310,8 +323,10 @@
CA77C4C73BEB532F6AC1C339 /* AppRelauncher.swift */,
38CB29411D3B889CE0623121 /* Deadline.swift */,
1ACFB877AD2138035F9E3EAE /* FaviconFetcher.swift */,
89214AFC85F9FE43143A36EC /* InputSettle.swift */,
313680424148829FCBEAA9BC /* ModelContext+Save.swift */,
E1E431CF7CE145D536081A23 /* UserAgentProvider.swift */,
B8291060BF6847242E1F03C6 /* WebViewJavaScript.swift */,
);
path = Utilities;
sourceTree = "<group>";
Expand Down Expand Up @@ -449,6 +464,9 @@
isa = PBXGroup;
children = (
FBA2B8A19B41207F66E51CC3 /* ChorusTests.swift */,
90198C05F204DF1B75DC485C /* InputSettleTests.swift */,
8F3C6319147E80D9FCD5260E /* WebViewDepartureTests.swift */,
E589AEBC286712A2556141FC /* WebViewPoolTests.swift */,
);
path = ChorusTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -583,6 +601,7 @@
3DEA98A98656EE981A29A71D /* FindInPageBar.swift in Sources */,
06696BB66C7BAA761E8291C9 /* HibernatedBadgePoller.swift in Sources */,
A98FA6B9454EC0BCEDEED093 /* InAppBrowserWindow.swift in Sources */,
C4603B5D275EEABB2283D850 /* InputSettle.swift in Sources */,
091825995DF2FD9C4A7BC86F /* KeyboardShortcutManager.swift in Sources */,
701984CFE765170D8EAE50EE /* MenuBarView.swift in Sources */,
6C15E37D4717938D4C31384A /* ModelContext+Save.swift in Sources */,
Expand Down Expand Up @@ -622,6 +641,8 @@
A22B72681582D92F92938917 /* WebToolbarView.swift in Sources */,
5561BE9B6C6D8AA46CD42884 /* WebViewContainer.swift in Sources */,
DF712EA56B5AE10C7BF77176 /* WebViewCoordinator.swift in Sources */,
D887CCC00E8FE67581C132C4 /* WebViewDeparture.swift in Sources */,
0294214694FE69AB69B03C55 /* WebViewJavaScript.swift in Sources */,
BAA5421430BDF6F9B918B7B5 /* WebViewPool.swift in Sources */,
783EF6C467034D74DE8D5F52 /* WebViewState.swift in Sources */,
27E1865FA1BB65766DBDB808 /* WhatsNew.swift in Sources */,
Expand All @@ -634,6 +655,9 @@
buildActionMask = 2147483647;
files = (
C4ADA7C24E806E00FF4709D1 /* ChorusTests.swift in Sources */,
D1C6FFBDDA20E8172D0BC347 /* InputSettleTests.swift in Sources */,
BEB67E84D36E90ADBC75AEF6 /* WebViewDepartureTests.swift in Sources */,
97D0EDAE4F2712616A66B8C2 /* WebViewPoolTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
54 changes: 7 additions & 47 deletions Chorus/App/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1058,56 +1058,14 @@ final class AppState {
})
}

/// Gives every live page a save point before the app quits, then lets it
/// go. Called from `applicationShouldTerminate`, so every way out — the
/// menu, ⌘Q, a relaunch, a Sparkle install — passes through it.
///
/// Pages save when they go hidden, and a quit kills them with no
/// `visibilitychange` or `pagehide` at all. Chorus pins every page to
/// visible besides, so WhatsApp Web never reached the point where it writes
/// its session, and could come back signed out. Three steps, each capped so
/// a wedged page can't hold the quit hostage, about a second in all:
/// release every page to hidden, give the pages a moment to write, then read
/// each data store's records — a round trip through the storage process that
/// lands behind the writes just queued. That last step is best effort.
/// Gives live pages bounded time before quit, relaunch and installation.
/// This grace period does not confirm a provider's server save.
func releasePagesForQuit() async {
let webViews = webViewPool.liveWebViews
guard !webViews.isEmpty else { return }

// Started together, awaited in turn: all the pages get the signal at
// once, and the deadline bounds the wait for the slowest.
await withDeadline(seconds: 0.3, fallback: ()) {
let releases = webViews.map { webView in
Task { @MainActor in
_ = try? await webView.evaluateJavaScript(UserScriptManager.quitReleaseJS)
}
}
for release in releases { await release.value }
}

try? await Task.sleep(for: Self.quitWriteWindow)

var seen = Set<ObjectIdentifier>()
let stores = webViews
.map { $0.configuration.websiteDataStore }
.filter { seen.insert(ObjectIdentifier($0)).inserted }
await withDeadline(seconds: 0.5, fallback: ()) {
let flushes = stores.map { store in
Task { @MainActor in
_ = await store.dataRecords(ofTypes: [
WKWebsiteDataTypeLocalStorage,
WKWebsiteDataTypeIndexedDBDatabases,
])
}
}
for flush in flushes { await flush.value }
}
await WebViewDeparture.prepareForQuit(in: webViews)
AppLogger.webView.info("Released \(webViews.count) pages for quit")
}

/// How long pages get to write after going hidden before quit goes on.
static let quitWriteWindow: Duration = .milliseconds(250)

/// Pauses or resumes polling when network connectivity toggles. While
/// offline every poll (active, background, and hibernated) would only fire
/// doomed requests, draining battery for nothing; on reconnect we restart
Expand Down Expand Up @@ -1196,10 +1154,12 @@ final class AppState {
// MARK: - Active service actions (driven by keyboard shortcuts)

/// Reload the currently displayed service's web view. Triggered by Cmd-R.
func reloadActiveService() {
func reloadActiveService() async {
guard let id = webViewPool.activeServiceID,
let webView = webViewPool.displayedWebView(for: id) else { return }
WebViewCoordinator.reload(webView, fallbackURL: fetchService(id: id).flatMap { URL(string: $0.url) })
// Capture the target before waiting, rather than reloading whichever
// service happens to be active when the wait ends.
await WebViewCoordinator.reload(webView, fallbackURL: fetchService(id: id).flatMap { URL(string: $0.url) })
}

// MARK: - Service tabs
Expand Down
20 changes: 19 additions & 1 deletion Chorus/App/ChorusApp.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import SwiftUI
import AppKit
import SwiftData
#if canImport(Sparkle)
import Sparkle
Expand Down Expand Up @@ -105,6 +106,23 @@ struct ChorusApp: App {
.keyboardShortcut("w", modifiers: .command)
}

// End native hover before hiding, without delaying the command.
CommandGroup(replacing: .appVisibility) {
Button("Hide Chorus") {
WebViewDeparture.hideApplication()
}
.keyboardShortcut("h", modifiers: .command)

Button("Hide Others") {
NSApp.hideOtherApplications(nil)
}
.keyboardShortcut("h", modifiers: [.command, .option])

Button("Show All") {
NSApp.unhideAllApplications(nil)
}
}

KeyboardShortcutCommands(
selectedServiceID: Binding(
get: { appState.selectedServiceID },
Expand All @@ -128,7 +146,7 @@ struct ChorusApp: App {

CommandGroup(after: .toolbar) {
Button("Reload") {
appState.reloadActiveService()
Task { await appState.reloadActiveService() }
}
.keyboardShortcut("r", modifiers: .command)

Expand Down
Loading