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>
This commit is contained in:
Chris Kolbu 2023-03-24 17:52:29 +11:00 committed by GitHub
parent 9746eb7674
commit b2f594f174
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 323 additions and 70 deletions

View file

@ -31,9 +31,9 @@ extension View {
case let .conversationDetail(conversation):
ConversationDetailView(conversation: conversation)
case let .hashTag(tag, accountId):
TimelineView(timeline: .constant(.hashtag(tag: tag, accountId: accountId)), scrollToTopSignal: .constant(0))
TimelineView(timeline: .constant(.hashtag(tag: tag, accountId: accountId)), scrollToTopSignal: .constant(0), canFilterTimeline: false)
case let .list(list):
TimelineView(timeline: .constant(.list(list: list)), scrollToTopSignal: .constant(0))
TimelineView(timeline: .constant(.list(list: list)), scrollToTopSignal: .constant(0), canFilterTimeline: false)
case let .following(id):
AccountsListView(mode: .following(accountId: id))
case let .followers(id):
@ -45,7 +45,7 @@ extension View {
case let .accountsList(accounts):
AccountsListView(mode: .accountsList(accounts: accounts))
case .trendingTimeline:
TimelineView(timeline: .constant(.trending), scrollToTopSignal: .constant(0))
TimelineView(timeline: .constant(.trending), scrollToTopSignal: .constant(0), canFilterTimeline: false)
case let .tagsList(tags):
TagsListView(tags: tags)
}

View file

@ -32,7 +32,7 @@ struct TimelineTab: View {
var body: some View {
NavigationStack(path: $routerPath.path) {
TimelineView(timeline: $timeline, scrollToTopSignal: $scrollToTopSignal)
TimelineView(timeline: $timeline, scrollToTopSignal: $scrollToTopSignal, canFilterTimeline: canFilterTimeline)
.withAppRouter()
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
.toolbar {

View file

@ -529,6 +529,8 @@
"accessibility.tabs.timeline.new-post.inputLabel2" = "Create";
"accessibility.tabs.timeline.unread-posts.label-%lld" = "%lld new posts";
"accessibility.tabs.timeline.unread-posts.hint" = "Scrolls the timeline.";
"accessibility.tabs.timeline.content-link-%@" = "Visit %@";
"accessibility.tabs.timeline.content-hashtag-%@" = "Hashtag %@";
"accessibility.app-account.selector.accounts" = "Уліковыя запісы";
"accessibility.app-account.selector.accounts.hint" = "Opens options sheet.";
"accessibility.tabs.profile.options.label" = "Options";
@ -562,6 +564,7 @@
"accessibility.status.a-replied-to-%@" = "%@ replied to";
"accessibility.image.alt-text-%@" = "Image alt text: %@";
"accessibility.image.alt-text-more.label" = "More alt text available";
"accessibility.tabs.messages.unread.label" = "Unread";
// MARK: Report
"report.comment.placeholder" = "Дадатковая інфармацыя";

View file

@ -523,6 +523,8 @@
"accessibility.tabs.timeline.new-post.inputLabel2" = "Create";
"accessibility.tabs.timeline.unread-posts.label-%lld" = "%lld new posts";
"accessibility.tabs.timeline.unread-posts.hint" = "Scrolls the timeline.";
"accessibility.tabs.timeline.content-link-%@" = "Visit %@";
"accessibility.tabs.timeline.content-hashtag-%@" = "Hashtag %@";
"accessibility.app-account.selector.accounts" = "Accounts";
"accessibility.app-account.selector.accounts.hint" = "Opens options sheet.";
"accessibility.tabs.profile.options.label" = "Options";
@ -556,6 +558,7 @@
"accessibility.status.a-replied-to-%@" = "%@ replied to";
"accessibility.image.alt-text-%@" = "Image alt text: %@";
"accessibility.image.alt-text-more.label" = "More alt text available";
"accessibility.tabs.messages.unread.label" = "Unread";
// MARK: Report
"report.comment.placeholder" = "Additional Info";

View file

@ -519,6 +519,8 @@
"accessibility.tabs.timeline.new-post.inputLabel2" = "Erzeugen";
"accessibility.tabs.timeline.unread-posts.label-%lld" = "%lld neue Beiträge";
"accessibility.tabs.timeline.unread-posts.hint" = "Bewegt durch die Timeline.";
"accessibility.tabs.timeline.content-link-%@" = "Visit %@";
"accessibility.tabs.timeline.content-hashtag-%@" = "Hashtag %@";
"accessibility.app-account.selector.accounts" = "Konten";
"accessibility.app-account.selector.accounts.hint" = "Öffnet die Optionen.";
"accessibility.tabs.profile.options.label" = "Optionen";
@ -552,6 +554,7 @@
"accessibility.status.a-replied-to-%@" = "%@ antwortete auf";
"accessibility.image.alt-text-%@" = "Alternativer Bildtext: %@";
"accessibility.image.alt-text-more.label" = "Weiterer Alt.-Text verfügbar";
"accessibility.tabs.messages.unread.label" = "Unread";
// MARK: Report
"report.comment.placeholder" = "Zusätzliche Informationen";

View file

@ -523,6 +523,8 @@
"accessibility.tabs.timeline.new-post.inputLabel2" = "Create";
"accessibility.tabs.timeline.unread-posts.label-%lld" = "%lld new posts";
"accessibility.tabs.timeline.unread-posts.hint" = "Scrolls the timeline.";
"accessibility.tabs.timeline.content-link-%@" = "Visit %@";
"accessibility.tabs.timeline.content-hashtag-%@" = "Hashtag %@";
"accessibility.editor.button.characters-remaining" = "Characters remaining";
"accessibility.editor.privacy.label" = "Visibility";
"accessibility.editor.privacy.hint" = "Changes post audience.";
@ -559,6 +561,7 @@
"accessibility.status.a-replied-to-%@" = "%@ replied to";
"accessibility.image.alt-text-%@" = "Image alt text: %@";
"accessibility.image.alt-text-more.label" = "More alt text available";
"accessibility.tabs.messages.unread.label" = "Unread";
// MARK: Report
"report.comment.placeholder" = "Additional Info";

View file

@ -525,6 +525,8 @@
"accessibility.tabs.timeline.new-post.inputLabel2" = "Create";
"accessibility.tabs.timeline.unread-posts.label-%lld" = "%lld new posts";
"accessibility.tabs.timeline.unread-posts.hint" = "Scrolls the timeline.";
"accessibility.tabs.timeline.content-link-%@" = "Visit %@";
"accessibility.tabs.timeline.content-hashtag-%@" = "Hashtag %@";
"accessibility.app-account.selector.accounts" = "Accounts";
"accessibility.app-account.selector.accounts.hint" = "Opens options sheet.";
"accessibility.tabs.profile.options.label" = "Options";
@ -558,6 +560,7 @@
"accessibility.status.a-replied-to-%@" = "%@ replied to";
"accessibility.image.alt-text-%@" = "Image alt text: %@";
"accessibility.image.alt-text-more.label" = "More alt text available";
"accessibility.tabs.messages.unread.label" = "Unread";
// MARK: Report
"report.comment.placeholder" = "Additional Info";

View file

@ -525,6 +525,8 @@
"accessibility.tabs.timeline.new-post.inputLabel2" = "Crear";
"accessibility.tabs.timeline.unread-posts.label-%lld" = "%lld publicaciones nuevas";
"accessibility.tabs.timeline.unread-posts.hint" = "Scrolls the timeline.";
"accessibility.tabs.timeline.content-link-%@" = "Visit %@";
"accessibility.tabs.timeline.content-hashtag-%@" = "Hashtag %@";
"accessibility.app-account.selector.accounts" = "Cuentas";
"accessibility.app-account.selector.accounts.hint" = "Abrir opciones.";
"accessibility.tabs.profile.options.label" = "Opciones";
@ -558,6 +560,7 @@
"accessibility.status.a-replied-to-%@" = "%@ respondió a";
"accessibility.image.alt-text-%@" = "Texto alt de la imagen: %@";
"accessibility.image.alt-text-more.label" = "Hay más text alt disponible";
"accessibility.tabs.messages.unread.label" = "Unread";
// MARK: Report
"report.comment.placeholder" = "Información adicional";

View file

@ -513,6 +513,8 @@
"accessibility.tabs.timeline.new-post.inputLabel2" = "Sortu";
"accessibility.tabs.timeline.unread-posts.label-%lld" = "%lld bidalketa berri";
"accessibility.tabs.timeline.unread-posts.hint" = "Denbora-lerroa korritzen du.";
"accessibility.tabs.timeline.content-link-%@" = "Visit %@";
"accessibility.tabs.timeline.content-hashtag-%@" = "Hashtag %@";
"accessibility.app-account.selector.accounts" = "Kontuak";
"accessibility.app-account.selector.accounts.hint" = "Aukeren orria irekitzen du.";
"accessibility.tabs.profile.options.label" = "Aukerak";
@ -546,6 +548,7 @@
"accessibility.status.a-replied-to-%@" = "%@(e)k honi erantzun dio:";
"accessibility.image.alt-text-%@" = "Irudiaren deskribapena: %@";
"accessibility.image.alt-text-more.label" = "Deskribapen testu gehiago dago";
"accessibility.tabs.messages.unread.label" = "Unread";
// MARK: Report
"report.comment.placeholder" = "Informazio gehigarria";

View file

@ -520,6 +520,8 @@
"accessibility.tabs.timeline.new-post.inputLabel2" = "Create";
"accessibility.tabs.timeline.unread-posts.label-%lld" = "%lld new posts";
"accessibility.tabs.timeline.unread-posts.hint" = "Scrolls the timeline.";
"accessibility.tabs.timeline.content-link-%@" = "Visit %@";
"accessibility.tabs.timeline.content-hashtag-%@" = "Hashtag %@";
"accessibility.app-account.selector.accounts" = "Comptes";
"accessibility.app-account.selector.accounts.hint" = "Opens options sheet.";
"accessibility.tabs.profile.options.label" = "Options";
@ -553,6 +555,7 @@
"accessibility.status.a-replied-to-%@" = "%@ replied to";
"accessibility.image.alt-text-%@" = "Image alt text: %@";
"accessibility.image.alt-text-more.label" = "More alt text available";
"accessibility.tabs.messages.unread.label" = "Unread";
// MARK: Report
"report.comment.placeholder" = "Information supplémentaire";

View file

@ -524,6 +524,8 @@
"accessibility.tabs.timeline.new-post.inputLabel2" = "Create";
"accessibility.tabs.timeline.unread-posts.label-%lld" = "%lld new posts";
"accessibility.tabs.timeline.unread-posts.hint" = "Scrolls the timeline.";
"accessibility.tabs.timeline.content-link-%@" = "Visit %@";
"accessibility.tabs.timeline.content-hashtag-%@" = "Hashtag %@";
"accessibility.app-account.selector.accounts" = "Account";
"accessibility.app-account.selector.accounts.hint" = "Opens options sheet.";
"accessibility.tabs.profile.options.label" = "Options";
@ -557,6 +559,7 @@
"accessibility.status.a-replied-to-%@" = "%@ replied to";
"accessibility.image.alt-text-%@" = "Image alt text: %@";
"accessibility.image.alt-text-more.label" = "More alt text available";
"accessibility.tabs.messages.unread.label" = "Unread";
// MARK: Report
"report.comment.placeholder" = "Informazioni aggiuntive";

View file

@ -524,6 +524,8 @@
"accessibility.tabs.timeline.new-post.inputLabel2" = "作成";
"accessibility.tabs.timeline.unread-posts.label-%lld" = "%lld 件の新しい投稿";
"accessibility.tabs.timeline.unread-posts.hint" = "タイムラインをスクロールします。";
"accessibility.tabs.timeline.content-link-%@" = "Visit %@";
"accessibility.tabs.timeline.content-hashtag-%@" = "Hashtag %@";
"accessibility.app-account.selector.accounts" = "アカウント";
"accessibility.app-account.selector.accounts.hint" = "オプション シートを開きます。";
"accessibility.tabs.profile.options.label" = "オプション";
@ -557,6 +559,7 @@
"accessibility.status.a-replied-to-%@" = "%@ に返信";
"accessibility.image.alt-text-%@" = "画像の代替テキスト: %@";
"accessibility.image.alt-text-more.label" = "より多くの代替テキストを利用できます";
"accessibility.tabs.messages.unread.label" = "Unread";
// MARK: Report
"report.comment.placeholder" = "追加情報";

View file

@ -526,6 +526,8 @@
"accessibility.tabs.timeline.new-post.inputLabel2" = "Create";
"accessibility.tabs.timeline.unread-posts.label-%lld" = "%lld new posts";
"accessibility.tabs.timeline.unread-posts.hint" = "Scrolls the timeline.";
"accessibility.tabs.timeline.content-link-%@" = "Visit %@";
"accessibility.tabs.timeline.content-hashtag-%@" = "Hashtag %@";
"accessibility.app-account.selector.accounts" = "계정";
"accessibility.app-account.selector.accounts.hint" = "Opens options sheet.";
"accessibility.tabs.profile.options.label" = "Options";
@ -559,6 +561,7 @@
"accessibility.status.a-replied-to-%@" = "%@ replied to";
"accessibility.image.alt-text-%@" = "Image alt text: %@";
"accessibility.image.alt-text-more.label" = "More alt text available";
"accessibility.tabs.messages.unread.label" = "Unread";
// MARK: Report
"report.comment.placeholder" = "추가 정보";

View file

@ -524,6 +524,8 @@
"accessibility.tabs.timeline.new-post.inputLabel2" = "Create";
"accessibility.tabs.timeline.unread-posts.label-%lld" = "%lld new posts";
"accessibility.tabs.timeline.unread-posts.hint" = "Scrolls the timeline.";
"accessibility.tabs.timeline.content-link-%@" = "Visit %@";
"accessibility.tabs.timeline.content-hashtag-%@" = "Hashtag %@";
"accessibility.app-account.selector.accounts" = "Accounts";
"accessibility.app-account.selector.accounts.hint" = "Opens options sheet.";
"accessibility.tabs.profile.options.label" = "Options";
@ -557,6 +559,7 @@
"accessibility.status.a-replied-to-%@" = "%@ replied to";
"accessibility.image.alt-text-%@" = "Image alt text: %@";
"accessibility.image.alt-text-more.label" = "More alt text available";
"accessibility.tabs.messages.unread.label" = "Unread";
// MARK: Report
"report.comment.placeholder" = "Additional Info";

View file

@ -521,6 +521,8 @@
"accessibility.tabs.timeline.new-post.inputLabel2" = "Maak aan";
"accessibility.tabs.timeline.unread-posts.label-%lld" = "%lld nieuwe posts";
"accessibility.tabs.timeline.unread-posts.hint" = "Scrollt de tijdlijn.";
"accessibility.tabs.timeline.content-link-%@" = "Visit %@";
"accessibility.tabs.timeline.content-hashtag-%@" = "Hashtag %@";
"accessibility.app-account.selector.accounts" = "Accounts";
"accessibility.app-account.selector.accounts.hint" = "Opent paneel voor opties.";
"accessibility.tabs.profile.options.label" = "Opties";
@ -554,6 +556,7 @@
"accessibility.status.a-replied-to-%@" = "%@ heeft geantwoord op";
"accessibility.image.alt-text-%@" = "Tekst voor afbeedling: %@";
"accessibility.image.alt-text-more.label" = "Meer tekst beschikbaar";
"accessibility.tabs.messages.unread.label" = "Unread";
// MARK: Report
"report.comment.placeholder" = "Aanvullende informatie";

View file

@ -515,6 +515,8 @@
"accessibility.tabs.timeline.new-post.inputLabel2" = "Create";
"accessibility.tabs.timeline.unread-posts.label-%lld" = "%lld new posts";
"accessibility.tabs.timeline.unread-posts.hint" = "Scrolls the timeline.";
"accessibility.tabs.timeline.content-link-%@" = "Visit %@";
"accessibility.tabs.timeline.content-hashtag-%@" = "Hashtag %@";
"accessibility.app-account.selector.accounts" = "Konta";
"accessibility.app-account.selector.accounts.hint" = "Opens options sheet.";
"accessibility.tabs.profile.options.label" = "Opcje";
@ -548,6 +550,7 @@
"accessibility.status.a-replied-to-%@" = "%@ odpowiedział do";
"accessibility.image.alt-text-%@" = "Tekst alternatywny obrazka: %@";
"accessibility.image.alt-text-more.label" = "Dostępna jest większa ilość tekstu alternatywnego";
"accessibility.tabs.messages.unread.label" = "Unread";
// MARK: Report
"report.comment.placeholder" = "Informacja dodatkowa";

View file

@ -524,6 +524,8 @@
"accessibility.tabs.timeline.new-post.inputLabel2" = "Create";
"accessibility.tabs.timeline.unread-posts.label-%lld" = "%lld new posts";
"accessibility.tabs.timeline.unread-posts.hint" = "Scrolls the timeline.";
"accessibility.tabs.timeline.content-link-%@" = "Visit %@";
"accessibility.tabs.timeline.content-hashtag-%@" = "Hashtag %@";
"accessibility.app-account.selector.accounts" = "Contas";
"accessibility.app-account.selector.accounts.hint" = "Opens options sheet.";
"accessibility.tabs.profile.options.label" = "Options";
@ -557,6 +559,7 @@
"accessibility.status.a-replied-to-%@" = "%@ replied to";
"accessibility.image.alt-text-%@" = "Image alt text: %@";
"accessibility.image.alt-text-more.label" = "More alt text available";
"accessibility.tabs.messages.unread.label" = "Unread";
// MARK: Report
"report.comment.placeholder" = "Informação Adicional";

View file

@ -524,6 +524,8 @@
"accessibility.tabs.timeline.new-post.inputLabel2" = "Create";
"accessibility.tabs.timeline.unread-posts.label-%lld" = "%lld new posts";
"accessibility.tabs.timeline.unread-posts.hint" = "Scrolls the timeline.";
"accessibility.tabs.timeline.content-link-%@" = "Visit %@";
"accessibility.tabs.timeline.content-hashtag-%@" = "Hashtag %@";
"accessibility.app-account.selector.accounts" = "Accounts";
"accessibility.app-account.selector.accounts.hint" = "Opens options sheet.";
"accessibility.tabs.profile.options.label" = "Options";
@ -557,6 +559,7 @@
"accessibility.status.a-replied-to-%@" = "%@ replied to";
"accessibility.image.alt-text-%@" = "Image alt text: %@";
"accessibility.image.alt-text-more.label" = "More alt text available";
"accessibility.tabs.messages.unread.label" = "Unread";
// MARK: Report
"report.comment.placeholder" = "Additional Info";

View file

@ -525,6 +525,8 @@
"accessibility.tabs.timeline.new-post.inputLabel2" = "Create";
"accessibility.tabs.timeline.unread-posts.label-%lld" = "%lld new posts";
"accessibility.tabs.timeline.unread-posts.hint" = "Scrolls the timeline.";
"accessibility.tabs.timeline.content-link-%@" = "Visit %@";
"accessibility.tabs.timeline.content-hashtag-%@" = "Hashtag %@";
"accessibility.app-account.selector.accounts" = "Профілі";
"accessibility.app-account.selector.accounts.hint" = "Opens options sheet.";
"accessibility.tabs.profile.options.label" = "Options";
@ -558,6 +560,7 @@
"accessibility.status.a-replied-to-%@" = "%@ replied to";
"accessibility.image.alt-text-%@" = "Image alt text: %@";
"accessibility.image.alt-text-more.label" = "More alt text available";
"accessibility.tabs.messages.unread.label" = "Unread";
// MARK: Report
"report.comment.placeholder" = "Додаткова інформація";

View file

@ -559,6 +559,7 @@
"accessibility.status.a-replied-to-%@" = "%@ 回复给";
"accessibility.image.alt-text-%@" = "图片描述文本:%@";
"accessibility.image.alt-text-more.label" = "更多描述文本可用";
"accessibility.tabs.messages.unread.label" = "Unread";
// MARK: Report
"report.comment.placeholder" = "附加信息";

View file

@ -524,6 +524,8 @@
"accessibility.tabs.timeline.new-post.inputLabel2" = "Create";
"accessibility.tabs.timeline.unread-posts.label-%lld" = "%lld new posts";
"accessibility.tabs.timeline.unread-posts.hint" = "Scrolls the timeline.";
"accessibility.tabs.timeline.content-link-%@" = "Visit %@";
"accessibility.tabs.timeline.content-hashtag-%@" = "Hashtag %@";
"accessibility.app-account.selector.accounts" = "帳號";
"accessibility.app-account.selector.accounts.hint" = "Opens options sheet.";
"accessibility.tabs.profile.options.label" = "Options";
@ -557,6 +559,7 @@
"accessibility.status.a-replied-to-%@" = "%@ replied to";
"accessibility.image.alt-text-%@" = "Image alt text: %@";
"accessibility.image.alt-text-more.label" = "More alt text available";
"accessibility.tabs.messages.unread.label" = "Unread";
// MARK: Report
"report.comment.placeholder" = "附加資訊";

View file

@ -15,9 +15,16 @@ struct ConversationsListRow: View {
@ObservedObject var viewModel: ConversationsListViewModel
var body: some View {
Button {
Task {
await viewModel.markAsRead(conversation: conversation)
}
routerPath.navigate(to: .conversationDetail(conversation: conversation))
} label: {
VStack(alignment: .leading) {
HStack(alignment: .top, spacing: 8) {
AvatarView(url: conversation.accounts.first!.avatar)
.accessibilityHidden(true)
VStack(alignment: .leading, spacing: 4) {
HStack {
EmojiTextApp(.init(stringValue: conversation.accounts.map { $0.safeDisplayName }.joined(separator: ", ")),
@ -34,6 +41,10 @@ struct ConversationsListRow: View {
Circle()
.foregroundColor(theme.tintColor)
.frame(width: 10, height: 10)
.accessibilityRepresentation {
Text("accessibility.tabs.messages.unread.label")
}
.accessibilitySortPriority(1)
}
if let message = conversation.lastStatus {
Text(message.createdAt.relativeFormatted)
@ -46,24 +57,33 @@ struct ConversationsListRow: View {
.foregroundColor(theme.labelColor)
.emojiSize(Font.scaledBodyFont.emojiSize)
.emojiBaselineOffset(Font.scaledBodyFont.emojiBaselineOffset)
.accessibilityLabel(conversation.lastStatus?.content.asRawText ?? "")
}
Spacer()
}
.contentShape(Rectangle())
.onTapGesture {
Task {
await viewModel.markAsRead(conversation: conversation)
}
routerPath.navigate(to: .conversationDetail(conversation: conversation))
}
.padding(.top, 4)
if conversation.lastStatus != nil {
actionsView
.padding(.bottom, 4)
.accessibilityHidden(true)
}
}
.contextMenu {
contextMenu
.accessibilityHidden(true)
}
.accessibilityElement(children: .combine)
.accessibilityActions {
replyAction
contextMenu
accessibilityActions
}
.accessibilityAction(.magicTap) {
if let lastStatus = conversation.lastStatus {
HapticManager.shared.fireHaptic(of: .notification(.success))
routerPath.presentedSheet = .replyToStatusEditor(status: lastStatus)
}
}
}
}
@ -88,6 +108,7 @@ struct ConversationsListRow: View {
@ViewBuilder
private var contextMenu: some View {
if conversation.unread {
Button {
Task {
await viewModel.markAsRead(conversation: conversation)
@ -95,6 +116,7 @@ struct ConversationsListRow: View {
} label: {
Label("conversations.action.mark-read", systemImage: "eye")
}
}
if let message = conversation.lastStatus {
Section("conversations.latest.message") {
@ -152,4 +174,53 @@ struct ConversationsListRow: View {
systemImage: conversation.lastStatus?.bookmarked ?? false ? "bookmark.fill" : "bookmark")
}
}
// MARK: - Accessibility actions
@ViewBuilder
var replyAction: some View {
if let lastStatus = conversation.lastStatus {
Button("status.action.reply") {
HapticManager.shared.fireHaptic(of: .notification(.success))
routerPath.presentedSheet = .replyToStatusEditor(status: lastStatus)
}
} else {
EmptyView()
}
}
@ViewBuilder
private var accessibilityActions: some View {
if let lastStatus = conversation.lastStatus {
if lastStatus.account.id != currentAccount.account?.id {
Button("@\(lastStatus.account.username)") {
HapticManager.shared.fireHaptic(of: .notification(.success))
routerPath.navigate(to: .accountDetail(id: lastStatus.account.id))
}
}
// Add in each detected link in the content
ForEach(lastStatus.content.links) { link in
switch link.type {
case .url:
if UIApplication.shared.canOpenURL(link.url) {
Button("accessibility.tabs.timeline.content-link-\(link.title)") {
HapticManager.shared.fireHaptic(of: .notification(.success))
_ = routerPath.handle(url: link.url)
}
}
case .hashtag:
Button("accessibility.tabs.timeline.content-hashtag-\(link.title)") {
HapticManager.shared.fireHaptic(of: .notification(.success))
_ = routerPath.handle(url: link.url)
}
case .mention:
Button("\(link.title)") {
HapticManager.shared.fireHaptic(of: .notification(.success))
_ = routerPath.handle(url: link.url)
}
}
}
}
}
}

View file

@ -11,6 +11,7 @@ public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable {
public var asMarkdown: String = ""
public var asRawText: String = ""
public var statusesURLs = [URL]()
public var links = [Link]()
public var asSafeMarkdownAttributedString: AttributedString = .init()
private var main_regex: NSRegularExpression?
@ -75,6 +76,15 @@ public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable {
} catch {
asSafeMarkdownAttributedString = AttributedString(stringLiteral: htmlValue)
}
links = asSafeMarkdownAttributedString.runs
.compactMap { run in
guard let link = run.link else {
return nil
}
return Link(link, displayString: String(self.asSafeMarkdownAttributedString[run.range].characters))
}
}
public init(stringValue: String, parseMarkdown: Bool = false) {
@ -94,6 +104,15 @@ public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable {
} else {
asSafeMarkdownAttributedString = AttributedString(stringLiteral: htmlValue)
}
links = asSafeMarkdownAttributedString.runs
.compactMap { run in
guard let link = run.link else {
return nil
}
return Link(link, displayString: String(self.asSafeMarkdownAttributedString[run.range].characters))
}
}
public func encode(to encoder: Encoder) throws {
@ -167,4 +186,39 @@ public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable {
}
} catch {}
}
public struct Link: Hashable, Identifiable {
public var id: Int { hashValue }
public let url: AttributeScopes.FoundationAttributes.LinkAttribute.Value
public let displayString: String
public let type: LinkType
public let title: String
init(_ url: AttributeScopes.FoundationAttributes.LinkAttribute.Value, displayString: String) {
self.url = url
self.displayString = displayString
switch displayString.first {
case "@":
self.type = .mention
self.title = displayString
case "#":
self.type = .hashtag
self.title = String(displayString.dropFirst())
default:
self.type = .url
var hostNameUrl = url.host ?? url.absoluteString
if hostNameUrl.hasPrefix("www.") {
hostNameUrl = String(hostNameUrl.dropFirst(4))
}
self.title = hostNameUrl
}
}
public enum LinkType {
case url
case mention
case hashtag
}
}
}

View file

@ -19,13 +19,17 @@ struct NotificationRowView: View {
HStack(alignment: .top, spacing: 8) {
if notification.accounts.count == 1 {
makeAvatarView(type: notification.type)
.accessibilityHidden(true)
} else {
makeNotificationIconView(type: notification.type)
.frame(width: AvatarView.Size.status.size.width,
height: AvatarView.Size.status.size.height)
.accessibilityHidden(true)
}
VStack(alignment: .leading, spacing: 2) {
makeMainLabel(type: notification.type)
// The main label is redundant for mentions
.accessibilityHidden(notification.type == .mention)
makeContent(type: notification.type)
if notification.type == .follow_request,
followRequests.map(\.id).contains(notification.accounts[0].id)
@ -34,6 +38,12 @@ struct NotificationRowView: View {
}
}
}
.accessibilityElement(children: .combine)
.accessibilityActions {
if notification.type == .follow {
accessibilityUserActions
}
}
.alignmentGuide(.listRowSeparatorLeading) { _ in
-100
}
@ -115,6 +125,7 @@ struct NotificationRowView: View {
Text("")
Text(Image(systemName: status.visibility.iconName))
}
.accessibilityHidden(true)
.font(.scaledFootnote)
.fontWeight(.regular)
.foregroundColor(.gray)
@ -130,6 +141,7 @@ struct NotificationRowView: View {
routerPath.navigate(to: .accountsList(accounts: notification.accounts))
}
}
.accessibilityElement(children: .combine)
}
@ViewBuilder
@ -161,6 +173,7 @@ struct NotificationRowView: View {
if type == .follow {
EmojiTextApp(notification.accounts[0].note,
emojis: notification.accounts[0].emojis)
.accessibilityLabel(notification.accounts[0].note.asRawText)
.lineLimit(3)
.font(.scaledCallout)
.emojiSize(Font.scaledCalloutFont.emojiSize)
@ -169,6 +182,7 @@ struct NotificationRowView: View {
.environment(\.openURL, OpenURLAction { url in
routerPath.handle(url: url)
})
.accessibilityAddTraits(.isButton)
}
}
.contentShape(Rectangle())
@ -181,4 +195,16 @@ struct NotificationRowView: View {
}
}
}
// MARK: - Accessibility actions
@ViewBuilder
private var accessibilityUserActions: some View {
ForEach(notification.accounts) { account in
Button("@\(account.username)") {
HapticManager.shared.fireHaptic(of: .notification(.success))
routerPath.navigate(to: .accountDetail(id: account.id))
}
}
}
}

View file

@ -175,5 +175,6 @@ public struct NotificationsListView: View {
.listRowSeparator(.hidden)
.listRowInsets(.init())
.frame(height: .layoutPadding)
.accessibilityHidden(true)
}
}

View file

@ -191,5 +191,6 @@ public struct StatusDetailView: View {
.listRowSeparator(.hidden)
.listRowInsets(.init())
.frame(height: .layoutPadding)
.accessibilityHidden(true)
}
}

View file

@ -27,6 +27,7 @@ public struct StatusEmbeddedView: View {
client: client,
routerPath: routerPath,
showActions: false) })
.accessibilityLabel(status.content.asRawText)
.environment(\.isCompact, true)
}
Spacer()
@ -39,6 +40,7 @@ public struct StatusEmbeddedView: View {
.stroke(.gray.opacity(0.35), lineWidth: 1)
)
.padding(.top, 8)
.accessibilityElement(children: .combine)
}
private func makeAccountView(account: Account) -> some View {

View file

@ -11,6 +11,7 @@ public struct StatusRowView: View {
@Environment(\.isInCaptureMode) private var isInCaptureMode: Bool
@Environment(\.redactionReasons) private var reasons
@Environment(\.isCompact) private var isCompact: Bool
@Environment(\.accessibilityEnabled) private var accessibilityEnabled
@EnvironmentObject private var theme: Theme
@ -95,13 +96,13 @@ public struct StatusRowView: View {
}
.swipeActions(edge: .trailing) {
// The actions associated with the swipes are exposed as custom accessibility actions and there is no way to remove them.
if !isCompact, UIAccessibility.isVoiceOverRunning == false {
if !isCompact, accessibilityEnabled == false {
StatusRowSwipeView(viewModel: viewModel, mode: .trailing)
}
}
.swipeActions(edge: .leading) {
// The actions associated with the swipes are exposed as custom accessibility actions and there is no way to remove them.
if !isCompact, UIAccessibility.isVoiceOverRunning == false {
if !isCompact, accessibilityEnabled == false {
StatusRowSwipeView(viewModel: viewModel, mode: .leading)
}
}
@ -111,19 +112,16 @@ public struct StatusRowView: View {
bottom: 12,
trailing: .layoutPadding))
.accessibilityElement(children: viewModel.isFocused ? .contain : .combine)
.accessibilityLabel(viewModel.isFocused == false && UIAccessibility.isVoiceOverRunning
.accessibilityLabel(viewModel.isFocused == false && accessibilityEnabled
? CombinedAccessibilityLabel(viewModel: viewModel).finalLabel() : Text(""))
.accessibilityCustomContent(
LocalizedStringKey("accessibility.status.spoiler-full-content"),
viewModel.finalStatus.content.asRawText,
importance: .high
)
.accessibilityAction {
viewModel.navigateToDetail()
}
.accessibilityActions {
if viewModel.showActions {
accessibilityActions
}
}
.background {
Color.clear
.contentShape(Rectangle())
@ -161,11 +159,15 @@ public struct StatusRowView: View {
@ViewBuilder
private var accessibilityActions: some View {
// Add the individual mentions as accessibility actions
ForEach(viewModel.status.mentions, id: \.id) { mention in
Button("@\(mention.username)") {
viewModel.routerPath.navigate(to: .accountDetail(id: mention.id))
// Add reply and quote, which are lost when the swipe actions are removed
Button("status.action.reply") {
HapticManager.shared.fireHaptic(of: .notification(.success))
viewModel.routerPath.presentedSheet = .replyToStatusEditor(status: viewModel.status)
}
Button("settings.swipeactions.status.action.quote") {
HapticManager.shared.fireHaptic(of: .notification(.success))
viewModel.routerPath.presentedSheet = .quoteStatusEditor(status: viewModel.status)
}
Button(viewModel.displaySpoiler ? "status.show-more" : "status.show-less") {
@ -175,8 +177,40 @@ public struct StatusRowView: View {
}
Button("@\(viewModel.status.account.username)") {
HapticManager.shared.fireHaptic(of: .notification(.success))
viewModel.routerPath.navigate(to: .accountDetail(id: viewModel.status.account.id))
}
// Add a reference to the post creator
if viewModel.status.account != viewModel.finalStatus.account {
Button("@\(viewModel.finalStatus.account.username)") {
HapticManager.shared.fireHaptic(of: .notification(.success))
viewModel.routerPath.navigate(to: .accountDetail(id: viewModel.finalStatus.account.id))
}
}
// Add in each detected link in the content
ForEach(viewModel.finalStatus.content.links) { link in
switch link.type {
case .url:
if UIApplication.shared.canOpenURL(link.url) {
Button("accessibility.tabs.timeline.content-link-\(link.title)") {
HapticManager.shared.fireHaptic(of: .notification(.success))
_ = viewModel.routerPath.handle(url: link.url)
}
}
case .hashtag:
Button("accessibility.tabs.timeline.content-hashtag-\(link.title)") {
HapticManager.shared.fireHaptic(of: .notification(.success))
_ = viewModel.routerPath.handle(url: link.url)
}
case .mention:
Button("\(link.title)") {
HapticManager.shared.fireHaptic(of: .notification(.success))
_ = viewModel.routerPath.handle(url: link.url)
}
}
}
}
private func makeFilterView(filter: Filter) -> some View {
@ -234,11 +268,11 @@ private struct CombinedAccessibilityLabel {
Text(hasSpoiler
? viewModel.finalStatus.spoilerText.asRawText
: viewModel.finalStatus.content.asRawText
) + Text(", ") +
) +
Text(hasSpoiler
? "status.editor.spoiler"
: ""
) + Text(", ") +
) +
imageAltText() + Text(", ") +
Text(viewModel.finalStatus.createdAt.relativeFormatted) + Text(", ") +
Text("status.summary.n-replies \(viewModel.finalStatus.repliesCount)") + Text(", ") +

View file

@ -44,6 +44,7 @@ struct StatusRowContentView: View {
Spacer()
}
}
.accessibilityHidden(viewModel.isFocused == false)
.padding(.vertical, 4)
}

View file

@ -27,10 +27,12 @@ public struct TimelineView: View {
@Binding var timeline: TimelineFilter
@Binding var scrollToTopSignal: Int
private let canFilterTimeline: Bool
public init(timeline: Binding<TimelineFilter>, scrollToTopSignal: Binding<Int>) {
public init(timeline: Binding<TimelineFilter>, scrollToTopSignal: Binding<Int>, canFilterTimeline: Bool) {
_timeline = timeline
_scrollToTopSignal = scrollToTopSignal
self.canFilterTimeline = canFilterTimeline
}
public var body: some View {
@ -99,9 +101,14 @@ public struct TimelineView: View {
}
}
.accessibilityRepresentation {
if canFilterTimeline {
Menu(timeline.localizedTitle()) {}
} else {
Text(timeline.localizedTitle())
}
}
.accessibilityAddTraits(.isHeader)
.accessibilityRemoveTraits(.isButton)
}
}
.navigationBarTitleDisplayMode(.inline)
@ -172,6 +179,7 @@ public struct TimelineView: View {
.font(.scaledFootnote)
.foregroundColor(.gray)
}
.accessibilityElement(children: .combine)
Spacer()
Button {
Task {