Skip to content

Commit 121d173

Browse files
committed
Migrate SuggestionWidget to latest TCA
1 parent 88cfef7 commit 121d173

18 files changed

Lines changed: 648 additions & 654 deletions

Core/Sources/SuggestionWidget/ChatPanelWindow.swift

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import AppKit
22
import ChatTab
3-
import Combine
43
import ComposableArchitecture
54
import Foundation
65
import SwiftUI
@@ -9,7 +8,7 @@ final class ChatPanelWindow: NSWindow {
98
override var canBecomeKey: Bool { true }
109
override var canBecomeMain: Bool { true }
1110

12-
private var cancellable: Set<AnyCancellable> = []
11+
private let storeObserver = NSObject()
1312

1413
var minimizeWindow: () -> Void = {}
1514

@@ -60,18 +59,17 @@ final class ChatPanelWindow: NSWindow {
6059
setIsVisible(true)
6160
isPanelDisplayed = false
6261

63-
let viewStore = ViewStore(store)
64-
viewStore.publisher
65-
.map(\.isDetached)
66-
.receive(on: DispatchQueue.main)
67-
.sink { [weak self] isDetached in
68-
guard let self else { return }
62+
storeObserver.observe { [weak self] in
63+
guard let self else { return }
64+
let isDetached = store.isDetached
65+
Task { @MainActor in
6966
if UserDefaults.shared.value(for: \.disableFloatOnTopWhenTheChatPanelIsDetached) {
7067
self.setFloatOnTop(!isDetached)
7168
} else {
7269
self.setFloatOnTop(true)
7370
}
74-
}.store(in: &cancellable)
71+
}
72+
}
7573
}
7674

7775
func setFloatOnTop(_ isFloatOnTop: Bool) {

Core/Sources/SuggestionWidget/ChatWindowView.swift

Lines changed: 110 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,9 @@ struct ChatWindowView: View {
1111
let store: StoreOf<ChatPanelFeature>
1212
let toggleVisibility: (Bool) -> Void
1313

14-
struct OverallState: Equatable {
15-
var isPanelDisplayed: Bool
16-
var colorScheme: ColorScheme
17-
var selectedTabId: String?
18-
}
19-
2014
var body: some View {
21-
WithViewStore(
22-
store,
23-
observe: {
24-
OverallState(
25-
isPanelDisplayed: $0.isPanelDisplayed,
26-
colorScheme: $0.colorScheme,
27-
selectedTabId: $0.chatTabGroup.selectedTabId
28-
)
29-
}
30-
) { viewStore in
15+
WithPerceptionTracking {
16+
let _ = store.chatTabGroup.selectedTabId // force re-evaluation
3117
VStack(spacing: 0) {
3218
Rectangle().fill(.regularMaterial).frame(height: 28)
3319

@@ -43,10 +29,10 @@ struct ChatWindowView: View {
4329
}
4430
.xcodeStyleFrame(cornerRadius: 10)
4531
.ignoresSafeArea(edges: .top)
46-
.onChange(of: viewStore.state.isPanelDisplayed) { isDisplayed in
32+
.onChange(of: store.isPanelDisplayed) { isDisplayed in
4733
toggleVisibility(isDisplayed)
4834
}
49-
.preferredColorScheme(viewStore.state.colorScheme)
35+
.preferredColorScheme(store.colorScheme)
5036
}
5137
}
5238
}
@@ -56,33 +42,33 @@ struct ChatTitleBar: View {
5642
@State var isHovering = false
5743

5844
var body: some View {
59-
HStack(spacing: 6) {
60-
Button(action: {
61-
store.send(.closeActiveTabClicked)
62-
}) {
63-
EmptyView()
64-
}
65-
.opacity(0)
66-
.keyboardShortcut("w", modifiers: [.command])
45+
WithPerceptionTracking {
46+
HStack(spacing: 6) {
47+
Button(action: {
48+
store.send(.closeActiveTabClicked)
49+
}) {
50+
EmptyView()
51+
}
52+
.opacity(0)
53+
.keyboardShortcut("w", modifiers: [.command])
6754

68-
Button(
69-
action: {
70-
store.send(.hideButtonClicked)
55+
Button(
56+
action: {
57+
store.send(.hideButtonClicked)
58+
}
59+
) {
60+
Image(systemName: "minus")
61+
.foregroundStyle(.black.opacity(0.5))
62+
.font(Font.system(size: 8).weight(.heavy))
7163
}
72-
) {
73-
Image(systemName: "minus")
74-
.foregroundStyle(.black.opacity(0.5))
75-
.font(Font.system(size: 8).weight(.heavy))
76-
}
77-
.opacity(0)
78-
.keyboardShortcut("m", modifiers: [.command])
64+
.opacity(0)
65+
.keyboardShortcut("m", modifiers: [.command])
7966

80-
Spacer()
67+
Spacer()
8168

82-
WithViewStore(store, observe: { $0.isDetached }) { viewStore in
8369
TrafficLightButton(
8470
isHovering: isHovering,
85-
isActive: viewStore.state,
71+
isActive: store.isDetached,
8672
color: Color(nsColor: .systemCyan),
8773
action: {
8874
store.send(.toggleChatPanelDetachedButtonClicked)
@@ -94,12 +80,12 @@ struct ChatTitleBar: View {
9480
.transformEffect(.init(translationX: 0, y: 0.5))
9581
}
9682
}
83+
.buttonStyle(.plain)
84+
.padding(.trailing, 8)
85+
.onHover(perform: { hovering in
86+
isHovering = hovering
87+
})
9788
}
98-
.buttonStyle(.plain)
99-
.padding(.trailing, 8)
100-
.onHover(perform: { hovering in
101-
isHovering = hovering
102-
})
10389
}
10490

10591
struct TrafficLightButton<Icon: View>: View {
@@ -157,30 +143,44 @@ struct ChatTabBar: View {
157143
var selectedTabId: String
158144
}
159145

160-
@Environment(\.chatTabPool) var chatTabPool
161-
@State var draggingTabId: String?
162-
163146
var body: some View {
164-
WithViewStore(
165-
store,
166-
observe: { TabBarState(
167-
tabInfo: $0.chatTabGroup.tabInfo,
168-
selectedTabId: $0.chatTabGroup.selectedTabId
169-
?? $0.chatTabGroup.tabInfo.first?.id ?? ""
170-
) }
171-
) { viewStore in
172-
HStack(spacing: 0) {
147+
HStack(spacing: 0) {
148+
Divider()
149+
Tabs(store: store)
150+
CreateButton(store: store)
151+
}
152+
.background {
153+
Button(action: { store.send(.switchToNextTab) }) { EmptyView() }
154+
.opacity(0)
155+
.keyboardShortcut("]", modifiers: [.command, .shift])
156+
Button(action: { store.send(.switchToPreviousTab) }) { EmptyView() }
157+
.opacity(0)
158+
.keyboardShortcut("[", modifiers: [.command, .shift])
159+
}
160+
}
161+
162+
struct Tabs: View {
163+
let store: StoreOf<ChatPanelFeature>
164+
@State var draggingTabId: String?
165+
@Environment(\.chatTabPool) var chatTabPool
166+
167+
var body: some View {
168+
WithPerceptionTracking {
169+
let tabInfo = store.chatTabGroup.tabInfo
170+
let selectedTabId = store.chatTabGroup.selectedTabId
171+
?? store.chatTabGroup.tabInfo.first?.id
172+
?? ""
173173
ScrollViewReader { proxy in
174174
ScrollView(.horizontal) {
175175
HStack(spacing: 0) {
176-
ForEach(viewStore.state.tabInfo, id: \.id) { info in
176+
ForEach(tabInfo, id: \.id) { info in
177177
if let tab = chatTabPool.getTab(of: info.id) {
178178
ChatTabBarButton(
179179
store: store,
180180
info: info,
181181
content: { tab.tabItem },
182182
icon: { tab.icon },
183-
isSelected: info.id == viewStore.state.selectedTabId
183+
isSelected: info.id == selectedTabId
184184
)
185185
.contextMenu {
186186
tab.menu
@@ -194,7 +194,7 @@ struct ChatTabBar: View {
194194
of: [.text],
195195
delegate: ChatTabBarDropDelegate(
196196
store: store,
197-
tabs: viewStore.state.tabInfo,
197+
tabs: tabInfo,
198198
itemId: info.id,
199199
draggingTabId: $draggingTabId
200200
)
@@ -207,72 +207,61 @@ struct ChatTabBar: View {
207207
}
208208
}
209209
.hideScrollIndicator()
210-
.onChange(of: viewStore.selectedTabId) { id in
210+
.onChange(of: selectedTabId) { id in
211211
withAnimation(.easeInOut(duration: 0.2)) {
212212
proxy.scrollTo(id)
213213
}
214214
}
215215
}
216-
217-
Divider()
218-
219-
createButton
220216
}
221217
}
222-
.background {
223-
Button(action: { store.send(.switchToNextTab) }) { EmptyView() }
224-
.opacity(0)
225-
.keyboardShortcut("]", modifiers: [.command, .shift])
226-
Button(action: { store.send(.switchToPreviousTab) }) { EmptyView() }
227-
.opacity(0)
228-
.keyboardShortcut("[", modifiers: [.command, .shift])
229-
}
230218
}
231219

232-
@ViewBuilder
233-
var createButton: some View {
234-
Menu {
235-
WithViewStore(store, observe: { $0.chatTabGroup.tabCollection }) { viewStore in
236-
ForEach(0..<viewStore.state.endIndex, id: \.self) { index in
237-
switch viewStore.state[index] {
238-
case let .kind(kind):
239-
Button(action: {
240-
store.send(.createNewTapButtonClicked(kind: kind))
241-
}) {
242-
Text(kind.title)
243-
}.disabled(kind.builder is DisabledChatTabBuilder)
244-
case let .folder(title, list):
245-
Menu {
246-
ForEach(0..<list.endIndex, id: \.self) { index in
247-
Button(action: {
248-
store
249-
.send(
250-
.createNewTapButtonClicked(
251-
kind: list[index]
252-
)
220+
struct CreateButton: View {
221+
let store: StoreOf<ChatPanelFeature>
222+
223+
var body: some View {
224+
WithPerceptionTracking {
225+
let collection = store.chatTabGroup.tabCollection
226+
Menu {
227+
ForEach(0..<collection.endIndex, id: \.self) { index in
228+
switch collection[index] {
229+
case let .kind(kind):
230+
Button(action: {
231+
store.send(.createNewTapButtonClicked(kind: kind))
232+
}) {
233+
Text(kind.title)
234+
}.disabled(kind.builder is DisabledChatTabBuilder)
235+
case let .folder(title, list):
236+
Menu {
237+
ForEach(0..<list.endIndex, id: \.self) { index in
238+
Button(action: {
239+
store.send(
240+
.createNewTapButtonClicked(kind: list[index])
253241
)
254-
}) {
255-
Text(list[index].title)
242+
}) {
243+
Text(list[index].title)
244+
}
256245
}
246+
} label: {
247+
Text(title)
257248
}
258-
} label: {
259-
Text(title)
260249
}
261250
}
251+
} label: {
252+
Image(systemName: "plus")
253+
} primaryAction: {
254+
store.send(.createNewTapButtonClicked(kind: nil))
255+
}
256+
.foregroundColor(.secondary)
257+
.menuStyle(.borderedButton)
258+
.padding(.horizontal, 4)
259+
.fixedSize(horizontal: true, vertical: false)
260+
.onHover { isHovering in
261+
if isHovering {
262+
store.send(.createNewTapButtonHovered)
263+
}
262264
}
263-
}
264-
} label: {
265-
Image(systemName: "plus")
266-
} primaryAction: {
267-
store.send(.createNewTapButtonClicked(kind: nil))
268-
}
269-
.foregroundColor(.secondary)
270-
.menuStyle(.borderedButton)
271-
.padding(.horizontal, 4)
272-
.fixedSize(horizontal: true, vertical: false)
273-
.onHover { isHovering in
274-
if isHovering {
275-
store.send(.createNewTapButtonHovered)
276265
}
277266
}
278267
}
@@ -349,32 +338,22 @@ struct ChatTabBarButton<Content: View, Icon: View>: View {
349338

350339
struct ChatTabContainer: View {
351340
let store: StoreOf<ChatPanelFeature>
352-
353-
struct TabContainerState: Equatable {
354-
var tabInfo: IdentifiedArray<String, ChatTabInfo>
355-
var selectedTabId: String?
356-
}
357-
358341
@Environment(\.chatTabPool) var chatTabPool
359342

360343
var body: some View {
361-
WithViewStore(
362-
store,
363-
observe: {
364-
TabContainerState(
365-
tabInfo: $0.chatTabGroup.tabInfo,
366-
selectedTabId: $0.chatTabGroup.selectedTabId
367-
?? $0.chatTabGroup.tabInfo.first?.id ?? ""
368-
)
369-
}
370-
) { viewStore in
344+
WithPerceptionTracking {
345+
let tabInfo = store.chatTabGroup.tabInfo
346+
let selectedTabId = store.chatTabGroup.selectedTabId
347+
?? store.chatTabGroup.tabInfo.first?.id
348+
?? ""
349+
371350
ZStack {
372-
if viewStore.state.tabInfo.isEmpty {
351+
if tabInfo.isEmpty {
373352
Text("Empty")
374353
} else {
375-
ForEach(viewStore.state.tabInfo) { tabInfo in
354+
ForEach(tabInfo) { tabInfo in
376355
if let tab = chatTabPool.getTab(of: tabInfo.id) {
377-
let isActive = tab.id == viewStore.state.selectedTabId
356+
let isActive = tab.id == selectedTabId
378357
tab.body
379358
.opacity(isActive ? 1 : 0)
380359
.disabled(!isActive)
@@ -428,12 +407,12 @@ struct ChatWindowView_Previews: PreviewProvider {
428407
.init(id: "5", title: "Empty-5"),
429408
.init(id: "6", title: "Empty-6"),
430409
.init(id: "7", title: "Empty-7"),
431-
],
410+
] as IdentifiedArray<String, ChatTabInfo>,
432411
selectedTabId: "2"
433412
),
434413
isPanelDisplayed: true
435414
),
436-
reducer: ChatPanelFeature()
415+
reducer: { ChatPanelFeature() }
437416
)
438417
}
439418

0 commit comments

Comments
 (0)