Simply detail view

This commit is contained in:
Thomas Ricouard 2023-09-17 14:43:01 +02:00
parent c0bfb94f97
commit d1d7efda3a
4 changed files with 116 additions and 108 deletions

View file

@ -29,6 +29,10 @@ private struct IsStatusFocused: EnvironmentKey {
static let defaultValue: Bool = false
}
private struct IsStatusReplyToPrevious: EnvironmentKey {
static let defaultValue: Bool = false
}
public extension EnvironmentValues {
var isSecondaryColumn: Bool {
get { self[SecondaryColumnKey.self] }
@ -64,4 +68,9 @@ public extension EnvironmentValues {
get { self[IsStatusFocused.self] }
set { self[IsStatusFocused.self] = newValue }
}
var isStatusReplyToPrevious: Bool {
get { self[IsStatusReplyToPrevious.self] }
set { self[IsStatusReplyToPrevious.self] = newValue }
}
}

View file

@ -103,67 +103,32 @@ public struct StatusDetailView: View {
private func makeStatusesListView(statuses: [Status], date _: Date) -> some View {
ForEach(statuses) { status in
var isReplyToPrevious: Bool = false
if let index = statuses.firstIndex(where: { $0.id == status.id }),
index > 0,
statuses[index - 1].id == status.inReplyToId
{
if index == 1, statuses.count > 2 {
let nextStatus = statuses[2]
isReplyToPrevious = nextStatus.inReplyToId == status.id
} else if statuses.count == 2 {
isReplyToPrevious = false
} else {
isReplyToPrevious = true
}
}
let isReplyToPrevious = viewModel.isReplyToPreviousCache[status.id] ?? false
let viewModel: StatusRowViewModel = .init(status: status,
client: client,
routerPath: routerPath)
return HStack(spacing: 0) {
if isReplyToPrevious {
Rectangle()
.fill(theme.tintColor)
.frame(width: 2)
.accessibilityHidden(true)
Spacer(minLength: 8)
}
if self.viewModel.statusId == status.id {
makeCurrentStatusView(status: status)
.environment(\.extraLeadingInset, isReplyToPrevious ? 10 : 0)
} else {
StatusRowView(viewModel: viewModel)
.environment(\.extraLeadingInset, isReplyToPrevious ? 10 : 0)
}
}
.listRowBackground(viewModel.highlightRowColor)
.listRowInsets(.init(top: 12,
leading: .layoutPadding,
bottom: 12,
trailing: .layoutPadding))
}
}
let isFocused = self.viewModel.statusId == status.id
private func makeCurrentStatusView(status: Status) -> some View {
StatusRowView(viewModel: .init(status: status,
client: client,
routerPath: routerPath))
.environment(\.isStatusFocused, true)
.environment(\.isStatusDetailLoaded, !viewModel.isLoadingContext)
.accessibilityFocused($initialFocusBugWorkaround, equals: true)
.overlay {
GeometryReader { reader in
VStack {}
.onAppear {
statusHeight = reader.size.height
}
StatusRowView(viewModel: viewModel)
.environment(\.extraLeadingInset, isReplyToPrevious ? 10 : 0)
.environment(\.isStatusReplyToPrevious, isReplyToPrevious)
.environment(\.isStatusFocused, isFocused)
.environment(\.isStatusDetailLoaded, isFocused ? !self.viewModel.isLoadingContext : false)
.overlay {
GeometryReader { reader in
VStack {}
.onAppear {
statusHeight = reader.size.height
}
}
}
}
.id(status.id)
// VoiceOver / Switch Control focus workaround
.onAppear {
initialFocusBugWorkaround = true
}
.id(status.id)
.listRowBackground(viewModel.highlightRowColor)
.listRowInsets(.init(top: 12,
leading: .layoutPadding,
bottom: 12,
trailing: .layoutPadding))
}
}
private var errorView: some View {

View file

@ -20,6 +20,7 @@ import SwiftUI
var isLoadingContext = true
var title: LocalizedStringKey = ""
var scrollToId: String?
var isReplyToPreviousCache: [String: Bool] = [:]
init(statusId: String) {
state = .loading
@ -80,17 +81,17 @@ import SwiftUI
var statuses = data.context.ancestors
statuses.append(data.status)
statuses.append(contentsOf: data.context.descendants)
cacheReplyTopPrevious(statuses: statuses)
StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)
if animate {
withAnimation {
isLoadingContext = false
state = .display(statuses: statuses, date: Date())
isLoadingContext = false
}
} else {
isLoadingContext = false
state = .display(statuses: statuses, date: Date())
isLoadingContext = false
scrollToId = statusId
}
} catch {
@ -108,6 +109,27 @@ import SwiftUI
return try await .init(status: status, context: context)
}
private func cacheReplyTopPrevious(statuses: [Status]) {
isReplyToPreviousCache = [:]
for status in statuses {
var isReplyToPrevious: Bool = false
if let index = statuses.firstIndex(where: { $0.id == status.id }),
index > 0,
statuses[index - 1].id == status.inReplyToId
{
if index == 1, statuses.count > 2 {
let nextStatus = statuses[2]
isReplyToPrevious = nextStatus.inReplyToId == status.id
} else if statuses.count == 2 {
isReplyToPrevious = false
} else {
isReplyToPrevious = true
}
}
isReplyToPreviousCache[status.id] = isReplyToPrevious
}
}
func handleEvent(event: any StreamEvent, currentAccount: Account?) {
Task {
if let event = event as? StreamEventUpdate,

View file

@ -14,6 +14,7 @@ public struct StatusRowView: View {
@Environment(\.accessibilityVoiceOverEnabled) private var accessibilityVoiceOverEnabled
@Environment(\.isStatusFocused) private var isFocused
@Environment(\.isStatusDetailLoaded) private var isStatusDetailLoaded
@Environment(\.isStatusReplyToPrevious) private var isStatusReplyToPrevious
@Environment(QuickLook.self) private var quickLook
@EnvironmentObject private var theme: Theme
@ -29,67 +30,78 @@ public struct StatusRowView: View {
}
public var body: some View {
VStack(alignment: .leading) {
if viewModel.isFiltered, let filter = viewModel.filter {
switch filter.filter.filterAction {
case .warn:
makeFilterView(filter: filter.filter)
case .hide:
EmptyView()
}
} else {
if !isCompact, theme.avatarPosition == .leading {
Group {
StatusRowReblogView(viewModel: viewModel)
StatusRowReplyView(viewModel: viewModel)
HStack(spacing: 0) {
if isStatusReplyToPrevious {
Rectangle()
.fill(theme.tintColor)
.frame(width: 2)
.accessibilityHidden(true)
Spacer(minLength: 8)
}
VStack(alignment: .leading) {
if viewModel.isFiltered, let filter = viewModel.filter {
switch filter.filter.filterAction {
case .warn:
makeFilterView(filter: filter.filter)
case .hide:
EmptyView()
}
.padding(.leading, AvatarView.Size.status.size.width + .statusColumnsSpacing)
}
HStack(alignment: .top, spacing: .statusColumnsSpacing) {
if !isCompact,
theme.avatarPosition == .leading
{
Button {
viewModel.navigateToAccountDetail(account: viewModel.finalStatus.account)
} label: {
AvatarView(url: viewModel.finalStatus.account.avatar, size: .status)
}
}
VStack(alignment: .leading) {
if !isCompact, theme.avatarPosition == .top {
} else {
if !isCompact, theme.avatarPosition == .leading {
Group {
StatusRowReblogView(viewModel: viewModel)
StatusRowReplyView(viewModel: viewModel)
}
VStack(alignment: .leading, spacing: 8) {
if !isCompact {
StatusRowHeaderView(viewModel: viewModel)
.padding(.leading, AvatarView.Size.status.size.width + .statusColumnsSpacing)
}
HStack(alignment: .top, spacing: .statusColumnsSpacing) {
if !isCompact,
theme.avatarPosition == .leading
{
Button {
viewModel.navigateToAccountDetail(account: viewModel.finalStatus.account)
} label: {
AvatarView(url: viewModel.finalStatus.account.avatar, size: .status)
}
StatusRowContentView(viewModel: viewModel)
.contentShape(Rectangle())
.onTapGesture {
guard !isFocused else { return }
viewModel.navigateToDetail()
}
.accessibilityActions {
if isFocused, viewModel.showActions {
accessibilityActions
}
}
}
VStack(alignment: .leading, spacing: 12) {
if viewModel.showActions, isFocused || theme.statusActionsDisplay != .none, !isInCaptureMode {
StatusRowActionsView(viewModel: viewModel)
.padding(.top, 8)
.tint(isFocused ? theme.tintColor : .gray)
VStack(alignment: .leading) {
if !isCompact, theme.avatarPosition == .top {
StatusRowReblogView(viewModel: viewModel)
StatusRowReplyView(viewModel: viewModel)
}
VStack(alignment: .leading, spacing: 8) {
if !isCompact {
StatusRowHeaderView(viewModel: viewModel)
}
StatusRowContentView(viewModel: viewModel)
.contentShape(Rectangle())
.onTapGesture {
guard !isFocused else { return }
viewModel.navigateToDetail()
}
.accessibilityActions {
if isFocused, viewModel.showActions {
accessibilityActions
}
}
}
if isFocused, isStatusDetailLoaded {
StatusRowDetailView(viewModel: viewModel)
VStack(alignment: .leading, spacing: 12) {
if viewModel.showActions, isFocused || theme.statusActionsDisplay != .none, !isInCaptureMode {
StatusRowActionsView(viewModel: viewModel)
.padding(.top, 8)
.tint(isFocused ? theme.tintColor : .gray)
.contentShape(Rectangle())
.onTapGesture {
guard !isFocused else { return }
viewModel.navigateToDetail()
}
}
if isFocused, isStatusDetailLoaded {
StatusRowDetailView(viewModel: viewModel)
.transition(.move(edge: .bottom))
.animation(.snappy, value: isStatusDetailLoaded)
}
}
}
}