IceCubesApp/Packages/StatusKit/Sources/StatusKit/Detail/StatusDetailView.swift

198 lines
6.2 KiB
Swift
Raw Normal View History

2023-01-17 10:36:01 +00:00
import DesignSystem
2022-12-22 09:53:36 +00:00
import Env
2023-01-17 10:36:01 +00:00
import Models
2022-12-21 17:28:21 +00:00
import Network
2023-01-17 10:36:01 +00:00
import SwiftUI
2022-11-29 10:46:02 +00:00
2023-09-18 19:03:52 +00:00
@MainActor
2022-11-29 10:46:02 +00:00
public struct StatusDetailView: View {
2023-09-18 19:03:52 +00:00
@Environment(Theme.self) private var theme
@Environment(CurrentAccount.self) private var currentAccount
@Environment(StreamWatcher.self) private var watcher
@Environment(Client.self) private var client
@Environment(RouterPath.self) private var routerPath
@Environment(\.isCompact) private var isCompact: Bool
@Environment(UserPreferences.self) private var userPreferences: UserPreferences
2023-07-19 05:46:25 +00:00
@State private var viewModel: StatusDetailViewModel
2023-07-19 05:46:25 +00:00
2022-12-21 17:28:21 +00:00
@State private var isLoaded: Bool = false
@State private var statusHeight: CGFloat = 0
2023-02-12 15:29:41 +00:00
/// April 4th, 2023: Without explicit focus being set, VoiceOver will skip over a seemingly random number of elements on this screen when pushing in from the main timeline.
/// By using ``@AccessibilityFocusState`` and setting focus once, we work around this issue.
@AccessibilityFocusState private var initialFocusBugWorkaround: Bool
2022-11-29 10:46:02 +00:00
public init(statusId: String) {
_viewModel = .init(wrappedValue: .init(statusId: statusId))
2022-11-29 10:46:02 +00:00
}
2023-02-12 15:29:41 +00:00
public init(status: Status) {
_viewModel = .init(wrappedValue: .init(status: status))
}
2023-02-12 15:29:41 +00:00
public init(remoteStatusURL: URL) {
_viewModel = .init(wrappedValue: .init(remoteStatusURL: remoteStatusURL))
}
2023-02-12 15:29:41 +00:00
2022-11-29 10:46:02 +00:00
public var body: some View {
GeometryReader { reader in
ScrollViewReader { proxy in
List {
if isLoaded {
topPaddingView
}
2023-02-12 15:29:41 +00:00
switch viewModel.state {
case .loading:
loadingDetailView
2023-02-12 15:29:41 +00:00
case let .display(statuses):
makeStatusesListView(statuses: statuses)
2023-02-12 15:29:41 +00:00
if !isLoaded {
loadingContextView
}
2023-02-12 15:29:41 +00:00
2024-02-14 11:48:14 +00:00
#if !os(visionOS)
Rectangle()
.foregroundColor(theme.secondaryBackgroundColor)
.frame(minHeight: reader.frame(in: .local).size.height - statusHeight)
.listRowSeparator(.hidden)
.listRowBackground(theme.secondaryBackgroundColor)
.listRowInsets(.init())
.accessibilityHidden(true)
#endif
2023-02-12 15:29:41 +00:00
case .error:
errorView
2022-12-21 17:28:21 +00:00
}
}
.environment(\.defaultMinListRowHeight, 1)
.listStyle(.plain)
#if !os(visionOS)
2024-02-14 11:48:14 +00:00
.scrollContentBackground(.hidden)
.background(theme.primaryBackgroundColor)
#endif
2024-02-14 11:48:14 +00:00
.onChange(of: viewModel.scrollToId) { _, newValue in
if let newValue {
viewModel.scrollToId = nil
proxy.scrollTo(newValue, anchor: .top)
}
2023-01-12 17:25:37 +00:00
}
2024-02-14 11:48:14 +00:00
.onAppear {
guard !isLoaded else { return }
viewModel.client = client
viewModel.routerPath = routerPath
Task {
let result = await viewModel.fetch()
isLoaded = true
if !result {
if let url = viewModel.remoteStatusURL {
await UIApplication.shared.open(url)
}
DispatchQueue.main.async {
_ = routerPath.path.popLast()
}
2024-01-01 20:29:28 +00:00
}
}
2023-01-12 17:25:37 +00:00
}
2022-12-21 17:28:21 +00:00
}
2023-12-10 07:42:26 +00:00
.refreshable {
Task {
await viewModel.fetch()
}
}
.onChange(of: watcher.latestEvent?.id) {
2022-12-25 16:39:23 +00:00
guard let lastEvent = watcher.latestEvent else { return }
viewModel.handleEvent(event: lastEvent, currentAccount: currentAccount.account)
}
2022-12-21 17:28:21 +00:00
}
.navigationTitle(viewModel.title)
.navigationBarTitleDisplayMode(.inline)
2022-11-29 10:46:02 +00:00
}
2023-02-18 06:26:48 +00:00
private func makeStatusesListView(statuses: [Status]) -> some View {
ForEach(statuses) { status in
let (indentationLevel, extraInsets) = viewModel.getIndentationLevel(id: status.id, maxIndent: userPreferences.getRealMaxIndent())
let viewModel: StatusRowViewModel = .init(status: status,
client: client,
routerPath: routerPath,
scrollToId: $viewModel.scrollToId)
let isFocused = self.viewModel.statusId == status.id
StatusRowView(viewModel: viewModel, context: .detail)
2024-01-01 13:13:25 +00:00
.id(status.id + (status.editedAt?.asDate.description ?? ""))
.environment(\.extraLeadingInset, !isCompact ? extraInsets : 0)
.environment(\.indentationLevel, !isCompact ? indentationLevel : 0)
.environment(\.isStatusFocused, isFocused)
.overlay {
if isFocused {
GeometryReader { reader in
VStack {}
.onAppear {
statusHeight = reader.size.height
}
2023-02-12 15:29:41 +00:00
}
}
}
2024-02-14 11:48:14 +00:00
#if !os(visionOS)
.listRowBackground(viewModel.highlightRowColor)
2024-02-14 11:48:14 +00:00
#endif
.listRowInsets(.init(top: 12,
leading: .layoutPadding,
bottom: 12,
trailing: .layoutPadding))
}
}
2023-02-12 15:29:41 +00:00
private var errorView: some View {
ErrorView(title: "status.error.title",
message: "status.error.message",
2023-03-13 12:38:28 +00:00
buttonTitle: "action.retry")
{
Task {
await viewModel.fetch()
}
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
.listRowSeparator(.hidden)
}
2023-02-12 15:29:41 +00:00
private var loadingDetailView: some View {
ForEach(Status.placeholders()) { status in
StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath))
.redacted(reason: .placeholder)
2023-09-18 16:55:11 +00:00
.allowsHitTesting(false)
}
}
2023-02-12 15:29:41 +00:00
private var loadingContextView: some View {
HStack {
Spacer()
ProgressView()
Spacer()
}
.frame(height: 50)
.listRowSeparator(.hidden)
#if !os(visionOS)
2024-02-14 11:48:14 +00:00
.listRowBackground(theme.secondaryBackgroundColor)
#endif
2024-02-14 11:48:14 +00:00
.listRowInsets(.init())
}
2023-02-12 15:29:41 +00:00
private var topPaddingView: some View {
HStack { EmptyView() }
2024-02-14 11:48:14 +00:00
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
2024-02-14 11:48:14 +00:00
#endif
.listRowSeparator(.hidden)
.listRowInsets(.init())
.frame(height: .layoutPadding)
Accessibility tweaks + Notifications and Messages tab uplift (#1292) * Improve StatusRowView accessibility actions Previously, there was no way to interact with links and hashtags. Now, these are added to the Actions rotor * Hide `topPaddingView`s from accessibility * Fix accessible header rendering in non-filterable TimelineViews Previously, all navigation title views were assumed to be popup buttons. Now, we only change the representation for timelines that are filterable. * Combine tagHeaderView text elements Previously, these were two separate items * Prefer shorter Quote action label * Improve accessibility of StatusEmbeddedView Previously, this element would be three different ones, and include all the actions on the `StatusRowView` proper. Now, it presents as one element with no actions. * Add haptics to StatusRowView accessibility actions * Improve accessibility of ConversationsListRow This commit adds: - A combined representation of the component views - “Unread” as the first part of the label (if this is the case) - All relevant actions as custom actions - Reply as magic tap * Remove StatusRowView accessibilityActions if viewModel.showActions is false * Hide media attachments from accessibility if the view is not focused * Combine NotificationRowView accessibility elements; add user actions Previously, there was no real way to interact with these notifications. Now, the notifications that show the actions row have the appropriate StatusRowView-derived actions, and new followers notifications have more actions that let you see each user’s profile. * Prefer @Environment’s `accessibilityEnabled` over `isVoiceOverRunning` This way we can cater for Voice Control, Full Keyboard Access and Switch Control as well. --------- Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
2023-03-24 06:52:29 +00:00
.accessibilityHidden(true)
}
2022-11-29 10:46:02 +00:00
}