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