Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 94 additions & 41 deletions Bitkit/Views/HomeScreen.swift
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import SwiftUI

struct HomeScreen: View {
@EnvironmentObject var activity: ActivityListViewModel
@EnvironmentObject var app: AppViewModel
@EnvironmentObject var settings: SettingsViewModel
@EnvironmentObject var wallet: WalletViewModel

@State private var scrollPosition: Int? = 0
@State private var isEditingWidgets = false

private var hasActivity: Bool {
return activity.latestActivities?.isEmpty == false
}
/// Overlay visibility is isolated via `@Observable` + `.environment` so toggling it does not
/// invalidate `HomeScreen`'s body (which would reset the scroll view's refresh layout).
@State private var pullRefreshIndicator = HomePullRefreshIndicator()

private var currentPage: Int {
scrollPosition ?? 0
Expand All @@ -21,41 +18,11 @@ struct HomeScreen: View {
ZStack(alignment: .top) {
Header(showWidgetEditButton: currentPage == 1, isEditingWidgets: $isEditingWidgets)

GeometryReader { geometry in
ScrollView(showsIndicators: false) {
LazyVStack {
HomeWalletView()
.frame(height: geometry.size.height)
.id(0)

if settings.showWidgets {
HomeWidgetsView(isEditingWidgets: $isEditingWidgets)
.frame(height: geometry.size.height)
.id(1)
}
}
.scrollTargetLayout()
}
.scrollTargetBehavior(.paging)
.scrollPosition(id: $scrollPosition)
.onChange(of: scrollPosition) { _, newValue in
// Dismiss this hint after the user has seen it and scrolls to widgets
if hasActivity, newValue == 1 {
app.hasDismissedWidgetsOnboardingHint = true
}
}
.refreshable {
guard currentPage == 0 else { return }
guard wallet.nodeLifecycleState == .running else { return }
do {
try await wallet.sync()
try await activity.syncLdkNodePayments()
} catch {
app.toast(error)
}
}
}
.ignoresSafeArea()
HomeScreenScrollContent(
scrollPosition: $scrollPosition,
isEditingWidgets: $isEditingWidgets,
refreshIndicator: pullRefreshIndicator
)

// Top and bottom gradients
VStack(spacing: 0) {
Expand All @@ -77,6 +44,9 @@ struct HomeScreen: View {
}
.ignoresSafeArea()
.allowsHitTesting(false)

HomePullRefreshOverlay()
.environment(pullRefreshIndicator)
}
.navigationBarHidden(true)
.onAppear {
Expand All @@ -87,3 +57,86 @@ struct HomeScreen: View {
}
}
}

// MARK: - Pull-to-refresh overlay (isolated observation)

@MainActor
@Observable
private final class HomePullRefreshIndicator {
var isVisible = false
}

private struct HomePullRefreshOverlay: View {
@Environment(HomePullRefreshIndicator.self) private var indicator

var body: some View {
if indicator.isVisible {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .textPrimary))
.padding(.top, ScreenLayout.headerHeight + 16)
.frame(maxWidth: .infinity)
.allowsHitTesting(false)
.transition(.opacity)
.animation(.easeInOut(duration: 0.2), value: indicator.isVisible)
}
}
}

// MARK: - Paged scroll (stable subtree; must not read `HomePullRefreshIndicator.isVisible` in `body`)

private struct HomeScreenScrollContent: View {
@EnvironmentObject private var activity: ActivityListViewModel
@EnvironmentObject private var app: AppViewModel
@EnvironmentObject private var settings: SettingsViewModel
@EnvironmentObject private var wallet: WalletViewModel

@Binding var scrollPosition: Int?
@Binding var isEditingWidgets: Bool
var refreshIndicator: HomePullRefreshIndicator

private var hasActivity: Bool {
return activity.latestActivities?.isEmpty == false
}

var body: some View {
GeometryReader { geometry in
ScrollView(showsIndicators: false) {
LazyVStack {
HomeWalletView()
.frame(height: geometry.size.height)
.id(0)

if settings.showWidgets {
HomeWidgetsView(isEditingWidgets: $isEditingWidgets)
.frame(height: geometry.size.height)
.id(1)
}
}
.scrollTargetLayout()
}
.scrollTargetBehavior(.paging)
.scrollPosition(id: $scrollPosition)
.onChange(of: scrollPosition) { _, newValue in
if hasActivity, newValue == 1 {
app.hasDismissedWidgetsOnboardingHint = true
}
}
.refreshable {
refreshIndicator.isVisible = true
defer { refreshIndicator.isVisible = false }

// guard scrollPosition == 0 else { return }
// guard wallet.nodeLifecycleState == .running else { return }
// do {
// try await wallet.sync()
// try await activity.syncLdkNodePayments()
// } catch {
// app.toast(error)
// }

try? await Task.sleep(for: .seconds(5))
}
}
.ignoresSafeArea()
}
}