Status detail: Switch to List container + refactor to something much better

This commit is contained in:
Thomas Ricouard 2023-02-10 18:21:05 +01:00
parent 06120974fa
commit feefb02456
9 changed files with 164 additions and 83 deletions

View file

@ -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):

View file

@ -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)

View file

@ -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: [])
}
}

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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 {

View file

@ -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

View file

@ -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))
}
}
}