import DesignSystem import Env import Models import Network import Shimmer import SwiftUI public struct ConversationsListView: View { @EnvironmentObject private var preferences: UserPreferences @EnvironmentObject private var routerPath: RouterPath @EnvironmentObject private var watcher: StreamWatcher @EnvironmentObject private var client: Client @EnvironmentObject private var theme: Theme @StateObject private var viewModel = ConversationsListViewModel() public init() {} private var conversations: Binding<[Conversation]> { if viewModel.isLoadingFirstPage { return Binding.constant(Conversation.placeholders()) } else { return $viewModel.conversations } } public var body: some View { ScrollView { LazyVStack { Group { if !conversations.isEmpty || viewModel.isLoadingFirstPage { ForEach(conversations) { $conversation in if viewModel.isLoadingFirstPage { ConversationsListRow(conversation: $conversation, viewModel: viewModel) .padding(.horizontal, .layoutPadding) .redacted(reason: .placeholder) } else { ConversationsListRow(conversation: $conversation, viewModel: viewModel) .padding(.horizontal, .layoutPadding) } Divider() } } else if conversations.isEmpty && !viewModel.isLoadingFirstPage && !viewModel.isError { EmptyView(iconName: "tray", title: "conversations.empty.title", message: "conversations.empty.message") } else if viewModel.isError { ErrorView(title: "conversations.error.title", message: "conversations.error.message", buttonTitle: "conversations.error.button") { Task { await viewModel.fetchConversations() } } } if viewModel.nextPage != nil { HStack { Spacer() ProgressView() Spacer() } .onAppear { if !viewModel.isLoadingNextPage { Task { await viewModel.fetchNextPage() } } } } } } .padding(.top, .layoutPadding) } .scrollContentBackground(.hidden) .background(theme.primaryBackgroundColor) .navigationTitle("conversations.navigation-title") .navigationBarTitleDisplayMode(.inline) .toolbar { StatusEditorToolbarItem(visibility: .direct) if UIDevice.current.userInterfaceIdiom == .pad && !preferences.showiPadSecondaryColumn { SecondaryColumnToolbarItem() } } .onChange(of: watcher.latestEvent?.id) { _ in if let latestEvent = watcher.latestEvent { viewModel.handleEvent(event: latestEvent) } } .refreshable { // note: this Task wrapper should not be necessary, but it reportedly crashes without it // when refreshing on an empty list Task { SoundEffectManager.shared.playSound(of: .pull) HapticManager.shared.fireHaptic(of: .dataRefresh(intensity: 0.3)) await viewModel.fetchConversations() HapticManager.shared.fireHaptic(of: .dataRefresh(intensity: 0.7)) SoundEffectManager.shared.playSound(of: .refresh) } } .onAppear { viewModel.client = client if client.isAuth { Task { await viewModel.fetchConversations() } } } } }