| index 7b75e69..c79aa33 100644 |
| --- a/ios/LChatMac/NativeFeedView.swift |
| +++ b/ios/LChatMac/NativeFeedView.swift |
| @@ -52,6 +52,12 @@ struct NativeFeedView: View { |
| // The model arrives PARKED (NativeFeedModelCache) — folded messages and |
| // a live socket survive leaving the chat, so re-opening never flashes. |
| _model = StateObject(wrappedValue: model) |
| + // A parked model shows from the FIRST frame: history is folded and |
| + // .defaultScrollAnchor(.bottom) makes the first layout land at the |
| + // bottom, so there's nothing to hide — holding it behind the settle |
| + // verify (a ~50ms round-trip) blinked on every chat switch. Only a |
| + // cold model starts hidden; the pin still verifies either way. |
| + _settled = State(initialValue: model.ready) |
| self.isGroup = isGroup |
| self.isActive = isActive |
| self.roster = roster |
| @@ -359,16 +365,17 @@ struct NativeFeedView: View { |
| // races layout and loses (the land-at-top bug, same as iOS). |
| .defaultScrollAnchor(.bottom) |
| // The feed reveals only once history has folded AND the pin has |
| - // verifiably landed — a parked chat settles in a frame or two (an |
| - // imperceptible blink), a cold one shows the loader; either way |
| - // the reader finds it already at the bottom, never mid-snap. |
| + // verifiably landed — a parked chat settles in a frame or two, a |
| + // cold one shows the loader; either way the reader finds it |
| + // already at the bottom, never mid-snap. The reveal is a hard cut |
| + // (no animation): with messages in hand, switching chats must |
| + // read as instant, not a cross-fade. |
| .opacity(model.ready && settled ? 1 : 0) |
| .overlay { |
| if !model.ready { |
| ProgressView().controlSize(.small) |
| } |
| } |
| - .animation(.easeOut(duration: 0.12), value: model.ready && settled) |
| } |
| // The thread view lifts over the FEED pane only (the web's |
| // .chatapp-feedwrap overlay) — applied before the composer inset, so |
| @@ -440,9 +447,10 @@ struct NativeFeedView: View { |
| |
| static let bottomSentinel = "bottom-sentinel" |
| |
| - // The feed stays INVISIBLE until the pin is verified flush — the reader |
| + // A COLD feed stays invisible until the pin is verified flush — the reader |
| // should find the chat already at the bottom, never watch it snap there. |
| - @State private var settled = false |
| + // A parked one starts settled (see init) so switching chats is instant. |
| + @State private var settled: Bool |
| |
| // Pin to the bottom sentinel and VERIFY it landed: the frame store knows |
| // where the last row actually is, so re-pin (a few frames apart) until its |