From 69cd7d80ad0ce10df734d7a28cfda291f7f2baa0 Mon Sep 17 00:00:00 2001 From: Talon Date: Thu, 18 Jun 2026 15:52:13 +0200 Subject: [PATCH] fix(macos): retain MainWindowController so the client stays alive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The main window controller was created as a local variable in ConnectWindowController.authSucceeded and never retained — ARC deallocated it immediately, which destroyed the VoiceCatClient (connection silently dropped), nil'd every button's weak target (clicks did nothing), and killed event delivery (channel list, messages, voice never worked). Symptom: TOFU (in the retained connect controller) worked, but everything in the main window was a zombie shell. Fix: store the MainWindowController in a new field on ConnectWindowController (which AppDelegate retains for the app's lifetime). Also add NSLog diagnostics in deinit/bootstrap/handleEvent so lifecycle and event delivery are observable from the terminal or Console.app. --- .../VoiceCatMac/Windows/ConnectWindowController.swift | 2 ++ .../VoiceCatMac/Windows/MainWindowController.swift | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/clients/apple/macOS/VoiceCatMac/Windows/ConnectWindowController.swift b/clients/apple/macOS/VoiceCatMac/Windows/ConnectWindowController.swift index da25b87..6c04c83 100644 --- a/clients/apple/macOS/VoiceCatMac/Windows/ConnectWindowController.swift +++ b/clients/apple/macOS/VoiceCatMac/Windows/ConnectWindowController.swift @@ -18,6 +18,7 @@ final class ConnectWindowController: NSWindowController, NSWindowDelegate { private var servers: [SavedServer] = ServerListStore.load() private var client: VoiceCatClient? private var identityDialogShown = false + private var mainWindowController: MainWindowController? // MARK: - Init @@ -298,6 +299,7 @@ final class ConnectWindowController: NSWindowController, NSWindowDelegate { private func authSucceeded(client: VoiceCatClient, selfUserId: UInt32, nickname: String) { client.onEvent = nil let mainWC = MainWindowController(client: client, selfUserId: selfUserId, nickname: nickname) + self.mainWindowController = mainWC mainWC.showWindow(nil) self.client = nil close() diff --git a/clients/apple/macOS/VoiceCatMac/Windows/MainWindowController.swift b/clients/apple/macOS/VoiceCatMac/Windows/MainWindowController.swift index ce26723..84d9dab 100644 --- a/clients/apple/macOS/VoiceCatMac/Windows/MainWindowController.swift +++ b/clients/apple/macOS/VoiceCatMac/Windows/MainWindowController.swift @@ -106,6 +106,10 @@ final class MainWindowController: NSWindowController, NSWindowDelegate { required init?(coder: NSCoder) { fatalError() } + deinit { + NSLog("[VoiceCatMac] MainWindowController deinit — client and event handlers are gone") + } + // MARK: - UI construction private func buildUI() { @@ -447,6 +451,10 @@ final class MainWindowController: NSWindowController, NSWindowDelegate { } ownPermissions = client.getPermissions() + NSLog("[VoiceCatMac] bootstrap: channels=%d users=%d perms{admin=%d kick=%d} currentChannelId=%u", + channels.count, allUsers.count, + ownPermissions.isAdmin, ownPermissions.canKick, currentChannelId) + refreshChannelTree() refreshUserList() rebuildScopePicker() @@ -459,6 +467,9 @@ final class MainWindowController: NSWindowController, NSWindowDelegate { // MARK: - Event handling private func handleEvent(_ event: VoiceCatEvent) { + NSLog("[VoiceCatMac] event type=%d result=%d userId=%u channelId=%u streamId=%u text=%@", + event.type.rawValue, event.result.rawValue, event.userId, event.channelId, + event.streamId, event.text ?? "(nil)") switch event.type { case .channelList: channels = client.listChannels()