mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-06-02 13:11:01 +00:00
Simply detail view
This commit is contained in:
parent
c0bfb94f97
commit
d1d7efda3a
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue