Skip to content

Commit e6940a5

Browse files
committed
Merge branch 'feature/use-scroll-event-to-disable-pin-to-bottom' into develop
2 parents 6938c9f + 309c70f commit e6940a5

File tree

1 file changed

+33
-52
lines changed

1 file changed

+33
-52
lines changed

Core/Sources/ChatGPTChatTab/ChatPanel.swift

Lines changed: 33 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import AppKit
2+
import Combine
23
import ComposableArchitecture
34
import MarkdownUI
45
import OpenAIService
@@ -40,12 +41,15 @@ private struct ListHeightPreferenceKey: PreferenceKey {
4041

4142
struct 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

705686
struct ChatPanel_Preview: PreviewProvider {

0 commit comments

Comments
 (0)