2023-01-05 11:21:54 +00:00
|
|
|
import DesignSystem
|
|
|
|
import Env
|
2023-01-17 10:36:01 +00:00
|
|
|
import Models
|
|
|
|
import Network
|
|
|
|
import SwiftUI
|
2023-01-05 11:21:54 +00:00
|
|
|
|
2023-09-19 07:18:20 +00:00
|
|
|
@MainActor
|
2023-01-05 11:21:54 +00:00
|
|
|
public struct ConversationsListView: View {
|
2023-09-19 07:18:20 +00:00
|
|
|
@Environment(UserPreferences.self) private var preferences
|
2023-09-18 05:01:23 +00:00
|
|
|
@Environment(RouterPath.self) private var routerPath
|
|
|
|
@Environment(StreamWatcher.self) private var watcher
|
|
|
|
@Environment(Client.self) private var client
|
2023-09-18 19:03:52 +00:00
|
|
|
@Environment(Theme.self) private var theme
|
2023-01-30 06:27:06 +00:00
|
|
|
|
2023-09-18 05:01:23 +00:00
|
|
|
@State private var viewModel = ConversationsListViewModel()
|
2023-01-30 06:27:06 +00:00
|
|
|
|
2023-10-05 06:22:45 +00:00
|
|
|
@Binding var scrollToTopSignal: Int
|
|
|
|
|
|
|
|
public init(scrollToTopSignal: Binding<Int>) {
|
|
|
|
_scrollToTopSignal = scrollToTopSignal
|
|
|
|
}
|
2023-01-30 06:27:06 +00:00
|
|
|
|
2023-02-26 05:45:57 +00:00
|
|
|
private var conversations: Binding<[Conversation]> {
|
|
|
|
if viewModel.isLoadingFirstPage {
|
2023-09-16 12:15:03 +00:00
|
|
|
Binding.constant(Conversation.placeholders())
|
2023-02-26 05:45:57 +00:00
|
|
|
} else {
|
2023-09-16 12:15:03 +00:00
|
|
|
$viewModel.conversations
|
2023-01-05 11:21:54 +00:00
|
|
|
}
|
2023-02-26 05:45:57 +00:00
|
|
|
}
|
|
|
|
|
2023-01-05 11:21:54 +00:00
|
|
|
public var body: some View {
|
2023-10-05 06:22:45 +00:00
|
|
|
ScrollViewReader { proxy in
|
|
|
|
ScrollView {
|
|
|
|
scrollToTopView
|
|
|
|
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)
|
|
|
|
.allowsHitTesting(false)
|
|
|
|
} else {
|
|
|
|
ConversationsListRow(conversation: $conversation, viewModel: viewModel)
|
|
|
|
.padding(.horizontal, .layoutPadding)
|
|
|
|
}
|
|
|
|
Divider()
|
2023-02-26 05:45:57 +00:00
|
|
|
}
|
2023-10-05 06:22:45 +00:00
|
|
|
} else if conversations.isEmpty, !viewModel.isLoadingFirstPage, !viewModel.isError {
|
2024-02-14 12:34:06 +00:00
|
|
|
PlaceholderView(iconName: "tray",
|
2024-03-11 08:05:52 +00:00
|
|
|
title: "conversations.empty.title",
|
|
|
|
message: "conversations.empty.message")
|
2023-10-05 06:22:45 +00:00
|
|
|
} else if viewModel.isError {
|
|
|
|
ErrorView(title: "conversations.error.title",
|
|
|
|
message: "conversations.error.message",
|
|
|
|
buttonTitle: "conversations.error.button")
|
|
|
|
{
|
|
|
|
Task {
|
|
|
|
await viewModel.fetchConversations()
|
|
|
|
}
|
2023-01-29 07:14:08 +00:00
|
|
|
}
|
2023-01-07 17:01:06 +00:00
|
|
|
}
|
2023-01-30 06:27:06 +00:00
|
|
|
|
2023-10-05 06:22:45 +00:00
|
|
|
if viewModel.nextPage != nil {
|
|
|
|
HStack {
|
|
|
|
Spacer()
|
|
|
|
ProgressView()
|
|
|
|
Spacer()
|
|
|
|
}
|
|
|
|
.onAppear {
|
|
|
|
if !viewModel.isLoadingNextPage {
|
|
|
|
Task {
|
|
|
|
await viewModel.fetchNextPage()
|
|
|
|
}
|
2023-01-29 07:14:08 +00:00
|
|
|
}
|
2023-01-21 14:31:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-10-05 06:22:45 +00:00
|
|
|
.padding(.top, .layoutPadding)
|
2023-01-05 11:21:54 +00:00
|
|
|
}
|
2023-12-19 08:51:20 +00:00
|
|
|
#if !os(visionOS)
|
2023-10-05 06:22:45 +00:00
|
|
|
.scrollContentBackground(.hidden)
|
|
|
|
.background(theme.primaryBackgroundColor)
|
2023-12-19 08:51:20 +00:00
|
|
|
#endif
|
2023-10-05 06:22:45 +00:00
|
|
|
.navigationTitle("conversations.navigation-title")
|
|
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
|
|
.onChange(of: watcher.latestEvent?.id) {
|
|
|
|
if let latestEvent = watcher.latestEvent {
|
|
|
|
viewModel.handleEvent(event: latestEvent)
|
|
|
|
}
|
2023-01-05 11:21:54 +00:00
|
|
|
}
|
2023-10-05 06:22:45 +00:00
|
|
|
.onChange(of: scrollToTopSignal) {
|
|
|
|
withAnimation {
|
|
|
|
proxy.scrollTo(ScrollToView.Constants.scrollToTop, anchor: .top)
|
|
|
|
}
|
2023-02-10 21:16:03 +00:00
|
|
|
}
|
2023-10-05 06:22:45 +00:00
|
|
|
.refreshable {
|
|
|
|
// note: this Task wrapper should not be necessary, but it reportedly crashes without it
|
|
|
|
// when refreshing on an empty list
|
2023-01-05 11:21:54 +00:00
|
|
|
Task {
|
2023-11-07 10:22:36 +00:00
|
|
|
SoundEffectManager.shared.playSound(.pull)
|
|
|
|
HapticManager.shared.fireHaptic(.dataRefresh(intensity: 0.3))
|
2023-01-05 11:21:54 +00:00
|
|
|
await viewModel.fetchConversations()
|
2023-11-07 10:22:36 +00:00
|
|
|
HapticManager.shared.fireHaptic(.dataRefresh(intensity: 0.7))
|
|
|
|
SoundEffectManager.shared.playSound(.refresh)
|
2023-10-05 06:22:45 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
.onAppear {
|
|
|
|
viewModel.client = client
|
|
|
|
if client.isAuth {
|
|
|
|
Task {
|
|
|
|
await viewModel.fetchConversations()
|
|
|
|
}
|
2023-01-05 11:21:54 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-10-05 06:22:45 +00:00
|
|
|
|
|
|
|
private var scrollToTopView: some View {
|
|
|
|
ScrollToView()
|
|
|
|
.frame(height: .scrollToViewHeight)
|
|
|
|
.onAppear {
|
|
|
|
viewModel.scrollToTopVisible = true
|
|
|
|
}
|
|
|
|
.onDisappear {
|
|
|
|
viewModel.scrollToTopVisible = false
|
|
|
|
}
|
|
|
|
}
|
2023-01-05 11:21:54 +00:00
|
|
|
}
|