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
|
2023-09-18 05:01:23 +00:00
|
|
|
@Environment(CurrentAccount.self) private var currentAccount
|
|
|
|
@Environment(StreamWatcher.self) private var watcher
|
|
|
|
@Environment(Client.self) private var client
|
|
|
|
@Environment(RouterPath.self) private var routerPath
|
2023-11-19 07:10:44 +00:00
|
|
|
@Environment(\.isCompact) private var isCompact: Bool
|
2023-12-14 06:17:09 +00:00
|
|
|
@Environment(UserPreferences.self) private var userPreferences: UserPreferences
|
2023-07-19 05:46:25 +00:00
|
|
|
|
2023-09-18 05:01:23 +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
|
2023-02-10 17:21:05 +00:00
|
|
|
@State private var statusHeight: CGFloat = 0
|
2023-02-12 15:29:41 +00:00
|
|
|
|
2023-04-04 06:12:25 +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-18 05:01:23 +00:00
|
|
|
_viewModel = .init(wrappedValue: .init(statusId: statusId))
|
2022-11-29 10:46:02 +00:00
|
|
|
}
|
2023-02-12 15:29:41 +00:00
|
|
|
|
2023-02-10 17:21:05 +00:00
|
|
|
public init(status: Status) {
|
2023-09-18 05:01:23 +00:00
|
|
|
_viewModel = .init(wrappedValue: .init(status: status))
|
2023-02-10 17:21:05 +00:00
|
|
|
}
|
2023-02-12 15:29:41 +00:00
|
|
|
|
2023-01-06 11:14:05 +00:00
|
|
|
public init(remoteStatusURL: URL) {
|
2023-09-18 05:01:23 +00:00
|
|
|
_viewModel = .init(wrappedValue: .init(remoteStatusURL: remoteStatusURL))
|
2023-01-06 11:14:05 +00:00
|
|
|
}
|
2023-02-12 15:29:41 +00:00
|
|
|
|
2022-11-29 10:46:02 +00:00
|
|
|
public var body: some View {
|
2023-02-10 17:21:05 +00:00
|
|
|
GeometryReader { reader in
|
|
|
|
ScrollViewReader { proxy in
|
|
|
|
List {
|
|
|
|
if isLoaded {
|
|
|
|
topPaddingView
|
|
|
|
}
|
2023-02-12 15:29:41 +00:00
|
|
|
|
2023-02-10 17:21:05 +00:00
|
|
|
switch viewModel.state {
|
|
|
|
case .loading:
|
|
|
|
loadingDetailView
|
2023-02-12 15:29:41 +00:00
|
|
|
|
2023-09-18 05:01:23 +00:00
|
|
|
case let .display(statuses):
|
|
|
|
makeStatusesListView(statuses: statuses)
|
2023-02-12 15:29:41 +00:00
|
|
|
|
2023-02-10 17:21:05 +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
|
|
|
|
2023-02-10 17:21:05 +00:00
|
|
|
case .error:
|
|
|
|
errorView
|
2022-12-21 17:28:21 +00:00
|
|
|
}
|
|
|
|
}
|
2023-02-10 17:21:05 +00:00
|
|
|
.environment(\.defaultMinListRowHeight, 1)
|
|
|
|
.listStyle(.plain)
|
2023-12-19 08:51:20 +00:00
|
|
|
#if !os(visionOS)
|
2024-02-14 11:48:14 +00:00
|
|
|
.scrollContentBackground(.hidden)
|
|
|
|
.background(theme.primaryBackgroundColor)
|
2023-12-19 08:51:20 +00:00
|
|
|
#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-02-10 17:21:05 +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()
|
|
|
|
}
|
|
|
|
}
|
2023-09-18 05:01:23 +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-18 05:01:23 +00:00
|
|
|
private func makeStatusesListView(statuses: [Status]) -> some View {
|
2023-02-16 11:15:13 +00:00
|
|
|
ForEach(statuses) { status in
|
2023-12-14 06:17:09 +00:00
|
|
|
let (indentationLevel, extraInsets) = viewModel.getIndentationLevel(id: status.id, maxIndent: userPreferences.getRealMaxIndent())
|
2023-02-16 11:15:13 +00:00
|
|
|
let viewModel: StatusRowViewModel = .init(status: status,
|
|
|
|
client: client,
|
2023-12-14 06:12:12 +00:00
|
|
|
routerPath: routerPath,
|
|
|
|
scrollToId: $viewModel.scrollToId)
|
2023-09-18 05:01:23 +00:00
|
|
|
let isFocused = self.viewModel.statusId == status.id
|
|
|
|
|
2024-01-18 05:41:40 +00:00
|
|
|
StatusRowView(viewModel: viewModel, context: .detail)
|
2024-01-01 13:13:25 +00:00
|
|
|
.id(status.id + (status.editedAt?.asDate.description ?? ""))
|
2023-11-19 07:10:44 +00:00
|
|
|
.environment(\.extraLeadingInset, !isCompact ? extraInsets : 0)
|
|
|
|
.environment(\.indentationLevel, !isCompact ? indentationLevel : 0)
|
2023-09-18 05:01:23 +00:00
|
|
|
.environment(\.isStatusFocused, isFocused)
|
|
|
|
.overlay {
|
|
|
|
if isFocused {
|
|
|
|
GeometryReader { reader in
|
|
|
|
VStack {}
|
|
|
|
.onAppear {
|
|
|
|
statusHeight = reader.size.height
|
|
|
|
}
|
2023-02-12 15:29:41 +00:00
|
|
|
}
|
2023-09-18 05:01:23 +00:00
|
|
|
}
|
2023-02-10 17:21:05 +00:00
|
|
|
}
|
2024-02-14 11:48:14 +00:00
|
|
|
#if !os(visionOS)
|
2023-09-18 05:01:23 +00:00
|
|
|
.listRowBackground(viewModel.highlightRowColor)
|
2024-02-14 11:48:14 +00:00
|
|
|
#endif
|
2023-09-18 05:01:23 +00:00
|
|
|
.listRowInsets(.init(top: 12,
|
|
|
|
leading: .layoutPadding,
|
|
|
|
bottom: 12,
|
|
|
|
trailing: .layoutPadding))
|
|
|
|
}
|
2023-02-10 17:21:05 +00:00
|
|
|
}
|
2023-02-12 15:29:41 +00:00
|
|
|
|
2023-02-10 17:21:05 +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")
|
|
|
|
{
|
2023-02-10 17:21:05 +00:00
|
|
|
Task {
|
|
|
|
await viewModel.fetch()
|
|
|
|
}
|
|
|
|
}
|
2023-12-19 08:51:20 +00:00
|
|
|
#if !os(visionOS)
|
2023-02-10 17:21:05 +00:00
|
|
|
.listRowBackground(theme.primaryBackgroundColor)
|
2023-12-19 08:51:20 +00:00
|
|
|
#endif
|
2023-02-10 17:21:05 +00:00
|
|
|
.listRowSeparator(.hidden)
|
|
|
|
}
|
2023-02-12 15:29:41 +00:00
|
|
|
|
2023-02-10 17:21:05 +00:00
|
|
|
private var loadingDetailView: some View {
|
|
|
|
ForEach(Status.placeholders()) { status in
|
2023-09-18 05:01:23 +00:00
|
|
|
StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath))
|
2023-02-10 17:21:05 +00:00
|
|
|
.redacted(reason: .placeholder)
|
2023-09-18 16:55:11 +00:00
|
|
|
.allowsHitTesting(false)
|
2023-02-10 17:21:05 +00:00
|
|
|
}
|
|
|
|
}
|
2023-02-12 15:29:41 +00:00
|
|
|
|
2023-02-10 17:21:05 +00:00
|
|
|
private var loadingContextView: some View {
|
|
|
|
HStack {
|
|
|
|
Spacer()
|
|
|
|
ProgressView()
|
|
|
|
Spacer()
|
|
|
|
}
|
|
|
|
.frame(height: 50)
|
|
|
|
.listRowSeparator(.hidden)
|
2023-12-19 08:51:20 +00:00
|
|
|
#if !os(visionOS)
|
2024-02-14 11:48:14 +00:00
|
|
|
.listRowBackground(theme.secondaryBackgroundColor)
|
2023-12-19 08:51:20 +00:00
|
|
|
#endif
|
2024-02-14 11:48:14 +00:00
|
|
|
.listRowInsets(.init())
|
2023-02-10 17:21:05 +00:00
|
|
|
}
|
2023-02-12 15:29:41 +00:00
|
|
|
|
2023-02-10 17:21:05 +00:00
|
|
|
private var topPaddingView: some View {
|
|
|
|
HStack { EmptyView() }
|
2024-02-14 11:48:14 +00:00
|
|
|
#if !os(visionOS)
|
2023-02-10 17:21:05 +00:00
|
|
|
.listRowBackground(theme.primaryBackgroundColor)
|
2024-02-14 11:48:14 +00:00
|
|
|
#endif
|
2023-02-10 17:21:05 +00:00
|
|
|
.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)
|
2023-02-10 17:21:05 +00:00
|
|
|
}
|
2022-11-29 10:46:02 +00:00
|
|
|
}
|