From 092a264f63041070eeb8bf9cbea19010f91d23ee Mon Sep 17 00:00:00 2001 From: Amandeep Grewal Date: Wed, 10 Dec 2025 04:53:59 -0500 Subject: [PATCH 1/3] sticky_top_bottom --- .../NetworkInspectorSidebarList.swift | 170 +++++++++++++----- 1 file changed, 127 insertions(+), 43 deletions(-) diff --git a/snapo-app-mac/Snap-O/NetworkInspector/NetworkInspectorSidebarList.swift b/snapo-app-mac/Snap-O/NetworkInspector/NetworkInspectorSidebarList.swift index a0be072..2b39986 100644 --- a/snapo-app-mac/Snap-O/NetworkInspector/NetworkInspectorSidebarList.swift +++ b/snapo-app-mac/Snap-O/NetworkInspector/NetworkInspectorSidebarList.swift @@ -7,65 +7,68 @@ struct NetworkInspectorSidebarList: View { let filteredItems: [NetworkInspectorListItemViewModel] let selectedServer: NetworkInspectorServerViewModel? @Binding var selectedItem: NetworkInspectorItemID? + @State private var isScrolledToTop = true + @State private var isScrolledToBottom = true var body: some View { - List(selection: $selectedItem) { - if items.isEmpty { - Text("No activity yet") - .foregroundStyle(.secondary) - } else if serverScopedItems.isEmpty { - Text(statusPlaceholder) - .foregroundStyle(.secondary) - } else if filteredItems.isEmpty { - Text("No matches") - .foregroundStyle(.secondary) - } else { - ForEach(filteredItems) { item in - VStack(alignment: .leading, spacing: 6) { - HStack(alignment: .center, spacing: 8) { - VStack(alignment: .leading) { - Text(item.primaryPathComponent) - .font(.subheadline.weight(.medium)) - .lineLimit(1) - if !item.secondaryPath.isEmpty { - Text(item.secondaryPath) - .font(.caption) - .foregroundStyle(.secondary) + ScrollViewReader { proxy in + List(selection: $selectedItem) { + if let placeholder = placeholderText { + placeholderRow(placeholder) + } else { + ForEach(filteredItems) { item in + VStack(alignment: .leading, spacing: 6) { + HStack(alignment: .center, spacing: 8) { + VStack(alignment: .leading) { + Text(item.primaryPathComponent) + .font(.subheadline.weight(.medium)) .lineLimit(1) - .truncationMode(.middle) + if !item.secondaryPath.isEmpty { + Text(item.secondaryPath) + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(1) + .truncationMode(.middle) + } } - } - Spacer() + Spacer() - Text(item.method) - .font(.system(.caption, design: .monospaced)) - .bold() - .foregroundStyle(.secondary) + Text(item.method) + .font(.system(.caption, design: .monospaced)) + .bold() + .foregroundStyle(.secondary) - statusView(for: item) - } - } - .contentShape(Rectangle()) - .contextMenu { - if case .request(let request) = item.kind { - Button("Copy URL") { - NetworkInspectorCopyExporter.copyURL(request.url) + statusView(for: item) } + } + .contentShape(Rectangle()) + .contextMenu { + if case .request(let request) = item.kind { + Button("Copy URL") { + NetworkInspectorCopyExporter.copyURL(request.url) + } - Button("Copy as cURL") { - if let model = store.requestViewModel(for: request.id) { - NetworkInspectorCopyExporter.copyCurl(for: model) + Button("Copy as cURL") { + if let model = store.requestViewModel(for: request.id) { + NetworkInspectorCopyExporter.copyCurl(for: model) + } } } } + .tag(item.id) + .background(alignment: .topLeading) { + edgeVisibilityMarkers(for: item.id) + } } - .tag(item.id) } } + .listStyle(.sidebar) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .onChange(of: filteredItems.map(\.id)) { previous, current in + handleListChange(previous: previous, current: current, proxy: proxy) + } } - .listStyle(.sidebar) - .frame(maxWidth: .infinity, maxHeight: .infinity) } @ViewBuilder @@ -116,10 +119,91 @@ struct NetworkInspectorSidebarList: View { } private extension NetworkInspectorSidebarList { + var firstItemID: NetworkInspectorItemID? { + filteredItems.first?.id + } + + var lastItemID: NetworkInspectorItemID? { + filteredItems.last?.id + } + + var isShowingItems: Bool { + !items.isEmpty && !serverScopedItems.isEmpty && !filteredItems.isEmpty + } + + var placeholderText: String? { + if items.isEmpty { return "No activity yet" } + if serverScopedItems.isEmpty { return statusPlaceholder } + if filteredItems.isEmpty { return "No matches" } + return nil + } + var statusPlaceholder: String { if selectedServer?.hasHello == true { return "No activity for this app yet" } return "Waiting for connection…" } + + @ViewBuilder + func placeholderRow(_ text: String) -> some View { + Text(text) + .foregroundStyle(.secondary) + .onAppear(perform: resetEdgePositions) + } + + @ViewBuilder + func edgeVisibilityMarkers(for id: NetworkInspectorItemID) -> some View { + if id == firstItemID { + EdgeVisibilityDetector { isScrolledToTop = $0 } + } + + if id == lastItemID { + EdgeVisibilityDetector { isScrolledToBottom = $0 } + } + } + + func handleListChange( + previous: [NetworkInspectorItemID], + current: [NetworkInspectorItemID], + proxy: ScrollViewProxy + ) { + guard isShowingItems, !current.isEmpty, + !Set(current).subtracting(previous).isEmpty + else { return } + + let anchor: UnitPoint + let targetID: NetworkInspectorItemID? + + switch store.listSortOrder { + case .newestFirst where isScrolledToTop: + anchor = .top + targetID = current.first + case .oldestFirst where isScrolledToBottom: + anchor = .bottom + targetID = current.last + default: + return + } + + if let targetID { + proxy.scrollTo(targetID, anchor: anchor) + } + } + + func resetEdgePositions() { + isScrolledToTop = true + isScrolledToBottom = true + } +} + +private struct EdgeVisibilityDetector: View { + var onVisibilityChange: (Bool) -> Void + + var body: some View { + Color.clear + .frame(width: 1, height: 1) + .onAppear { onVisibilityChange(true) } + .onDisappear { onVisibilityChange(false) } + } } From 3dd7c4cd3f03ccc84ec91d6f24a85b4a17ef2917 Mon Sep 17 00:00:00 2001 From: Amandeep Grewal Date: Wed, 10 Dec 2025 05:16:42 -0500 Subject: [PATCH 2/3] codex's fixes --- .../NetworkInspectorSidebarList.swift | 11 +++-------- .../NetworkInspector/NetworkInspectorStore.swift | 8 +++++++- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/snapo-app-mac/Snap-O/NetworkInspector/NetworkInspectorSidebarList.swift b/snapo-app-mac/Snap-O/NetworkInspector/NetworkInspectorSidebarList.swift index 2b39986..9ff32cb 100644 --- a/snapo-app-mac/Snap-O/NetworkInspector/NetworkInspectorSidebarList.swift +++ b/snapo-app-mac/Snap-O/NetworkInspector/NetworkInspectorSidebarList.swift @@ -8,7 +8,7 @@ struct NetworkInspectorSidebarList: View { let selectedServer: NetworkInspectorServerViewModel? @Binding var selectedItem: NetworkInspectorItemID? @State private var isScrolledToTop = true - @State private var isScrolledToBottom = true + @State private var isScrolledToBottom = false var body: some View { ScrollViewReader { proxy in @@ -65,8 +65,8 @@ struct NetworkInspectorSidebarList: View { } .listStyle(.sidebar) .frame(maxWidth: .infinity, maxHeight: .infinity) - .onChange(of: filteredItems.map(\.id)) { previous, current in - handleListChange(previous: previous, current: current, proxy: proxy) + .onChange(of: filteredItems) { previous, current in + handleListChange(previous: previous.map(\.id), current: current.map(\.id), proxy: proxy) } } } @@ -149,7 +149,6 @@ private extension NetworkInspectorSidebarList { func placeholderRow(_ text: String) -> some View { Text(text) .foregroundStyle(.secondary) - .onAppear(perform: resetEdgePositions) } @ViewBuilder @@ -191,10 +190,6 @@ private extension NetworkInspectorSidebarList { } } - func resetEdgePositions() { - isScrolledToTop = true - isScrolledToBottom = true - } } private struct EdgeVisibilityDetector: View { diff --git a/snapo-app-mac/Snap-O/NetworkInspector/NetworkInspectorStore.swift b/snapo-app-mac/Snap-O/NetworkInspector/NetworkInspectorStore.swift index 9e55a56..b28538b 100644 --- a/snapo-app-mac/Snap-O/NetworkInspector/NetworkInspectorStore.swift +++ b/snapo-app-mac/Snap-O/NetworkInspector/NetworkInspectorStore.swift @@ -1100,7 +1100,7 @@ struct NetworkInspectorWebSocketSummary: Identifiable { } } -struct NetworkInspectorListItemViewModel: Identifiable { +struct NetworkInspectorListItemViewModel: Identifiable, Equatable { enum Kind { case request(NetworkInspectorRequestSummary) case webSocket(NetworkInspectorWebSocketSummary) @@ -1189,6 +1189,12 @@ struct NetworkInspectorListItemViewModel: Identifiable { } } +extension NetworkInspectorListItemViewModel { + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.id == rhs.id + } +} + enum NetworkInspectorDetailViewModel { case request(NetworkInspectorRequestID) case webSocket(NetworkInspectorWebSocketID) From 4445d601cad00663bef8ccee15a764088d00085d Mon Sep 17 00:00:00 2001 From: Amandeep Grewal Date: Wed, 10 Dec 2025 05:18:42 -0500 Subject: [PATCH 3/3] format --- .../Snap-O/NetworkInspector/NetworkInspectorSidebarList.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/snapo-app-mac/Snap-O/NetworkInspector/NetworkInspectorSidebarList.swift b/snapo-app-mac/Snap-O/NetworkInspector/NetworkInspectorSidebarList.swift index 9ff32cb..c9d3d05 100644 --- a/snapo-app-mac/Snap-O/NetworkInspector/NetworkInspectorSidebarList.swift +++ b/snapo-app-mac/Snap-O/NetworkInspector/NetworkInspectorSidebarList.swift @@ -189,7 +189,6 @@ private extension NetworkInspectorSidebarList { proxy.scrollTo(targetID, anchor: anchor) } } - } private struct EdgeVisibilityDetector: View {