eleven / messenger Public

mac: hidden panes must not fight for the composer's focus (#930)

Switching spaces left the previous pane's composerFocused stale at
true — the old @FocusState dropped itself when focus moved; the
representable's plain @State cannot — so the hidden pane's field kept
async-stealing first responder from the one the user switched to,
reading as a dead input for a few seconds. Focus claims now follow
isActive: a pane releases its claim when it goes inactive and only the
active pane's mount-time asserts (onAppear / settled) may grab it.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 23917e1833d978e9b49284b320fd232374af6405 parent 408106b
1 file changed, +11 −3
  • ios/LChatMac/NativeFeedView.swift +11 −3
diff --git a/ios/LChatMac/NativeFeedView.swift b/ios/LChatMac/NativeFeedView.swift
index 10941af..a704007 100644
--- a/ios/LChatMac/NativeFeedView.swift
+++ b/ios/LChatMac/NativeFeedView.swift
@@ -572,7 +572,14 @@ struct NativeFeedView: View {
installSwipeMonitor()
registerForDrops()
}
- .onChange(of: isActive) { _, active in frames.isActive = active }
+ .onChange(of: isActive) { _, active in
+ frames.isActive = active
+ // Release or claim the composer with the pane: a hidden pane's
+ // stale focused=true would keep async-stealing first responder
+ // from the visible pane's field (the old @FocusState dropped
+ // itself automatically; the representable's @State cannot).
+ composerFocused = active
+ }
.onDisappear {
// Deliberately NOT model.stop(): parked models keep their socket
// (the FeedWebCache rule); the cache's eviction stops them.
@@ -1337,13 +1344,14 @@ struct NativeFeedView: View {
// feed reveals: a FocusState write in onAppear races the
// freshly-inserted (still invisible) field and loses.
.onAppear {
+ guard isActive else { return }
composerFocused = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
- composerFocused = true
+ if isActive { composerFocused = true }
}
}
.onChange(of: settled) { _, isSettled in
- if isSettled { composerFocused = true }
+ if isSettled, isActive { composerFocused = true }
}
Button(action: sendDraft) {
if sendQueued {