Status detail: Switch to List container + refactor to something much better
This commit is contained in:
parent
06120974fa
commit
feefb02456
|
@ -21,10 +21,12 @@ extension View {
|
|||
AccountSettingsView(account: account, appAccount: appAccount)
|
||||
case let .statusDetail(id):
|
||||
StatusDetailView(statusId: id)
|
||||
case let .conversationDetail(conversation):
|
||||
ConversationDetailView(conversation: conversation)
|
||||
case let .statusDetailWithStatus(status):
|
||||
StatusDetailView(status: status)
|
||||
case let .remoteStatusDetail(url):
|
||||
StatusDetailView(remoteStatusURL: url)
|
||||
case let .conversationDetail(conversation):
|
||||
ConversationDetailView(conversation: conversation)
|
||||
case let .hashTag(tag, accountId):
|
||||
TimelineView(timeline: .constant(.hashtag(tag: tag, accountId: accountId)), scrollToTopSignal: .constant(0))
|
||||
case let .list(list):
|
||||
|
|
|
@ -8,8 +8,9 @@ public enum RouterDestinations: Hashable {
|
|||
case accountDetailWithAccount(account: Account)
|
||||
case accountSettingsWithAccount(account: Account, appAccount: AppAccount)
|
||||
case statusDetail(id: String)
|
||||
case conversationDetail(conversation: Conversation)
|
||||
case statusDetailWithStatus(status: Status)
|
||||
case remoteStatusDetail(url: URL)
|
||||
case conversationDetail(conversation: Conversation)
|
||||
case hashTag(tag: String, account: String?)
|
||||
case list(list: Models.List)
|
||||
case followers(id: String)
|
||||
|
|
|
@ -3,4 +3,8 @@ import Foundation
|
|||
public struct StatusContext: Decodable {
|
||||
public let ancestors: [Status]
|
||||
public let descendants: [Status]
|
||||
|
||||
public static func empty() -> StatusContext {
|
||||
.init(ancestors: [], descendants: [])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import Models
|
|||
import Network
|
||||
import Shimmer
|
||||
import SwiftUI
|
||||
import DesignSystem
|
||||
|
||||
public struct StatusDetailView: View {
|
||||
@EnvironmentObject private var theme: Theme
|
||||
|
@ -14,86 +15,87 @@ public struct StatusDetailView: View {
|
|||
@Environment(\.openURL) private var openURL
|
||||
@StateObject private var viewModel: StatusDetailViewModel
|
||||
@State private var isLoaded: Bool = false
|
||||
|
||||
@State private var statusHeight: CGFloat = 0
|
||||
|
||||
public init(statusId: String) {
|
||||
_viewModel = StateObject(wrappedValue: .init(statusId: statusId))
|
||||
}
|
||||
|
||||
|
||||
public init(status: Status) {
|
||||
_viewModel = StateObject(wrappedValue: .init(status: status))
|
||||
}
|
||||
|
||||
public init(remoteStatusURL: URL) {
|
||||
_viewModel = StateObject(wrappedValue: .init(remoteStatusURL: remoteStatusURL))
|
||||
}
|
||||
|
||||
|
||||
public var body: some View {
|
||||
ScrollViewReader { proxy in
|
||||
ZStack {
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
Group {
|
||||
switch viewModel.state {
|
||||
case .loading:
|
||||
ForEach(Status.placeholders()) { status in
|
||||
StatusRowView(viewModel: .init(status: status, isCompact: false))
|
||||
.padding(.horizontal, .layoutPadding)
|
||||
.redacted(reason: .placeholder)
|
||||
Divider()
|
||||
.padding(.vertical, .dividerPadding)
|
||||
}
|
||||
case let .display(status, context):
|
||||
if !context.ancestors.isEmpty {
|
||||
ForEach(context.ancestors) { ancestor in
|
||||
StatusRowView(viewModel: .init(status: ancestor, isCompact: false))
|
||||
.padding(.horizontal, .layoutPadding)
|
||||
Divider()
|
||||
.padding(.vertical, .dividerPadding)
|
||||
}
|
||||
}
|
||||
StatusRowView(viewModel: .init(status: status,
|
||||
isCompact: false,
|
||||
isFocused: true))
|
||||
.padding(.horizontal, .layoutPadding)
|
||||
.id(status.id)
|
||||
Divider()
|
||||
.padding(.bottom, .dividerPadding * 2)
|
||||
if !context.descendants.isEmpty {
|
||||
ForEach(context.descendants) { descendant in
|
||||
StatusRowView(viewModel: .init(status: descendant, isCompact: false))
|
||||
.padding(.horizontal, .layoutPadding)
|
||||
Divider()
|
||||
.padding(.vertical, .dividerPadding)
|
||||
}
|
||||
}
|
||||
|
||||
case .error:
|
||||
ErrorView(title: "status.error.title",
|
||||
message: "status.error.message",
|
||||
buttonTitle: "action.retry") {
|
||||
Task {
|
||||
await viewModel.fetch()
|
||||
}
|
||||
}
|
||||
GeometryReader { reader in
|
||||
ScrollViewReader { proxy in
|
||||
List {
|
||||
if isLoaded {
|
||||
topPaddingView
|
||||
}
|
||||
|
||||
switch viewModel.state {
|
||||
case .loading:
|
||||
loadingDetailView
|
||||
|
||||
case let .display(status, context):
|
||||
if !context.ancestors.isEmpty {
|
||||
ForEach(context.ancestors) { ancestor in
|
||||
StatusRowView(viewModel: .init(status: ancestor, isCompact: false))
|
||||
}
|
||||
}
|
||||
|
||||
makeCurrentStatusView(status: status)
|
||||
|
||||
if !context.descendants.isEmpty {
|
||||
ForEach(context.descendants) { descendant in
|
||||
StatusRowView(viewModel: .init(status: descendant, isCompact: false))
|
||||
}
|
||||
}
|
||||
|
||||
if !isLoaded {
|
||||
loadingContextView
|
||||
}
|
||||
|
||||
Rectangle()
|
||||
.foregroundColor(theme.secondaryBackgroundColor)
|
||||
.frame(minHeight: reader.frame(in: .local).size.height - statusHeight)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(theme.secondaryBackgroundColor)
|
||||
.listRowInsets(.init())
|
||||
|
||||
case .error:
|
||||
errorView
|
||||
}
|
||||
.padding(.top, .layoutPadding)
|
||||
}
|
||||
.environment(\.defaultMinListRowHeight, 1)
|
||||
.listStyle(.plain)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(theme.primaryBackgroundColor)
|
||||
}
|
||||
.task {
|
||||
guard !isLoaded else { return }
|
||||
isLoaded = true
|
||||
viewModel.client = client
|
||||
let result = await viewModel.fetch()
|
||||
if !result {
|
||||
if let url = viewModel.remoteStatusURL {
|
||||
openURL(url)
|
||||
.onChange(of: viewModel.scrollToId, perform: { scrollToId in
|
||||
if let scrollToId {
|
||||
viewModel.scrollToId = nil
|
||||
proxy.scrollTo(scrollToId, anchor: .top)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
_ = routerPath.path.popLast()
|
||||
})
|
||||
.task {
|
||||
guard !isLoaded else { return }
|
||||
viewModel.client = client
|
||||
let result = await viewModel.fetch()
|
||||
isLoaded = true
|
||||
|
||||
if !result {
|
||||
if let url = viewModel.remoteStatusURL {
|
||||
openURL(url)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
_ = routerPath.path.popLast()
|
||||
}
|
||||
}
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
proxy.scrollTo(viewModel.statusId, anchor: .center)
|
||||
}
|
||||
}
|
||||
.onChange(of: watcher.latestEvent?.id) { _ in
|
||||
guard let lastEvent = watcher.latestEvent else { return }
|
||||
|
@ -103,4 +105,58 @@ public struct StatusDetailView: View {
|
|||
.navigationTitle(viewModel.title)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
||||
private func makeCurrentStatusView(status: Status) -> some View {
|
||||
StatusRowView(viewModel: .init(status: status,
|
||||
isCompact: false,
|
||||
isFocused: true))
|
||||
.overlay {
|
||||
GeometryReader { reader in
|
||||
VStack{}
|
||||
.onAppear {
|
||||
statusHeight = reader.size.height
|
||||
}
|
||||
}
|
||||
}
|
||||
.id(status.id)
|
||||
}
|
||||
|
||||
private var errorView: some View {
|
||||
ErrorView(title: "status.error.title",
|
||||
message: "status.error.message",
|
||||
buttonTitle: "action.retry") {
|
||||
Task {
|
||||
await viewModel.fetch()
|
||||
}
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowSeparator(.hidden)
|
||||
}
|
||||
|
||||
private var loadingDetailView: some View {
|
||||
ForEach(Status.placeholders()) { status in
|
||||
StatusRowView(viewModel: .init(status: status, isCompact: false))
|
||||
.redacted(reason: .placeholder)
|
||||
}
|
||||
}
|
||||
|
||||
private var loadingContextView: some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
ProgressView()
|
||||
Spacer()
|
||||
}
|
||||
.frame(height: 50)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(theme.secondaryBackgroundColor)
|
||||
.listRowInsets(.init())
|
||||
}
|
||||
|
||||
private var topPaddingView: some View {
|
||||
HStack { EmptyView() }
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowInsets(.init())
|
||||
.frame(height: .layoutPadding)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,12 +16,20 @@ class StatusDetailViewModel: ObservableObject {
|
|||
|
||||
@Published var state: State = .loading
|
||||
@Published var title: LocalizedStringKey = ""
|
||||
@Published var scrollToId: String?
|
||||
|
||||
init(statusId: String) {
|
||||
state = .loading
|
||||
self.statusId = statusId
|
||||
remoteStatusURL = nil
|
||||
}
|
||||
|
||||
init(status: Status) {
|
||||
state = .display(status: status, context: .empty())
|
||||
title = "status.post-from-\(status.account.displayNameWithoutEmojis)"
|
||||
statusId = status.id
|
||||
remoteStatusURL = nil
|
||||
}
|
||||
|
||||
init(remoteStatusURL: URL) {
|
||||
state = .loading
|
||||
|
@ -64,8 +72,9 @@ class StatusDetailViewModel: ObservableObject {
|
|||
guard let client, let statusId else { return }
|
||||
do {
|
||||
let data = try await fetchContextData(client: client, statusId: statusId)
|
||||
state = .display(status: data.status, context: data.context)
|
||||
title = "status.post-from-\(data.status.account.displayNameWithoutEmojis)"
|
||||
state = .display(status: data.status, context: data.context)
|
||||
scrollToId = statusId
|
||||
} catch {
|
||||
state = .error(error: error)
|
||||
}
|
||||
|
|
|
@ -24,11 +24,6 @@ public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
|
|||
StatusRowView(viewModel: .init(status: status, isCompact: false))
|
||||
.padding(.horizontal, isEmbdedInList ? 0 : .layoutPadding)
|
||||
.redacted(reason: .placeholder)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowInsets(.init(top: 12,
|
||||
leading: .layoutPadding,
|
||||
bottom: 12,
|
||||
trailing: .layoutPadding))
|
||||
if !isEmbdedInList {
|
||||
Divider()
|
||||
.padding(.vertical, .dividerPadding)
|
||||
|
@ -52,11 +47,6 @@ public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
|
|||
StatusRowView(viewModel: viewModel)
|
||||
.padding(.horizontal, isEmbdedInList ? 0 : .layoutPadding)
|
||||
.id(status.id)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowInsets(.init(top: 12,
|
||||
leading: .layoutPadding,
|
||||
bottom: 12,
|
||||
trailing: .layoutPadding))
|
||||
.onAppear {
|
||||
fetcher.statusDidAppear(status: status)
|
||||
}
|
||||
|
|
|
@ -49,7 +49,9 @@ struct StatusRowDetailView: View {
|
|||
|
||||
if viewModel.favoritesCount > 0 {
|
||||
Divider()
|
||||
NavigationLink(value: RouterDestinations.favoritedBy(id: viewModel.status.id)) {
|
||||
Button {
|
||||
routerPath.navigate(to: .favoritedBy(id: viewModel.status.id))
|
||||
} label: {
|
||||
HStack {
|
||||
Text("status.summary.n-favorites \(viewModel.favoritesCount)")
|
||||
.font(.scaledCallout)
|
||||
|
@ -59,10 +61,13 @@ struct StatusRowDetailView: View {
|
|||
}
|
||||
.frame(height: 20)
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
}
|
||||
if viewModel.reblogsCount > 0 {
|
||||
Divider()
|
||||
NavigationLink(value: RouterDestinations.rebloggedBy(id: viewModel.status.id)) {
|
||||
Button {
|
||||
routerPath.navigate(to: .rebloggedBy(id: viewModel.status.id))
|
||||
} label: {
|
||||
HStack {
|
||||
Text("status.summary.n-boosts \(viewModel.reblogsCount)")
|
||||
.font(.scaledCallout)
|
||||
|
@ -72,6 +77,7 @@ struct StatusRowDetailView: View {
|
|||
}
|
||||
.frame(height: 20)
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
}
|
||||
}
|
||||
.task {
|
||||
|
|
|
@ -93,6 +93,11 @@ public struct StatusRowView: View {
|
|||
leadingSwipeActions
|
||||
}
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowInsets(.init(top: 12,
|
||||
leading: .layoutPadding,
|
||||
bottom: 12,
|
||||
trailing: .layoutPadding))
|
||||
.accessibilityElement(children: viewModel.isFocused ? .contain : .combine)
|
||||
.accessibilityActions {
|
||||
accesibilityActions
|
||||
|
@ -331,8 +336,12 @@ public struct StatusRowView: View {
|
|||
contextMenu
|
||||
} label: {
|
||||
Image(systemName: "ellipsis")
|
||||
.foregroundColor(.gray)
|
||||
.frame(width: 20, height: 30)
|
||||
}
|
||||
.menuStyle(.borderlessButton)
|
||||
.foregroundColor(.gray)
|
||||
.contentShape(Rectangle())
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
|
|
@ -97,7 +97,11 @@ public class StatusRowViewModel: ObservableObject {
|
|||
if isRemote, let url = URL(string: status.reblog?.url ?? status.url ?? "") {
|
||||
routerPath.navigate(to: .remoteStatusDetail(url: url))
|
||||
} else {
|
||||
routerPath.navigate(to: .statusDetail(id: status.reblog?.id ?? status.id))
|
||||
if let id = status.reblog?.id {
|
||||
routerPath.navigate(to: .statusDetail(id: id))
|
||||
} else {
|
||||
routerPath.navigate(to: .statusDetailWithStatus(status: status))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue