feat: add accessibility supports for timeline
This commit is contained in:
parent
108c6af575
commit
6ba6598b96
|
@ -93,6 +93,22 @@
|
|||
},
|
||||
"time_left": "%s left",
|
||||
"closed": "Closed"
|
||||
},
|
||||
"actions": {
|
||||
"reply": "Reply",
|
||||
"reblog": "Reblog",
|
||||
"unreblog": "Unreblog",
|
||||
"favorite": "Favorite",
|
||||
"unfavorite": "Unfavorite",
|
||||
"menu": "Menu"
|
||||
},
|
||||
"tag": {
|
||||
"url": "URL",
|
||||
"mention": "Mention",
|
||||
"link": "Link",
|
||||
"hashtag": "Hashtag",
|
||||
"email": "Email",
|
||||
"emoji": "Emoji"
|
||||
}
|
||||
},
|
||||
"firendship": {
|
||||
|
@ -125,6 +141,11 @@
|
|||
"blocked_warning": "You can’t view Artbot’s profile\n until they unblock you.",
|
||||
"suspended_warning": "This account has been suspended.",
|
||||
"user_suspended_warning": "%s's account has been suspended."
|
||||
},
|
||||
"accessibility": {
|
||||
"count_replies": "%s replies",
|
||||
"count_reblogs": "%s reblogs",
|
||||
"count_favorites": "%s favorites"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -756,6 +756,7 @@
|
|||
DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveUserInterfaceStyleBarButtonItem.swift; sourceTree = "<group>"; };
|
||||
DB084B5625CBC56C00F898ED /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = "<group>"; };
|
||||
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = "<group>"; };
|
||||
DB0F814C264BBDD900F2A12B /* ActiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ActiveLabel.swift; path = ../ActiveLabel.swift; sourceTree = "<group>"; };
|
||||
DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDimmableButton.swift; sourceTree = "<group>"; };
|
||||
DB1D186B25EF5BA7003F1F23 /* PollTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollTableView.swift; sourceTree = "<group>"; };
|
||||
|
@ -1645,6 +1646,7 @@
|
|||
children = (
|
||||
DBF53F5F25C14E88008AAC7B /* Mastodon.xctestplan */,
|
||||
DBF53F6025C14E9D008AAC7B /* MastodonSDK.xctestplan */,
|
||||
DB0F814C264BBDD900F2A12B /* ActiveLabel.swift */,
|
||||
DB3D0FED25BAA42200EAA174 /* MastodonSDK */,
|
||||
DB427DD425BAA00100D1B89D /* Mastodon */,
|
||||
DB427DEB25BAA00100D1B89D /* MastodonTests */,
|
||||
|
|
|
@ -58,6 +58,7 @@ extension StatusSection {
|
|||
)
|
||||
}
|
||||
cell.delegate = statusTableViewCellDelegate
|
||||
cell.isAccessibilityElement = true
|
||||
return cell
|
||||
case .status(let objectID, let attribute),
|
||||
.root(let objectID, let attribute),
|
||||
|
@ -97,7 +98,22 @@ extension StatusSection {
|
|||
}
|
||||
}
|
||||
cell.delegate = statusTableViewCellDelegate
|
||||
|
||||
switch item {
|
||||
case .root:
|
||||
cell.statusView.activeTextLabel.isAccessibilityElement = false
|
||||
var accessibilityElements: [Any] = []
|
||||
accessibilityElements.append(cell.statusView.nameLabel)
|
||||
accessibilityElements.append(cell.statusView.dateLabel)
|
||||
accessibilityElements.append(contentsOf: cell.statusView.activeTextLabel.createAccessibilityElements())
|
||||
accessibilityElements.append(contentsOf: cell.statusView.statusMosaicImageViewContainer.imageViews)
|
||||
accessibilityElements.append(cell.statusView.playerContainerView)
|
||||
accessibilityElements.append(cell.statusView.actionToolbarContainer)
|
||||
accessibilityElements.append(cell.threadMetaView)
|
||||
cell.accessibilityElements = accessibilityElements
|
||||
default:
|
||||
cell.isAccessibilityElement = true
|
||||
cell.accessibilityElements = nil
|
||||
}
|
||||
return cell
|
||||
case .leafBottomLoader:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ThreadReplyLoaderTableViewCell.self), for: indexPath) as! ThreadReplyLoaderTableViewCell
|
||||
|
@ -179,6 +195,7 @@ extension StatusSection {
|
|||
}()
|
||||
cell.statusView.nameLabel.configure(content: nameText, emojiDict: (status.reblog ?? status).author.emojiDict)
|
||||
cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct
|
||||
|
||||
// set avatar
|
||||
if let reblog = status.reblog {
|
||||
cell.statusView.avatarButton.isHidden = true
|
||||
|
@ -196,6 +213,7 @@ extension StatusSection {
|
|||
content: (status.reblog ?? status).content,
|
||||
emojiDict: (status.reblog ?? status).emojiDict
|
||||
)
|
||||
cell.statusView.activeTextLabel.accessibilityLanguage = (status.reblog ?? status).language
|
||||
|
||||
// set visibility
|
||||
if let visibility = (status.reblog ?? status).visibility {
|
||||
|
@ -275,6 +293,7 @@ extension StatusSection {
|
|||
break
|
||||
}
|
||||
}
|
||||
imageView.accessibilityLabel = meta.altText
|
||||
Publishers.CombineLatest(
|
||||
statusItemAttribute.isImageLoaded,
|
||||
statusItemAttribute.isRevealing
|
||||
|
@ -452,6 +471,7 @@ extension StatusSection {
|
|||
.sink { [weak cell] _ in
|
||||
guard let cell = cell else { return }
|
||||
cell.statusView.dateLabel.text = createdAt.shortTimeAgoSinceNow
|
||||
cell.statusView.dateLabel.accessibilityLabel = createdAt.timeAgoSinceNow
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
|
||||
|
@ -572,6 +592,7 @@ extension StatusSection {
|
|||
formatter.timeStyle = .short
|
||||
return formatter.string(from: status.createdAt)
|
||||
}()
|
||||
cell.threadMetaView.dateLabel.accessibilityLabel = DateFormatter.localizedString(from: status.createdAt, dateStyle: .medium, timeStyle: .short)
|
||||
let reblogCountTitle: String = {
|
||||
let count = status.reblogsCount.intValue
|
||||
if count > 1 {
|
||||
|
@ -609,6 +630,7 @@ extension StatusSection {
|
|||
return L10n.Common.Controls.Status.userReblogged(name)
|
||||
}()
|
||||
cell.statusView.headerInfoLabel.configure(content: headerText, emojiDict: status.author.emojiDict)
|
||||
cell.statusView.headerInfoLabel.isAccessibilityElement = true
|
||||
} else if status.inReplyToID != nil {
|
||||
cell.statusView.headerContainerView.isHidden = false
|
||||
cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage)
|
||||
|
@ -621,8 +643,10 @@ extension StatusSection {
|
|||
return L10n.Common.Controls.Status.userRepliedTo(name)
|
||||
}()
|
||||
cell.statusView.headerInfoLabel.configure(content: headerText, emojiDict: status.replyTo?.author.emojiDict ?? [:])
|
||||
cell.statusView.headerInfoLabel.isAccessibilityElement = true
|
||||
} else {
|
||||
cell.statusView.headerContainerView.isHidden = true
|
||||
cell.statusView.headerInfoLabel.isAccessibilityElement = false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -640,6 +664,9 @@ extension StatusSection {
|
|||
return StatusSection.formattedNumberTitleForActionButton(count)
|
||||
}()
|
||||
cell.statusView.actionToolbarContainer.replyButton.setTitle(replyCountTitle, for: .normal)
|
||||
cell.statusView.actionToolbarContainer.replyButton.accessibilityValue = status.repliesCount.flatMap {
|
||||
L10n.Common.Controls.Timeline.Accessibility.countReplies($0.intValue)
|
||||
} ?? nil
|
||||
// set reblog
|
||||
let isReblogged = status.rebloggedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false
|
||||
let reblogCountTitle: String = {
|
||||
|
@ -648,6 +675,11 @@ extension StatusSection {
|
|||
}()
|
||||
cell.statusView.actionToolbarContainer.reblogButton.setTitle(reblogCountTitle, for: .normal)
|
||||
cell.statusView.actionToolbarContainer.isReblogButtonHighlight = isReblogged
|
||||
cell.statusView.actionToolbarContainer.reblogButton.accessibilityLabel = isReblogged ? L10n.Common.Controls.Status.Actions.unreblog : L10n.Common.Controls.Status.Actions.reblog
|
||||
cell.statusView.actionToolbarContainer.reblogButton.accessibilityValue = {
|
||||
guard status.reblogsCount.intValue > 0 else { return nil }
|
||||
return L10n.Common.Controls.Timeline.Accessibility.countReblogs(status.reblogsCount.intValue)
|
||||
}()
|
||||
// set like
|
||||
let isLike = status.favouritedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false
|
||||
let favoriteCountTitle: String = {
|
||||
|
@ -656,7 +688,11 @@ extension StatusSection {
|
|||
}()
|
||||
cell.statusView.actionToolbarContainer.favoriteButton.setTitle(favoriteCountTitle, for: .normal)
|
||||
cell.statusView.actionToolbarContainer.isFavoriteButtonHighlight = isLike
|
||||
|
||||
cell.statusView.actionToolbarContainer.favoriteButton.accessibilityLabel = isLike ? L10n.Common.Controls.Status.Actions.unfavorite : L10n.Common.Controls.Status.Actions.favorite
|
||||
cell.statusView.actionToolbarContainer.favoriteButton.accessibilityValue = {
|
||||
guard status.favouritesCount.intValue > 0 else { return nil }
|
||||
return L10n.Common.Controls.Timeline.Accessibility.countReblogs(status.favouritesCount.intValue)
|
||||
}()
|
||||
Publishers.CombineLatest(
|
||||
dependency.context.blockDomainService.blockedDomains,
|
||||
ManagedObjectObserver.observe(object: status.authorForUserProvider)
|
||||
|
|
|
@ -32,6 +32,8 @@ extension ActiveLabel {
|
|||
text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
|
||||
#endif
|
||||
|
||||
accessibilityContainerType = .semanticGroup
|
||||
|
||||
switch style {
|
||||
case .default:
|
||||
font = .preferredFont(forTextStyle: .body)
|
||||
|
@ -61,8 +63,10 @@ extension ActiveLabel {
|
|||
if let parseResult = try? MastodonStatusContent.parse(content: content, emojiDict: emojiDict) {
|
||||
text = parseResult.trimmed
|
||||
activeEntities = parseResult.activeEntities
|
||||
accessibilityLabel = parseResult.original
|
||||
} else {
|
||||
text = ""
|
||||
accessibilityLabel = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,5 +83,110 @@ extension ActiveLabel {
|
|||
let parseResult = MastodonField.parse(field: field)
|
||||
text = parseResult.value
|
||||
activeEntities = parseResult.activeEntities
|
||||
accessibilityLabel = parseResult.value
|
||||
}
|
||||
}
|
||||
|
||||
extension ActiveEntity {
|
||||
|
||||
var accessibilityLabelDescription: String {
|
||||
switch self.type {
|
||||
case .email: return L10n.Common.Controls.Status.Tag.email
|
||||
case .hashtag: return L10n.Common.Controls.Status.Tag.hashtag
|
||||
case .mention: return L10n.Common.Controls.Status.Tag.mention
|
||||
case .url: return L10n.Common.Controls.Status.Tag.url
|
||||
case .emoji: return L10n.Common.Controls.Status.Tag.emoji
|
||||
}
|
||||
}
|
||||
|
||||
var accessibilityValueDescription: String {
|
||||
switch self.type {
|
||||
case .email(let text, _): return text
|
||||
case .hashtag(let text, _): return text
|
||||
case .mention(let text, _): return text
|
||||
case .url(_, let trimmed, _, _): return trimmed
|
||||
case .emoji(let text, _, _): return text
|
||||
}
|
||||
}
|
||||
|
||||
func accessibilityElement(in accessibilityContainer: Any) -> ActiveLabelAccessibilityElement? {
|
||||
if case .emoji = self.type {
|
||||
return nil
|
||||
}
|
||||
|
||||
let element = ActiveLabelAccessibilityElement(accessibilityContainer: accessibilityContainer)
|
||||
element.accessibilityTraits = .button
|
||||
element.accessibilityLabel = accessibilityLabelDescription
|
||||
element.accessibilityValue = accessibilityValueDescription
|
||||
return element
|
||||
}
|
||||
}
|
||||
|
||||
final class ActiveLabelAccessibilityElement: UIAccessibilityElement {
|
||||
var index: Int!
|
||||
}
|
||||
|
||||
// MARK: - UIAccessibilityContainer
|
||||
extension ActiveLabel {
|
||||
|
||||
func createAccessibilityElements() -> [UIAccessibilityElement] {
|
||||
var elements: [UIAccessibilityElement] = []
|
||||
|
||||
let element = ActiveLabelAccessibilityElement(accessibilityContainer: self)
|
||||
element.accessibilityTraits = .staticText
|
||||
element.accessibilityLabel = accessibilityLabel
|
||||
element.accessibilityFrame = superview!.convert(frame, to: nil)
|
||||
element.accessibilityLanguage = accessibilityLanguage
|
||||
elements.append(element)
|
||||
|
||||
for eneity in activeEntities {
|
||||
guard let element = eneity.accessibilityElement(in: self) else { continue }
|
||||
var glyphRange = NSRange()
|
||||
layoutManager.characterRange(forGlyphRange: eneity.range, actualGlyphRange: &glyphRange)
|
||||
let rect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
|
||||
element.accessibilityFrame = self.convert(rect, to: nil)
|
||||
element.accessibilityContainer = self
|
||||
elements.append(element)
|
||||
}
|
||||
|
||||
return elements
|
||||
}
|
||||
|
||||
// public override func accessibilityElementCount() -> Int {
|
||||
// return 1 + activeEntities.count
|
||||
// }
|
||||
//
|
||||
// public override func accessibilityElement(at index: Int) -> Any? {
|
||||
// if index == 0 {
|
||||
// let element = ActiveLabelAccessibilityElement(accessibilityContainer: self)
|
||||
// element.accessibilityTraits = .staticText
|
||||
// element.accessibilityLabel = accessibilityLabel
|
||||
// element.accessibilityFrame = superview!.convert(frame, to: nil)
|
||||
// element.index = index
|
||||
// return element
|
||||
// }
|
||||
//
|
||||
// let index = index - 1
|
||||
// guard index < activeEntities.count else { return nil }
|
||||
// let eneity = activeEntities[index]
|
||||
// guard let element = eneity.accessibilityElement(in: self) else { return nil }
|
||||
//
|
||||
// var glyphRange = NSRange()
|
||||
// layoutManager.characterRange(forGlyphRange: eneity.range, actualGlyphRange: &glyphRange)
|
||||
// let rect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
|
||||
// element.accessibilityFrame = self.convert(rect, to: nil)
|
||||
// element.accessibilityContainer = self
|
||||
//
|
||||
// return element
|
||||
// }
|
||||
//
|
||||
// public override func index(ofAccessibilityElement element: Any) -> Int {
|
||||
// guard let element = element as? ActiveLabelAccessibilityElement,
|
||||
// let index = element.index else {
|
||||
// return NSNotFound
|
||||
// }
|
||||
//
|
||||
// return index
|
||||
// }
|
||||
|
||||
}
|
||||
|
|
|
@ -208,6 +208,20 @@ internal enum L10n {
|
|||
internal static func userRepliedTo(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Common.Controls.Status.UserRepliedTo", String(describing: p1))
|
||||
}
|
||||
internal enum Actions {
|
||||
/// Favorite
|
||||
internal static let favorite = L10n.tr("Localizable", "Common.Controls.Status.Actions.Favorite")
|
||||
/// Menu
|
||||
internal static let menu = L10n.tr("Localizable", "Common.Controls.Status.Actions.Menu")
|
||||
/// Reblog
|
||||
internal static let reblog = L10n.tr("Localizable", "Common.Controls.Status.Actions.Reblog")
|
||||
/// Reply
|
||||
internal static let reply = L10n.tr("Localizable", "Common.Controls.Status.Actions.Reply")
|
||||
/// Unfavorite
|
||||
internal static let unfavorite = L10n.tr("Localizable", "Common.Controls.Status.Actions.Unfavorite")
|
||||
/// Unreblog
|
||||
internal static let unreblog = L10n.tr("Localizable", "Common.Controls.Status.Actions.Unreblog")
|
||||
}
|
||||
internal enum Poll {
|
||||
/// Closed
|
||||
internal static let closed = L10n.tr("Localizable", "Common.Controls.Status.Poll.Closed")
|
||||
|
@ -238,8 +252,36 @@ internal enum L10n {
|
|||
}
|
||||
}
|
||||
}
|
||||
internal enum Tag {
|
||||
/// Email
|
||||
internal static let email = L10n.tr("Localizable", "Common.Controls.Status.Tag.Email")
|
||||
/// Emoji
|
||||
internal static let emoji = L10n.tr("Localizable", "Common.Controls.Status.Tag.Emoji")
|
||||
/// Hashtag
|
||||
internal static let hashtag = L10n.tr("Localizable", "Common.Controls.Status.Tag.Hashtag")
|
||||
/// Link
|
||||
internal static let link = L10n.tr("Localizable", "Common.Controls.Status.Tag.Link")
|
||||
/// Mention
|
||||
internal static let mention = L10n.tr("Localizable", "Common.Controls.Status.Tag.Mention")
|
||||
/// URL
|
||||
internal static let url = L10n.tr("Localizable", "Common.Controls.Status.Tag.Url")
|
||||
}
|
||||
}
|
||||
internal enum Timeline {
|
||||
internal enum Accessibility {
|
||||
/// %@ favorites
|
||||
internal static func countFavorites(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Common.Controls.Timeline.Accessibility.CountFavorites", String(describing: p1))
|
||||
}
|
||||
/// %@ reblogs
|
||||
internal static func countReblogs(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Common.Controls.Timeline.Accessibility.CountReblogs", String(describing: p1))
|
||||
}
|
||||
/// %@ replies
|
||||
internal static func countReplies(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Common.Controls.Timeline.Accessibility.CountReplies", String(describing: p1))
|
||||
}
|
||||
}
|
||||
internal enum Header {
|
||||
/// You can’t view Artbot’s profile\n until they unblock you.
|
||||
internal static let blockedWarning = L10n.tr("Localizable", "Common.Controls.Timeline.Header.BlockedWarning")
|
||||
|
|
|
@ -59,7 +59,6 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
|||
extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||
|
||||
|
||||
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) {
|
||||
StatusProviderFacade.responseToStatusContentWarningRevealAction(provider: self, cell: cell)
|
||||
}
|
||||
|
@ -76,7 +75,11 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
|||
|
||||
extension StatusTableViewCellDelegate where Self: StatusProvider & MediaPreviewableViewController {
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) {
|
||||
StatusProviderFacade.coordinateToStatusMediaPreviewScene(provider: self, cell: cell, mosaicImageView: mosaicImageViewContainer, didTapImageView: imageView, atIndex: index)
|
||||
if UIAccessibility.isVoiceOverRunning, !(self is ThreadViewController) {
|
||||
StatusProviderFacade.coordinateToStatusThreadScene(for: .primary, provider: self, cell: cell)
|
||||
} else {
|
||||
StatusProviderFacade.coordinateToStatusMediaPreviewScene(provider: self, cell: cell, mosaicImageView: mosaicImageViewContainer, didTapImageView: imageView, atIndex: index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -64,6 +64,12 @@ Please check your internet connection.";
|
|||
"Common.Controls.Firendship.UnblockUser" = "Unblock %@";
|
||||
"Common.Controls.Firendship.Unmute" = "Unmute";
|
||||
"Common.Controls.Firendship.UnmuteUser" = "Unmute %@";
|
||||
"Common.Controls.Status.Actions.Favorite" = "Favorite";
|
||||
"Common.Controls.Status.Actions.Menu" = "Menu";
|
||||
"Common.Controls.Status.Actions.Reblog" = "Reblog";
|
||||
"Common.Controls.Status.Actions.Reply" = "Reply";
|
||||
"Common.Controls.Status.Actions.Unfavorite" = "Unfavorite";
|
||||
"Common.Controls.Status.Actions.Unreblog" = "Unreblog";
|
||||
"Common.Controls.Status.ContentWarning" = "content warning";
|
||||
"Common.Controls.Status.ContentWarningText" = "cw: %@";
|
||||
"Common.Controls.Status.MediaContentWarning" = "Tap to reveal that may be sensitive";
|
||||
|
@ -75,8 +81,17 @@ Please check your internet connection.";
|
|||
"Common.Controls.Status.Poll.VoterCount.Multiple" = "%d voters";
|
||||
"Common.Controls.Status.Poll.VoterCount.Single" = "%d voter";
|
||||
"Common.Controls.Status.ShowPost" = "Show Post";
|
||||
"Common.Controls.Status.Tag.Email" = "Email";
|
||||
"Common.Controls.Status.Tag.Emoji" = "Emoji";
|
||||
"Common.Controls.Status.Tag.Hashtag" = "Hashtag";
|
||||
"Common.Controls.Status.Tag.Link" = "Link";
|
||||
"Common.Controls.Status.Tag.Mention" = "Mention";
|
||||
"Common.Controls.Status.Tag.Url" = "URL";
|
||||
"Common.Controls.Status.UserReblogged" = "%@ reblogged";
|
||||
"Common.Controls.Status.UserRepliedTo" = "Replied to %@";
|
||||
"Common.Controls.Timeline.Accessibility.CountFavorites" = "%@ favorites";
|
||||
"Common.Controls.Timeline.Accessibility.CountReblogs" = "%@ reblogs";
|
||||
"Common.Controls.Timeline.Accessibility.CountReplies" = "%@ replies";
|
||||
"Common.Controls.Timeline.Header.BlockedWarning" = "You can’t view Artbot’s profile
|
||||
until they unblock you.";
|
||||
"Common.Controls.Timeline.Header.BlockingWarning" = "You can’t view Artbot’s profile
|
||||
|
|
|
@ -36,7 +36,7 @@ final class MediaPreviewViewModel: NSObject {
|
|||
switch entity.type {
|
||||
case .image:
|
||||
guard let url = URL(string: entity.url) else { continue }
|
||||
let meta = MediaPreviewImageViewModel.RemoteImagePreviewMeta(url: url, thumbnail: thumbnail)
|
||||
let meta = MediaPreviewImageViewModel.RemoteImagePreviewMeta(url: url, thumbnail: thumbnail, altText: entity.descriptionString)
|
||||
let mediaPreviewImageModel = MediaPreviewImageViewModel(meta: meta)
|
||||
let mediaPreviewImageViewController = MediaPreviewImageViewController()
|
||||
mediaPreviewImageViewController.viewModel = mediaPreviewImageModel
|
||||
|
@ -60,7 +60,7 @@ final class MediaPreviewViewModel: NSObject {
|
|||
managedObjectContext.performAndWait {
|
||||
let account = managedObjectContext.object(with: meta.accountObjectID) as! MastodonUser
|
||||
let avatarURL = account.headerImageURLWithFallback(domain: account.domain)
|
||||
let meta = MediaPreviewImageViewModel.RemoteImagePreviewMeta(url: avatarURL, thumbnail: meta.preloadThumbnailImage)
|
||||
let meta = MediaPreviewImageViewModel.RemoteImagePreviewMeta(url: avatarURL, thumbnail: meta.preloadThumbnailImage, altText: nil)
|
||||
let mediaPreviewImageModel = MediaPreviewImageViewModel(meta: meta)
|
||||
let mediaPreviewImageViewController = MediaPreviewImageViewController()
|
||||
mediaPreviewImageViewController.viewModel = mediaPreviewImageModel
|
||||
|
@ -80,7 +80,7 @@ final class MediaPreviewViewModel: NSObject {
|
|||
managedObjectContext.performAndWait {
|
||||
let account = managedObjectContext.object(with: meta.accountObjectID) as! MastodonUser
|
||||
let avatarURL = account.avatarImageURLWithFallback(domain: account.domain)
|
||||
let meta = MediaPreviewImageViewModel.RemoteImagePreviewMeta(url: avatarURL, thumbnail: meta.preloadThumbnailImage)
|
||||
let meta = MediaPreviewImageViewModel.RemoteImagePreviewMeta(url: avatarURL, thumbnail: meta.preloadThumbnailImage, altText: nil)
|
||||
let mediaPreviewImageModel = MediaPreviewImageViewModel(meta: meta)
|
||||
let mediaPreviewImageViewController = MediaPreviewImageViewController()
|
||||
mediaPreviewImageViewController.viewModel = mediaPreviewImageModel
|
||||
|
|
|
@ -18,6 +18,7 @@ final class MediaPreviewImageView: UIScrollView {
|
|||
imageView.isUserInteractionEnabled = true
|
||||
// accessibility
|
||||
imageView.accessibilityIgnoresInvertColors = true
|
||||
imageView.isAccessibilityElement = true
|
||||
return imageView
|
||||
}()
|
||||
|
||||
|
|
|
@ -76,6 +76,7 @@ extension MediaPreviewImageViewController {
|
|||
guard let image = image else { return }
|
||||
self.previewImageView.imageView.image = image
|
||||
self.previewImageView.setup(image: image, container: self.previewImageView, forceUpdate: true)
|
||||
self.previewImageView.imageView.accessibilityLabel = self.viewModel.altText
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
|
|
@ -17,10 +17,12 @@ class MediaPreviewImageViewModel {
|
|||
|
||||
// output
|
||||
let image: CurrentValueSubject<UIImage?, Never>
|
||||
let altText: String?
|
||||
|
||||
init(meta: RemoteImagePreviewMeta) {
|
||||
self.item = .status(meta)
|
||||
self.image = CurrentValueSubject(meta.thumbnail)
|
||||
self.altText = meta.altText
|
||||
|
||||
let url = meta.url
|
||||
ImageDownloader.default.download(URLRequest(url: url), completion: { [weak self] response in
|
||||
|
@ -38,6 +40,7 @@ class MediaPreviewImageViewModel {
|
|||
init(meta: LocalImagePreviewMeta) {
|
||||
self.item = .local(meta)
|
||||
self.image = CurrentValueSubject(meta.image)
|
||||
self.altText = nil
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -64,6 +67,7 @@ extension MediaPreviewImageViewModel {
|
|||
struct RemoteImagePreviewMeta {
|
||||
let url: URL
|
||||
let thumbnail: UIImage?
|
||||
let altText: String?
|
||||
}
|
||||
|
||||
struct LocalImagePreviewMeta {
|
||||
|
|
|
@ -31,6 +31,7 @@ final class MosaicImageViewContainer: UIView {
|
|||
let tapGesture = UITapGestureRecognizer.singleTapGestureRecognizer
|
||||
tapGesture.addTarget(self, action: #selector(MosaicImageViewContainer.photoTapGestureRecognizerHandler(_:)))
|
||||
imageView.addGestureRecognizer(tapGesture)
|
||||
imageView.isAccessibilityElement = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ class ContentWarningOverlayView: UIView {
|
|||
label.text = L10n.Common.Controls.Status.mediaContentWarning
|
||||
label.textAlignment = .center
|
||||
label.numberOfLines = 0
|
||||
label.isAccessibilityElement = false
|
||||
return label
|
||||
}()
|
||||
|
||||
|
@ -40,6 +41,7 @@ class ContentWarningOverlayView: UIView {
|
|||
label.text = L10n.Common.Controls.Status.mediaContentWarning
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.textAlignment = .center
|
||||
label.isAccessibilityElement = false
|
||||
return label
|
||||
}()
|
||||
let blurContentWarningLabel: UILabel = {
|
||||
|
@ -49,6 +51,7 @@ class ContentWarningOverlayView: UIView {
|
|||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
label.textAlignment = .center
|
||||
label.layer.setupShadow()
|
||||
label.isAccessibilityElement = false
|
||||
return label
|
||||
}()
|
||||
|
||||
|
|
|
@ -96,6 +96,7 @@ final class StatusView: UIView {
|
|||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
label.font = .systemFont(ofSize: 17)
|
||||
label.text = "·"
|
||||
label.isAccessibilityElement = false
|
||||
return label
|
||||
}()
|
||||
|
||||
|
@ -104,6 +105,7 @@ final class StatusView: UIView {
|
|||
label.font = .systemFont(ofSize: 15, weight: .regular)
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
label.text = "@alice"
|
||||
label.isAccessibilityElement = false
|
||||
return label
|
||||
}()
|
||||
|
||||
|
|
|
@ -79,6 +79,10 @@ extension ThreadMetaView {
|
|||
favoriteButton.setContentHuggingPriority(.required - 1, for: .horizontal)
|
||||
|
||||
updateContainerLayout()
|
||||
|
||||
// TODO:
|
||||
reblogButton.isAccessibilityElement = false
|
||||
favoriteButton.isAccessibilityElement = false
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
|
|
|
@ -81,6 +81,7 @@ final class StatusTableViewCell: UITableViewCell, StatusCell {
|
|||
threadMetaView.isHidden = true
|
||||
disposeBag.removeAll()
|
||||
observations.removeAll()
|
||||
isAccessibilityElement = false // reset behavior
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
|
@ -357,3 +358,10 @@ extension StatusTableViewCell: ActionToolbarContainerDelegate {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
extension StatusTableViewCell {
|
||||
override var accessibilityActivationPoint: CGPoint {
|
||||
get { return .zero }
|
||||
set { }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -105,6 +105,11 @@ extension ActionToolbarContainer {
|
|||
let starImage = UIImage(systemName: "star.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 17, weight: .bold))!.withRenderingMode(.alwaysTemplate)
|
||||
let moreImage = UIImage(systemName: "ellipsis", withConfiguration: UIImage.SymbolConfiguration(pointSize: 17, weight: .bold))!.withRenderingMode(.alwaysTemplate)
|
||||
|
||||
replyButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.reply
|
||||
reblogButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.reblog // needs update to follow state
|
||||
favoriteButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.favorite // needs update to follow state
|
||||
moreButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.menu
|
||||
|
||||
switch style {
|
||||
case .inline:
|
||||
buttons.forEach { button in
|
||||
|
@ -194,6 +199,14 @@ extension ActionToolbarContainer {
|
|||
|
||||
}
|
||||
|
||||
extension ActionToolbarContainer {
|
||||
|
||||
override var accessibilityElements: [Any]? {
|
||||
get { [replyButton, reblogButton, favoriteButton, moreButton] }
|
||||
set { }
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
import SwiftUI
|
||||
|
||||
|
|
|
@ -28,7 +28,8 @@ struct MosaicImageViewModel {
|
|||
let mosaicMeta = MosaicMeta(
|
||||
url: url,
|
||||
size: CGSize(width: width, height: height),
|
||||
blurhash: element.blurhash
|
||||
blurhash: element.blurhash,
|
||||
altText: element.descriptionString
|
||||
)
|
||||
metas.append(mosaicMeta)
|
||||
}
|
||||
|
@ -43,6 +44,7 @@ struct MosaicMeta {
|
|||
let url: URL
|
||||
let size: CGSize
|
||||
let blurhash: String?
|
||||
let altText: String?
|
||||
|
||||
let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.MosaicMeta.working-queue", qos: .userInitiated, attributes: .concurrent)
|
||||
|
||||
|
|
Loading…
Reference in New Issue