diff --git a/Packages/Status/Sources/Status/Row/StatusRowAccessibilityLabel.swift b/Packages/Status/Sources/Status/Row/StatusRowAccessibilityLabel.swift new file mode 100644 index 00000000..de8a0c76 --- /dev/null +++ b/Packages/Status/Sources/Status/Row/StatusRowAccessibilityLabel.swift @@ -0,0 +1,122 @@ +import SwiftUI +import Models + +/// A utility that creates a suitable combined accessibility label for a `StatusRowView` that is not focused. +@MainActor +struct StatusRowAccessibilityLabel { + let viewModel: StatusRowViewModel + + var hasSpoiler: Bool { + viewModel.displaySpoiler && viewModel.finalStatus.spoilerText.asRawText.isEmpty == false + } + + var isReply: Bool { + if let accountId = viewModel.status.inReplyToAccountId, viewModel.status.mentions.contains(where: { $0.id == accountId }) { + return true + } + return false + } + + var isBoost: Bool { + viewModel.status.reblog != nil + } + + var filter: Filter? { + guard viewModel.isFiltered else { + return nil + } + return viewModel.filter?.filter + } + + func finalLabel() -> Text { + if let filter { + switch filter.filterAction { + case .warn: + Text("status.filter.filtered-by-\(filter.title)") + case .hide: + Text("") + } + } else { + userNamePreamble() + + Text(hasSpoiler + ? viewModel.finalStatus.spoilerText.asRawText + : viewModel.finalStatus.content.asRawText + ) + + Text(hasSpoiler + ? "status.editor.spoiler" + : "" + ) + Text(", ") + + pollText() + + imageAltText() + + Text(viewModel.finalStatus.createdAt.relativeFormatted) + Text(", ") + + Text("status.summary.n-replies \(viewModel.finalStatus.repliesCount)") + Text(", ") + + Text("status.summary.n-boosts \(viewModel.finalStatus.reblogsCount)") + Text(", ") + + Text("status.summary.n-favorites \(viewModel.finalStatus.favouritesCount)") + } + } + + func userNamePreamble() -> Text { + switch (isReply, isBoost) { + case (true, false): + Text("accessibility.status.a-replied-to-\(finalUserDisplayName())") + Text(" ") + case (_, true): + Text("accessibility.status.a-boosted-b-\(userDisplayName())-\(finalUserDisplayName())") + Text(", ") + default: + Text(userDisplayName()) + Text(", ") + } + } + + func userDisplayName() -> String { + viewModel.status.account.displayNameWithoutEmojis.count < 4 + ? viewModel.status.account.safeDisplayName + : viewModel.status.account.displayNameWithoutEmojis + } + + func finalUserDisplayName() -> String { + viewModel.finalStatus.account.displayNameWithoutEmojis.count < 4 + ? viewModel.finalStatus.account.safeDisplayName + : viewModel.finalStatus.account.displayNameWithoutEmojis + } + + func imageAltText() -> Text { + let descriptions = viewModel.finalStatus.mediaAttachments + .compactMap(\.description) + + if descriptions.count == 1 { + return Text("accessibility.image.alt-text-\(descriptions[0])") + Text(", ") + } else if descriptions.count > 1 { + return Text("accessibility.image.alt-text-\(descriptions[0])") + Text(", ") + Text("accessibility.image.alt-text-more.label") + Text(", ") + } else if viewModel.finalStatus.mediaAttachments.isEmpty == false { + let differentTypes = Set(viewModel.finalStatus.mediaAttachments.compactMap(\.localizedTypeDescription)).sorted() + return Text("accessibility.status.contains-media.label-\(ListFormatter.localizedString(byJoining: differentTypes))") + Text(", ") + } else { + return Text("") + } + } + + func pollText() -> Text { + if let poll = viewModel.finalStatus.poll { + let showPercentage = poll.expired || poll.voted ?? false + let title: LocalizedStringKey = poll.expired + ? "accessibility.status.poll.finished.label" + : "accessibility.status.poll.active.label" + + return poll.options.enumerated().reduce(into: Text(title)) { text, pair in + let (index, option) = pair + let selected = poll.ownVotes?.contains(index) ?? false + let percentage = poll.safeVotersCount > 0 && option.votesCount != nil + ? Int(round(Double(option.votesCount!) / Double(poll.safeVotersCount) * 100)) + : 0 + + text = text + + Text(selected ? "accessibility.status.poll.selected.label" : "") + + Text(", ") + + Text("accessibility.status.poll.option-prefix-\(index + 1)-of-\(poll.options.count)") + + Text(", ") + + Text(option.title) + + Text(showPercentage ? ", \(percentage)%. " : ". ") + } + } + return Text("") + } +} diff --git a/Packages/Status/Sources/Status/Row/StatusRowView.swift b/Packages/Status/Sources/Status/Row/StatusRowView.swift index d726404c..640605ee 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowView.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowView.swift @@ -149,7 +149,7 @@ public struct StatusRowView: View { trailing: .layoutPadding)) .accessibilityElement(children: isFocused ? .contain : .combine) .accessibilityLabel(isFocused == false && accessibilityVoiceOverEnabled - ? CombinedAccessibilityLabel(viewModel: viewModel).finalLabel() : Text("")) + ? StatusRowAccessibilityLabel(viewModel: viewModel).finalLabel() : Text("")) .accessibilityHidden(viewModel.filter?.filter.filterAction == .hide) .accessibilityAction { guard !isFocused else { return } @@ -312,122 +312,3 @@ public struct StatusRowView: View { } } -/// A utility that creates a suitable combined accessibility label for a `StatusRowView` that is not focused. -@MainActor -private struct CombinedAccessibilityLabel { - let viewModel: StatusRowViewModel - - var hasSpoiler: Bool { - viewModel.displaySpoiler && viewModel.finalStatus.spoilerText.asRawText.isEmpty == false - } - - var isReply: Bool { - if let accountId = viewModel.status.inReplyToAccountId, viewModel.status.mentions.contains(where: { $0.id == accountId }) { - return true - } - return false - } - - var isBoost: Bool { - viewModel.status.reblog != nil - } - - var filter: Filter? { - guard viewModel.isFiltered else { - return nil - } - return viewModel.filter?.filter - } - - func finalLabel() -> Text { - if let filter { - switch filter.filterAction { - case .warn: - Text("status.filter.filtered-by-\(filter.title)") - case .hide: - Text("") - } - } else { - userNamePreamble() + - Text(hasSpoiler - ? viewModel.finalStatus.spoilerText.asRawText - : viewModel.finalStatus.content.asRawText - ) + - Text(hasSpoiler - ? "status.editor.spoiler" - : "" - ) + Text(", ") + - pollText() + - imageAltText() + - Text(viewModel.finalStatus.createdAt.relativeFormatted) + Text(", ") + - Text("status.summary.n-replies \(viewModel.finalStatus.repliesCount)") + Text(", ") + - Text("status.summary.n-boosts \(viewModel.finalStatus.reblogsCount)") + Text(", ") + - Text("status.summary.n-favorites \(viewModel.finalStatus.favouritesCount)") - } - } - - func userNamePreamble() -> Text { - switch (isReply, isBoost) { - case (true, false): - Text("accessibility.status.a-replied-to-\(finalUserDisplayName())") + Text(" ") - case (_, true): - Text("accessibility.status.a-boosted-b-\(userDisplayName())-\(finalUserDisplayName())") + Text(", ") - default: - Text(userDisplayName()) + Text(", ") - } - } - - func userDisplayName() -> String { - viewModel.status.account.displayNameWithoutEmojis.count < 4 - ? viewModel.status.account.safeDisplayName - : viewModel.status.account.displayNameWithoutEmojis - } - - func finalUserDisplayName() -> String { - viewModel.finalStatus.account.displayNameWithoutEmojis.count < 4 - ? viewModel.finalStatus.account.safeDisplayName - : viewModel.finalStatus.account.displayNameWithoutEmojis - } - - func imageAltText() -> Text { - let descriptions = viewModel.finalStatus.mediaAttachments - .compactMap(\.description) - - if descriptions.count == 1 { - return Text("accessibility.image.alt-text-\(descriptions[0])") + Text(", ") - } else if descriptions.count > 1 { - return Text("accessibility.image.alt-text-\(descriptions[0])") + Text(", ") + Text("accessibility.image.alt-text-more.label") + Text(", ") - } else if viewModel.finalStatus.mediaAttachments.isEmpty == false { - let differentTypes = Set(viewModel.finalStatus.mediaAttachments.compactMap(\.localizedTypeDescription)).sorted() - return Text("accessibility.status.contains-media.label-\(ListFormatter.localizedString(byJoining: differentTypes))") + Text(", ") - } else { - return Text("") - } - } - - func pollText() -> Text { - if let poll = viewModel.finalStatus.poll { - let showPercentage = poll.expired || poll.voted ?? false - let title: LocalizedStringKey = poll.expired - ? "accessibility.status.poll.finished.label" - : "accessibility.status.poll.active.label" - - return poll.options.enumerated().reduce(into: Text(title)) { text, pair in - let (index, option) = pair - let selected = poll.ownVotes?.contains(index) ?? false - let percentage = poll.safeVotersCount > 0 && option.votesCount != nil - ? Int(round(Double(option.votesCount!) / Double(poll.safeVotersCount) * 100)) - : 0 - - text = text + - Text(selected ? "accessibility.status.poll.selected.label" : "") + - Text(", ") + - Text("accessibility.status.poll.option-prefix-\(index + 1)-of-\(poll.options.count)") + - Text(", ") + - Text(option.title) + - Text(showPercentage ? ", \(percentage)%. " : ". ") - } - } - return Text("") - } -} diff --git a/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift b/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift index 948cac5e..cba5c623 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift @@ -186,20 +186,6 @@ import SwiftUI } } - func navigateToMention(mention: Mention) { - if isRemote { - withAnimation { - isLoadingRemoteContent = true - } - Task { - await routerPath.navigateToAccountFrom(url: mention.url) - isLoadingRemoteContent = false - } - } else { - routerPath.navigate(to: .accountDetail(id: mention.id)) - } - } - func goToParent() { guard let id = status.inReplyToId else { return } if let _ = scrollToId {