11import AppKit
2+ import Combine
23import ComposableArchitecture
34import MarkdownUI
45import OpenAIService
@@ -40,12 +41,15 @@ private struct ListHeightPreferenceKey: PreferenceKey {
4041
4142struct ChatPanelMessages : View {
4243 let chat : StoreOf < Chat >
44+ @State var cancellable = Set < AnyCancellable > ( )
4345 @State var isScrollToBottomButtonDisplayed = true
4446 @State var isPinnedToBottom = true
4547 @Namespace var bottomID
4648 @Namespace var scrollSpace
4749 @State var scrollOffset : Double = 0
4850 @State var listHeight : Double = 0
51+
52+ @Environment ( \. isEnabled) var isEnabled
4953
5054 var body : some View {
5155 ScrollViewReader { proxy in
@@ -80,10 +84,6 @@ struct ChatPanelMessages: View {
8084 value: offset
8185 )
8286 } )
83- . preference (
84- key: ListHeightPreferenceKey . self,
85- value: listGeo. size. height
86- )
8787 }
8888 . modify { view in
8989 if #available( macOS 13 . 0 , * ) {
@@ -95,22 +95,15 @@ struct ChatPanelMessages: View {
9595 }
9696 . listStyle ( . plain)
9797 . coordinateSpace ( name: scrollSpace)
98+ . preference (
99+ key: ListHeightPreferenceKey . self,
100+ value: listGeo. size. height
101+ )
98102 . onPreferenceChange ( ListHeightPreferenceKey . self) { value in
99103 listHeight = value
100104 updatePinningState ( )
101105 }
102106 . onPreferenceChange ( ScrollViewOffsetPreferenceKey . self) { value in
103- /// I don't know if there is a way to detect that a scroll is triggered by user
104- let scrollUpToThreshold = listHeight > 0 // sometimes it can suddenly become 0
105- && value > listHeight + 32 + 20 // scroll up to a threshold
106- && value > scrollOffset // it's scroll up
107- && value - scrollOffset < 100 // it's not some mystery jump
108- /// Scroll up too much and the tracker is lost
109- let checkerOutOfScope = value <= 0
110- if checkerOutOfScope || scrollUpToThreshold {
111- isPinnedToBottom = false
112- }
113-
114107 scrollOffset = value
115108 updatePinningState ( )
116109 }
@@ -121,7 +114,6 @@ struct ChatPanelMessages: View {
121114 . opacity ( viewStore. state ? 1 : 0 )
122115 . disabled ( !viewStore. state)
123116 . transformEffect ( . init( translationX: 0 , y: viewStore. state ? 0 : 20 ) )
124- . animation ( . easeInOut( duration: 0.2 ) , value: viewStore. state)
125117 }
126118 }
127119 . overlay ( alignment: . bottomTrailing) {
@@ -134,12 +126,34 @@ struct ChatPanelMessages: View {
134126 }
135127 }
136128 }
129+ . onAppear {
130+ trackScrollWheel ( )
131+ }
132+ . onDisappear {
133+ cancellable. forEach { $0. cancel ( ) }
134+ cancellable = [ ]
135+ }
136+ }
137+
138+ func trackScrollWheel( ) {
139+ NSApplication . shared. publisher ( for: \. currentEvent)
140+ . filter { _ in isEnabled }
141+ . filter { event in event? . type == . scrollWheel }
142+ . sink { event in
143+ guard isEnabled, isPinnedToBottom else { return }
144+ let delta = event? . deltaY ?? 0
145+ let scrollUp = delta > 0
146+ if scrollUp {
147+ isPinnedToBottom = false
148+ }
149+ }
150+ . store ( in: & cancellable)
137151 }
138152
139153 @MainActor
140154 func updatePinningState( ) {
141155 // where does the 32 come from?
142- withAnimation {
156+ withAnimation ( . linear ( duration : 0.1 ) ) {
143157 isScrollToBottomButtonDisplayed = scrollOffset > listHeight + 32 + 20
144158 || scrollOffset <= 0
145159 }
@@ -148,6 +162,7 @@ struct ChatPanelMessages: View {
148162 @ViewBuilder
149163 func scrollToBottomButton( proxy: ScrollViewProxy ) -> some View {
150164 Button ( action: {
165+ isPinnedToBottom = true
151166 withAnimation ( . easeInOut( duration: 0.1 ) ) {
152167 proxy. scrollTo ( bottomID, anchor: . bottom)
153168 }
@@ -193,6 +208,7 @@ struct ChatPanelMessages: View {
193208 . onChange ( of: viewStore. state. isReceivingMessage) { isReceiving in
194209 if isReceiving {
195210 pinnedToBottom = true
211+ scrollToBottom ( )
196212 }
197213 }
198214 . onChange ( of: viewStore. state. lastMessage) { _ in
@@ -665,41 +681,6 @@ struct RoundedCorners: Shape {
665681 }
666682}
667683
668- struct GlobalChatSwitchToggleStyle : ToggleStyle {
669- func makeBody( configuration: Configuration ) -> some View {
670- HStack ( spacing: 4 ) {
671- Text ( configuration. isOn ? " Shared Conversation " : " Local Conversation " )
672- . foregroundStyle ( . tertiary)
673-
674- RoundedRectangle ( cornerRadius: 10 , style: . circular)
675- . foregroundColor ( configuration. isOn ? Color . indigo : . gray. opacity ( 0.5 ) )
676- . frame ( width: 30 , height: 20 , alignment: . center)
677- . overlay (
678- Circle ( )
679- . fill ( . regularMaterial)
680- . padding ( . all, 2 )
681- . overlay (
682- Image (
683- systemName: configuration
684- . isOn ? " globe " : " doc.circle "
685- )
686- . resizable ( )
687- . aspectRatio ( contentMode: . fit)
688- . frame ( width: 12 , height: 12 , alignment: . center)
689- . foregroundStyle ( . secondary)
690- )
691- . offset ( x: configuration. isOn ? 5 : - 5 , y: 0 )
692- . animation ( . linear( duration: 0.1 ) , value: configuration. isOn)
693- )
694- . onTapGesture { configuration. isOn. toggle ( ) }
695- . overlay {
696- RoundedRectangle ( cornerRadius: 10 , style: . circular)
697- . stroke ( . black. opacity ( 0.2 ) , lineWidth: 1 )
698- }
699- }
700- }
701- }
702-
703684// MARK: - Previews
704685
705686struct ChatPanel_Preview : PreviewProvider {
0 commit comments