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

175 lines
5.4 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 Shimmer
import SwiftUI
2022-11-29 10:46:02 +00:00
public struct StatusDetailView: View {
2022-12-29 09:39:34 +00:00
@EnvironmentObject private var theme: Theme
2023-09-17 04:44:45 +00:00
@Environment(CurrentAccount.self) private var currentAccount
2023-09-16 16:58:13 +00:00
@Environment(StreamWatcher.self) private var watcher
2023-09-16 16:40:59 +00:00
@Environment(Client.self) private var client
2023-09-16 16:58:13 +00:00
@Environment(RouterPath.self) private var routerPath
2023-07-19 05:46:25 +00:00
2023-09-17 05:53:32 +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) {
2023-09-17 05:53:32 +00:00
_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) {
2023-09-17 05:53:32 +00:00
_viewModel = .init(wrappedValue: .init(status: status))
}
2023-02-12 15:29:41 +00:00
public init(remoteStatusURL: URL) {
2023-09-17 05:53:32 +00:00
_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
2023-09-17 18:13:03 +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
Rectangle()
.foregroundColor(theme.secondaryBackgroundColor)
.frame(minHeight: reader.frame(in: .local).size.height - statusHeight)
.listRowSeparator(.hidden)
.listRowBackground(theme.secondaryBackgroundColor)
.listRowInsets(.init())
Timeline & Timeline detail accessibility uplift (#1323) * Improve accessibility of StatusPollView Previously, this view did not provide the proper context to indicate that it represented a poll. Now, we’ve added - A container that will stay “Active poll” or “Poll results” when the cursor first hits one of the options; - A prefix to say “Option X of Y” before each option; - A Selected trait on the selected option(s), if present - Consolidating and adding an `.updatesFrequently` trait to the footer view with the countdown. * Add poll description in StatusRowView combinedAccessibilityLabel This largely duplicates the logic in `StatusPollView`. * Improve accessibility of media attachments Previously, the media attachments without alt text would not show up in the consolidated `StatusRowView`, nor would they be meaningfully explained on the status detail screen. Now, they are presented with their attachment type. * Change accessibilityRepresentation of AppAcountsSelectorView * Change Notifications tab title view accessibility representation to Menu Previously it would present as a button * Hide layout `Rectangle`s from accessibility * Consolidate `StatusRowDetailView` accessibility representation * Improve readability of Poll accessibility label * Ensure poll options don’t present as interactive when the poll is finished * Improve accessibility of StatusRowCardView Previously, it would present as four separate elements, including an image without a description, all interactive, none with an interactive trait. Now, it presents as a single element with the `.link` trait * Improve accessibility of StatusRowHeaderView Previously, it had no traits and no actions except inherited ones. Now it presents as a button, triggering its primary action. It also has custom actions corresponding to its context menu * Avoid applying the StatusRowView custom actions to every view when contained * Provide context for the application name * Add pauses to StatusRowView combinedAccessibilityLabel * Hide `TimelineView.scrollToTopView` from accessibility * Set appropriate font style on Notification header After the change the Text needed a `.headline` style to match the prior appearance. * Fix bug in accessibilityRepresentation of TimelineView nav bar title Previously, it would not display the proper label for .remoteLocal filter options. * Ensure that pop-up button nav bar titles are interactive * Ensure TextView responds to Environment.sizeCategory This resolves #1309 * Fix button --------- Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
2023-03-28 16:48:58 +00:00
.accessibilityHidden(true)
2023-02-12 15:29:41 +00:00
case .error:
errorView
2022-12-21 17:28:21 +00:00
}
}
.environment(\.defaultMinListRowHeight, 1)
.listStyle(.plain)
.scrollContentBackground(.hidden)
.background(theme.primaryBackgroundColor)
2023-09-17 06:51:32 +00:00
.onChange(of: viewModel.scrollToId) { _, newValue in
2023-09-16 16:40:59 +00:00
if let newValue {
viewModel.scrollToId = nil
2023-09-16 16:40:59 +00:00
proxy.scrollTo(newValue, anchor: .top)
2023-01-12 17:25:37 +00:00
}
2023-09-17 06:51:32 +00:00
}
.task {
guard !isLoaded else { return }
viewModel.client = client
2023-03-13 17:47:24 +00:00
viewModel.routerPath = routerPath
let result = await viewModel.fetch()
isLoaded = true
2023-02-12 15:29:41 +00:00
if !result {
if let url = viewModel.remoteStatusURL {
await UIApplication.shared.open(url)
}
DispatchQueue.main.async {
_ = routerPath.path.popLast()
}
2023-01-12 17:25:37 +00:00
}
}
2022-12-21 17:28:21 +00:00
}
2023-09-16 16:40:59 +00:00
.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
2023-09-17 18:13:03 +00:00
private func makeStatusesListView(statuses: [Status]) -> some View {
ForEach(statuses) { status in
2023-09-17 12:43:01 +00:00
let isReplyToPrevious = viewModel.isReplyToPreviousCache[status.id] ?? false
let viewModel: StatusRowViewModel = .init(status: status,
client: client,
routerPath: routerPath)
2023-09-17 12:43:01 +00:00
let isFocused = self.viewModel.statusId == status.id
StatusRowView(viewModel: viewModel)
.environment(\.extraLeadingInset, isReplyToPrevious ? 10 : 0)
.environment(\.isStatusReplyToPrevious, isReplyToPrevious)
.environment(\.isStatusFocused, isFocused)
.overlay {
2023-09-17 12:53:55 +00:00
if isFocused {
GeometryReader { reader in
VStack {}
.onAppear {
statusHeight = reader.size.height
}
}
2023-09-17 12:43:01 +00:00
}
2023-02-16 16:07:52 +00:00
}
2023-09-17 12:43:01 +00:00
.id(status.id)
.listRowBackground(viewModel.highlightRowColor)
.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()
}
}
.listRowBackground(theme.primaryBackgroundColor)
.listRowSeparator(.hidden)
}
2023-02-12 15:29:41 +00:00
private var loadingDetailView: some View {
ForEach(Status.placeholders()) { status in
2023-09-16 16:40:59 +00:00
StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath))
.redacted(reason: .placeholder)
}
}
2023-02-12 15:29:41 +00:00
private var loadingContextView: some View {
HStack {
Spacer()
ProgressView()
Spacer()
}
.frame(height: 50)
.listRowSeparator(.hidden)
.listRowBackground(theme.secondaryBackgroundColor)
.listRowInsets(.init())
}
2023-02-12 15:29:41 +00:00
private var topPaddingView: some View {
HStack { EmptyView() }
.listRowBackground(theme.primaryBackgroundColor)
.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
}