Merge pull request #552 from j-f1/notifications-tab-a11y

Improve accessibility of the notifications tab
This commit is contained in:
Marcus Kida 2022-11-21 16:57:00 +01:00 committed by GitHub
commit 6532f60cd5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 454 additions and 307 deletions

View File

@ -96,7 +96,7 @@
"tabs": {
"home": "Home",
"search": "Search",
"notification": "Notification",
"notifications": "Notifications",
"profile": "Profile"
},
"keyboard": {

View File

@ -96,7 +96,7 @@
"tabs": {
"home": "Home",
"search": "Search",
"notification": "Notification",
"notifications": "Notifications",
"profile": "Profile"
},
"keyboard": {

View File

@ -71,8 +71,8 @@ final public class SceneCoordinator {
self.setup()
try await Task.sleep(nanoseconds: .second * 1)
// redirect to notification tab
self.switchToTabBar(tab: .notification)
// redirect to notifications tab
self.switchToTabBar(tab: .notifications)
// Delay in next run loop
DispatchQueue.main.async { [weak self] in

View File

@ -51,7 +51,7 @@ class MainTabBarController: UITabBarController {
case home
case search
case compose
case notification
case notifications
case me
var tag: Int {
@ -63,7 +63,7 @@ class MainTabBarController: UITabBarController {
case .home: return L10n.Common.Controls.Tabs.home
case .search: return L10n.Common.Controls.Tabs.search
case .compose: return L10n.Common.Controls.Actions.compose
case .notification: return L10n.Common.Controls.Tabs.notification
case .notifications: return L10n.Common.Controls.Tabs.notifications
case .me: return L10n.Common.Controls.Tabs.profile
}
}
@ -73,7 +73,7 @@ class MainTabBarController: UITabBarController {
case .home: return Asset.ObjectsAndTools.house.image.withRenderingMode(.alwaysTemplate)
case .search: return Asset.ObjectsAndTools.magnifyingglass.image.withRenderingMode(.alwaysTemplate)
case .compose: return Asset.ObjectsAndTools.squareAndPencil.image.withRenderingMode(.alwaysTemplate)
case .notification: return Asset.ObjectsAndTools.bell.image.withRenderingMode(.alwaysTemplate)
case .notifications: return Asset.ObjectsAndTools.bell.image.withRenderingMode(.alwaysTemplate)
case .me: return UIImage(systemName: "person")!
}
}
@ -83,7 +83,7 @@ class MainTabBarController: UITabBarController {
case .home: return Asset.ObjectsAndTools.houseFill.image.withRenderingMode(.alwaysTemplate)
case .search: return Asset.ObjectsAndTools.magnifyingglassFill.image.withRenderingMode(.alwaysTemplate)
case .compose: return Asset.ObjectsAndTools.squareAndPencil.image.withRenderingMode(.alwaysTemplate)
case .notification: return Asset.ObjectsAndTools.bellFill.image.withRenderingMode(.alwaysTemplate)
case .notifications: return Asset.ObjectsAndTools.bellFill.image.withRenderingMode(.alwaysTemplate)
case .me: return UIImage(systemName: "person.fill")!
}
}
@ -93,7 +93,7 @@ class MainTabBarController: UITabBarController {
case .home: return Asset.ObjectsAndTools.house.image.withRenderingMode(.alwaysTemplate).resized(size: CGSize(width: 80, height: 80))
case .search: return Asset.ObjectsAndTools.magnifyingglass.image.withRenderingMode(.alwaysTemplate).resized(size: CGSize(width: 80, height: 80))
case .compose: return Asset.ObjectsAndTools.squareAndPencil.image.withRenderingMode(.alwaysTemplate).resized(size: CGSize(width: 80, height: 80))
case .notification: return Asset.ObjectsAndTools.bell.image.withRenderingMode(.alwaysTemplate).resized(size: CGSize(width: 80, height: 80))
case .notifications: return Asset.ObjectsAndTools.bell.image.withRenderingMode(.alwaysTemplate).resized(size: CGSize(width: 80, height: 80))
case .me: return UIImage(systemName: "person", withConfiguration: UIImage.SymbolConfiguration(pointSize: 80))!
}
}
@ -103,7 +103,7 @@ class MainTabBarController: UITabBarController {
case .home: return Asset.ObjectsAndTools.house.image.withRenderingMode(.alwaysTemplate)
case .search: return Asset.ObjectsAndTools.magnifyingglass.image.withRenderingMode(.alwaysTemplate)
case .compose: return Asset.ObjectsAndTools.squareAndPencil.image.withRenderingMode(.alwaysTemplate)
case .notification: return Asset.ObjectsAndTools.bell.image.withRenderingMode(.alwaysTemplate)
case .notifications: return Asset.ObjectsAndTools.bell.image.withRenderingMode(.alwaysTemplate)
case .me: return UIImage(systemName: "person")!
}
}
@ -129,7 +129,7 @@ class MainTabBarController: UITabBarController {
viewController = _viewController
case .compose:
viewController = UIViewController()
case .notification:
case .notifications:
let _viewController = NotificationViewController()
_viewController.context = context
_viewController.coordinator = coordinator
@ -274,7 +274,7 @@ extension MainTabBarController {
} ?? false
let image: UIImage = {
if currentTab == .notification {
if currentTab == .notifications {
return hasUnreadPushNotification ? Asset.ObjectsAndTools.bellBadgeFill.image.withRenderingMode(.alwaysTemplate) : Asset.ObjectsAndTools.bellFill.image.withRenderingMode(.alwaysTemplate)
} else {
return hasUnreadPushNotification ? Asset.ObjectsAndTools.bellBadge.image.withRenderingMode(.alwaysTemplate) : Asset.ObjectsAndTools.bell.image.withRenderingMode(.alwaysTemplate)
@ -654,7 +654,7 @@ extension MainTabBarController {
let tabs: [Tab] = [
.home,
.search,
.notification,
.notifications,
.me
]
for (i, tab) in tabs.enumerated() {

View File

@ -100,7 +100,7 @@ extension SidebarViewModel {
.store(in: &cell.disposeBag)
switch item {
case .notification:
case .notifications:
Publishers.CombineLatest(
self.context.notificationService.unreadNotificationCountDidUpdate,
self.$currentTab
@ -116,7 +116,7 @@ extension SidebarViewModel {
}()
let image: UIImage = {
if currentTab == .notification {
if currentTab == .notifications {
return hasUnreadPushNotification ? Asset.ObjectsAndTools.bellBadgeFill.image.withRenderingMode(.alwaysTemplate) : Asset.ObjectsAndTools.bellFill.image.withRenderingMode(.alwaysTemplate)
} else {
return hasUnreadPushNotification ? Asset.ObjectsAndTools.bellBadge.image.withRenderingMode(.alwaysTemplate) : Asset.ObjectsAndTools.bell.image.withRenderingMode(.alwaysTemplate)
@ -193,7 +193,7 @@ extension SidebarViewModel {
let items: [Item] = [
.tab(.home),
.tab(.search),
.tab(.notification),
.tab(.notifications),
.tab(.me),
.setting,
]

View File

@ -111,6 +111,7 @@ extension NotificationView {
self.viewModel.notificationIndicatorText = nil
return
}
self.viewModel.type = type
func createMetaContent(text: String, emojis: MastodonContent.Emojis) -> MetaContent {
let content = MastodonContent(content: text, emojis: emojis)

View File

@ -96,7 +96,6 @@ extension StatusThreadRootTableViewCell {
override var accessibilityElements: [Any]? {
get {
var elements = [
statusView.headerContainerView,
statusView.authorView,
statusView.viewModel.isContentReveal
? statusView.contentMetaText.textView

View File

@ -175,7 +175,7 @@ extension SceneDelegate {
return false
}
coordinator.switchToTabBar(tab: .notification)
coordinator.switchToTabBar(tab: .notifications)
case "org.joinmastodon.app.new-post":
if coordinator?.tabBarController.topMost is ComposeViewController {

View File

@ -1,241 +1,257 @@
{
"object": {
"pins": [
{
"package": "Alamofire",
"repositoryURL": "https://github.com/Alamofire/Alamofire.git",
"state": {
"branch": null,
"revision": "8dd85aee02e39dd280c75eef88ffdb86eed4b07b",
"version": "5.6.2"
}
},
{
"package": "AlamofireImage",
"repositoryURL": "https://github.com/Alamofire/AlamofireImage.git",
"state": {
"branch": null,
"revision": "98cbb00ce0ec5fc8e52a5b50a6bfc08d3e5aee10",
"version": "4.2.0"
}
},
{
"package": "CommonOSLog",
"repositoryURL": "https://github.com/MainasuK/CommonOSLog",
"state": {
"branch": null,
"revision": "c121624a30698e9886efe38aebb36ff51c01b6c2",
"version": "0.1.1"
}
},
{
"package": "FaviconFinder",
"repositoryURL": "https://github.com/will-lumley/FaviconFinder.git",
"state": {
"branch": null,
"revision": "1f74844f77f79b95c0bb0130b3a87d4f340e6d3a",
"version": "3.3.0"
}
},
{
"package": "FLAnimatedImage",
"repositoryURL": "https://github.com/Flipboard/FLAnimatedImage.git",
"state": {
"branch": null,
"revision": "d4f07b6f164d53c1212c3e54d6460738b1981e9f",
"version": "1.0.17"
}
},
{
"package": "FPSIndicator",
"repositoryURL": "https://github.com/MainasuK/FPSIndicator.git",
"state": {
"branch": null,
"revision": "e4a5067ccd5293b024c767f09e51056afd4a4796",
"version": "1.1.0"
}
},
{
"package": "Fuzi",
"repositoryURL": "https://github.com/cezheng/Fuzi.git",
"state": {
"branch": null,
"revision": "f08c8323da21e985f3772610753bcfc652c2103f",
"version": "3.1.3"
}
},
{
"package": "KeychainAccess",
"repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess.git",
"state": {
"branch": null,
"revision": "84e546727d66f1adc5439debad16270d0fdd04e7",
"version": "4.2.2"
}
},
{
"package": "MetaTextKit",
"repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git",
"state": {
"branch": null,
"revision": "dcd5255d6930c2fab408dc8562c577547e477624",
"version": "2.2.5"
}
},
{
"package": "Nuke",
"repositoryURL": "https://github.com/kean/Nuke.git",
"state": {
"branch": null,
"revision": "a002b7fd786f2df2ed4333fe73a9727499fd9d97",
"version": "10.11.2"
}
},
{
"package": "NukeFLAnimatedImagePlugin",
"repositoryURL": "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git",
"state": {
"branch": null,
"revision": "b59c346a7d536336db3b0f12c72c6e53ee709e16",
"version": "8.0.0"
}
},
{
"package": "Pageboy",
"repositoryURL": "https://github.com/uias/Pageboy",
"state": {
"branch": null,
"revision": "af8fa81788b893205e1ff42ddd88c5b0b315d7c5",
"version": "3.7.0"
}
},
{
"package": "PanModal",
"repositoryURL": "https://github.com/slackhq/PanModal.git",
"state": {
"branch": null,
"revision": "b012aecb6b67a8e46369227f893c12544846613f",
"version": "1.2.7"
}
},
{
"package": "SDWebImage",
"repositoryURL": "https://github.com/SDWebImage/SDWebImage.git",
"state": {
"branch": null,
"revision": "9248fe561a2a153916fb9597e3af4434784c6d32",
"version": "5.13.4"
}
},
{
"package": "swift-collections",
"repositoryURL": "https://github.com/apple/swift-collections.git",
"state": {
"branch": null,
"revision": "f504716c27d2e5d4144fa4794b12129301d17729",
"version": "1.0.3"
}
},
{
"package": "swift-nio",
"repositoryURL": "https://github.com/apple/swift-nio.git",
"state": {
"branch": null,
"revision": "546610d52b19be3e19935e0880bb06b9c03f5cef",
"version": "1.14.4"
}
},
{
"package": "swift-nio-zlib-support",
"repositoryURL": "https://github.com/apple/swift-nio-zlib-support.git",
"state": {
"branch": null,
"revision": "37760e9a52030bb9011972c5213c3350fa9d41fd",
"version": "1.0.0"
}
},
{
"package": "SwiftSoup",
"repositoryURL": "https://github.com/scinfu/SwiftSoup.git",
"state": {
"branch": null,
"revision": "6778575285177365cbad3e5b8a72f2a20583cfec",
"version": "2.4.3"
}
},
{
"package": "Introspect",
"repositoryURL": "https://github.com/siteline/SwiftUI-Introspect.git",
"state": {
"branch": null,
"revision": "f2616860a41f9d9932da412a8978fec79c06fe24",
"version": "0.1.4"
}
},
{
"package": "SwiftyJSON",
"repositoryURL": "https://github.com/SwiftyJSON/SwiftyJSON.git",
"state": {
"branch": null,
"revision": "b3dcd7dbd0d488e1a7077cb33b00f2083e382f07",
"version": "5.0.1"
}
},
{
"package": "TabBarPager",
"repositoryURL": "https://github.com/TwidereProject/TabBarPager.git",
"state": {
"branch": null,
"revision": "488aa66d157a648901b61721212c0dec23d27ee5",
"version": "0.1.0"
}
},
{
"package": "Tabman",
"repositoryURL": "https://github.com/uias/Tabman",
"state": {
"branch": null,
"revision": "4a4f7c755b875ffd4f9ef10d67a67883669d2465",
"version": "2.13.0"
}
},
{
"package": "ThirdPartyMailer",
"repositoryURL": "https://github.com/vtourraine/ThirdPartyMailer.git",
"state": {
"branch": null,
"revision": "44c1cfaa6969963f22691aa67f88a69e3b6d651f",
"version": "2.1.0"
}
},
{
"package": "TOCropViewController",
"repositoryURL": "https://github.com/TimOliver/TOCropViewController.git",
"state": {
"branch": null,
"revision": "d0470491f56e734731bbf77991944c0dfdee3e0e",
"version": "2.6.1"
}
},
{
"package": "UIHostingConfigurationBackport",
"repositoryURL": "https://github.com/woxtu/UIHostingConfigurationBackport.git",
"state": {
"branch": null,
"revision": "6091f2d38faa4b24fc2ca0389c651e2f666624a3",
"version": "0.1.0"
}
},
{
"package": "UITextView+Placeholder",
"repositoryURL": "https://github.com/MainasuK/UITextView-Placeholder.git",
"state": {
"branch": null,
"revision": "20f513ded04a040cdf5467f0891849b1763ede3b",
"version": "1.4.1"
}
"pins" : [
{
"identity" : "alamofire",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Alamofire/Alamofire.git",
"state" : {
"revision" : "8dd85aee02e39dd280c75eef88ffdb86eed4b07b",
"version" : "5.6.2"
}
]
},
"version": 1
},
{
"identity" : "alamofireimage",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Alamofire/AlamofireImage.git",
"state" : {
"revision" : "98cbb00ce0ec5fc8e52a5b50a6bfc08d3e5aee10",
"version" : "4.2.0"
}
},
{
"identity" : "commonoslog",
"kind" : "remoteSourceControl",
"location" : "https://github.com/MainasuK/CommonOSLog",
"state" : {
"revision" : "c121624a30698e9886efe38aebb36ff51c01b6c2",
"version" : "0.1.1"
}
},
{
"identity" : "faviconfinder",
"kind" : "remoteSourceControl",
"location" : "https://github.com/will-lumley/FaviconFinder.git",
"state" : {
"revision" : "1f74844f77f79b95c0bb0130b3a87d4f340e6d3a",
"version" : "3.3.0"
}
},
{
"identity" : "flanimatedimage",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Flipboard/FLAnimatedImage.git",
"state" : {
"revision" : "d4f07b6f164d53c1212c3e54d6460738b1981e9f",
"version" : "1.0.17"
}
},
{
"identity" : "fpsindicator",
"kind" : "remoteSourceControl",
"location" : "https://github.com/MainasuK/FPSIndicator.git",
"state" : {
"revision" : "e4a5067ccd5293b024c767f09e51056afd4a4796",
"version" : "1.1.0"
}
},
{
"identity" : "fuzi",
"kind" : "remoteSourceControl",
"location" : "https://github.com/cezheng/Fuzi.git",
"state" : {
"revision" : "f08c8323da21e985f3772610753bcfc652c2103f",
"version" : "3.1.3"
}
},
{
"identity" : "keychainaccess",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kishikawakatsumi/KeychainAccess.git",
"state" : {
"revision" : "84e546727d66f1adc5439debad16270d0fdd04e7",
"version" : "4.2.2"
}
},
{
"identity" : "kingfisher",
"kind" : "remoteSourceControl",
"location" : "https://github.com/onevcat/Kingfisher.git",
"state" : {
"revision" : "44e891bdb61426a95e31492a67c7c0dfad1f87c5",
"version" : "7.4.1"
}
},
{
"identity" : "metatextkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/TwidereProject/MetaTextKit.git",
"state" : {
"revision" : "dcd5255d6930c2fab408dc8562c577547e477624",
"version" : "2.2.5"
}
},
{
"identity" : "nextlevelsessionexporter",
"kind" : "remoteSourceControl",
"location" : "https://github.com/NextLevel/NextLevelSessionExporter.git",
"state" : {
"revision" : "b6c0cce1aa37fe1547d694f958fac3c3524b74da",
"version" : "0.4.6"
}
},
{
"identity" : "nuke",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kean/Nuke.git",
"state" : {
"revision" : "a002b7fd786f2df2ed4333fe73a9727499fd9d97",
"version" : "10.11.2"
}
},
{
"identity" : "nuke-flanimatedimage-plugin",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git",
"state" : {
"revision" : "b59c346a7d536336db3b0f12c72c6e53ee709e16",
"version" : "8.0.0"
}
},
{
"identity" : "pageboy",
"kind" : "remoteSourceControl",
"location" : "https://github.com/uias/Pageboy",
"state" : {
"revision" : "af8fa81788b893205e1ff42ddd88c5b0b315d7c5",
"version" : "3.7.0"
}
},
{
"identity" : "panmodal",
"kind" : "remoteSourceControl",
"location" : "https://github.com/slackhq/PanModal.git",
"state" : {
"revision" : "b012aecb6b67a8e46369227f893c12544846613f",
"version" : "1.2.7"
}
},
{
"identity" : "sdwebimage",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/SDWebImage.git",
"state" : {
"revision" : "9248fe561a2a153916fb9597e3af4434784c6d32",
"version" : "5.13.4"
}
},
{
"identity" : "stripes",
"kind" : "remoteSourceControl",
"location" : "https://github.com/eneko/Stripes.git",
"state" : {
"revision" : "d533fd44b8043a3abbf523e733599173d6f98c11",
"version" : "0.2.0"
}
},
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git",
"state" : {
"revision" : "f504716c27d2e5d4144fa4794b12129301d17729",
"version" : "1.0.3"
}
},
{
"identity" : "swift-nio",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio.git",
"state" : {
"revision" : "546610d52b19be3e19935e0880bb06b9c03f5cef",
"version" : "1.14.4"
}
},
{
"identity" : "swift-nio-zlib-support",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-zlib-support.git",
"state" : {
"revision" : "37760e9a52030bb9011972c5213c3350fa9d41fd",
"version" : "1.0.0"
}
},
{
"identity" : "swiftsoup",
"kind" : "remoteSourceControl",
"location" : "https://github.com/scinfu/SwiftSoup.git",
"state" : {
"revision" : "6778575285177365cbad3e5b8a72f2a20583cfec",
"version" : "2.4.3"
}
},
{
"identity" : "swiftui-introspect",
"kind" : "remoteSourceControl",
"location" : "https://github.com/siteline/SwiftUI-Introspect.git",
"state" : {
"revision" : "f2616860a41f9d9932da412a8978fec79c06fe24",
"version" : "0.1.4"
}
},
{
"identity" : "tabbarpager",
"kind" : "remoteSourceControl",
"location" : "https://github.com/TwidereProject/TabBarPager.git",
"state" : {
"revision" : "488aa66d157a648901b61721212c0dec23d27ee5",
"version" : "0.1.0"
}
},
{
"identity" : "tabman",
"kind" : "remoteSourceControl",
"location" : "https://github.com/uias/Tabman",
"state" : {
"revision" : "4a4f7c755b875ffd4f9ef10d67a67883669d2465",
"version" : "2.13.0"
}
},
{
"identity" : "thirdpartymailer",
"kind" : "remoteSourceControl",
"location" : "https://github.com/vtourraine/ThirdPartyMailer.git",
"state" : {
"revision" : "44c1cfaa6969963f22691aa67f88a69e3b6d651f",
"version" : "2.1.0"
}
},
{
"identity" : "tocropviewcontroller",
"kind" : "remoteSourceControl",
"location" : "https://github.com/TimOliver/TOCropViewController.git",
"state" : {
"revision" : "d0470491f56e734731bbf77991944c0dfdee3e0e",
"version" : "2.6.1"
}
},
{
"identity" : "uihostingconfigurationbackport",
"kind" : "remoteSourceControl",
"location" : "https://github.com/woxtu/UIHostingConfigurationBackport.git",
"state" : {
"revision" : "6091f2d38faa4b24fc2ca0389c651e2f666624a3",
"version" : "0.1.0"
}
},
{
"identity" : "uitextview-placeholder",
"kind" : "remoteSourceControl",
"location" : "https://github.com/MainasuK/UITextView-Placeholder.git",
"state" : {
"revision" : "20f513ded04a040cdf5467f0891849b1763ede3b",
"version" : "1.4.1"
}
}
],
"version" : 2
}

View File

@ -7,7 +7,7 @@
import Foundation
public enum MastodonNotificationType: RawRepresentable {
public enum MastodonNotificationType: RawRepresentable, Equatable {
case follow
case followRequest
case mention

View File

@ -366,8 +366,8 @@ public enum L10n {
public enum Tabs {
/// Home
public static let home = L10n.tr("Localizable", "Common.Controls.Tabs.Home", fallback: "Home")
/// Notification
public static let notification = L10n.tr("Localizable", "Common.Controls.Tabs.Notification", fallback: "Notification")
/// Notifications
public static let notifications = L10n.tr("Localizable", "Common.Controls.Tabs.Notifications", fallback: "Notifications")
/// Profile
public static let profile = L10n.tr("Localizable", "Common.Controls.Tabs.Profile", fallback: "Profile")
/// Search

View File

@ -131,7 +131,7 @@ Please check your internet connection.";
"Common.Controls.Status.Visibility.PrivateFromMe" = "Only my followers can see this post.";
"Common.Controls.Status.Visibility.Unlisted" = "Everyone can see this post but not display in the public timeline.";
"Common.Controls.Tabs.Home" = "Home";
"Common.Controls.Tabs.Notification" = "Notification";
"Common.Controls.Tabs.Notifications" = "Notifications";
"Common.Controls.Tabs.Profile" = "Profile";
"Common.Controls.Tabs.Search" = "Search";
"Common.Controls.Timeline.Filtered" = "Filtered";

View File

@ -25,7 +25,8 @@ extension NotificationView {
let logger = Logger(subsystem: "NotificationView", category: "ViewModel")
@Published public var authContext: AuthContext?
@Published public var type: MastodonNotificationType?
@Published public var notificationIndicatorText: MetaContent?
@Published public var authorAvatarImage: UIImage?
@ -54,7 +55,7 @@ extension NotificationView.ViewModel {
bindAuthor(notificationView: notificationView)
bindAuthorMenu(notificationView: notificationView)
bindFollowRequest(notificationView: notificationView)
$authContext
.assign(to: \.authContext, on: notificationView.statusView.viewModel)
.store(in: &disposeBag)
@ -100,20 +101,21 @@ extension NotificationView.ViewModel {
}
.store(in: &disposeBag)
// timestamp
Publishers.CombineLatest(
let formattedTimestamp = Publishers.CombineLatest(
$timestamp,
timestampUpdatePublisher.prepend(Date()).eraseToAnyPublisher()
)
.sink { [weak self] timestamp, _ in
guard let timestamp = timestamp else {
notificationView.dateLabel.configure(content: PlaintextMetaContent(string: ""))
return
}
let text = timestamp.localizedTimeAgoSinceNow
notificationView.dateLabel.configure(content: PlaintextMetaContent(string: text))
.map { timestamp, _ in
timestamp?.localizedTimeAgoSinceNow ?? ""
}
.store(in: &disposeBag)
.removeDuplicates()
formattedTimestamp
.sink { timestamp in
notificationView.dateLabel.configure(content: PlaintextMetaContent(string: timestamp))
}
.store(in: &disposeBag)
// notification type indicator
$notificationIndicatorText
.sink { text in
@ -124,6 +126,76 @@ extension NotificationView.ViewModel {
}
}
.store(in: &disposeBag)
Publishers.CombineLatest4(
$authorName,
$authorUsername,
$notificationIndicatorText,
formattedTimestamp
)
.sink { name, username, type, timestamp in
notificationView.accessibilityLabel = [
"\(name?.string ?? "") \(type?.string ?? "")",
username.map { "@\($0)" } ?? "",
timestamp
].joined(separator: ", ")
if !notificationView.statusView.isHidden {
notificationView.accessibilityLabel! += ", " + (notificationView.statusView.accessibilityLabel ?? "")
}
if !notificationView.quoteStatusViewContainerView.isHidden {
notificationView.accessibilityLabel! += ", " + (notificationView.quoteStatusView.accessibilityLabel ?? "")
}
}
.store(in: &disposeBag)
Publishers.CombineLatest(
$authorAvatarImage,
$type
)
.sink { avatarImage, type in
var actions = [UIAccessibilityCustomAction]()
// these notifications can be directly actioned to view the profile
if type != .follow, type != .followRequest {
actions.append(
UIAccessibilityCustomAction(
name: L10n.Common.Controls.Status.showUserProfile,
image: avatarImage
) { [weak notificationView] _ in
guard let notificationView = notificationView, let delegate = notificationView.delegate else { return false }
delegate.notificationView(notificationView, authorAvatarButtonDidPressed: notificationView.avatarButton)
return true
}
)
}
if type == .followRequest {
actions.append(
UIAccessibilityCustomAction(
name: L10n.Common.Controls.Actions.confirm,
image: Asset.Editing.checkmark20.image
) { [weak notificationView] _ in
guard let notificationView = notificationView, let delegate = notificationView.delegate else { return false }
delegate.notificationView(notificationView, acceptFollowRequestButtonDidPressed: notificationView.acceptFollowRequestButton)
return true
}
)
actions.append(
UIAccessibilityCustomAction(
name: L10n.Common.Controls.Actions.delete,
image: Asset.Circles.forbidden20.image
) { [weak notificationView] _ in
guard let notificationView = notificationView, let delegate = notificationView.delegate else { return false }
delegate.notificationView(notificationView, rejectFollowRequestButtonDidPressed: notificationView.rejectFollowRequestButton)
return true
}
)
}
notificationView.notificationActions = actions
}
.store(in: &disposeBag)
}
private func bindAuthorMenu(notificationView: NotificationView) {
@ -146,7 +218,9 @@ extension NotificationView.ViewModel {
isMyself: isMyself,
isBookmarking: false // no bookmark action display for notification item
)
notificationView.menuButton.menu = notificationView.setupAuthorMenu(menuContext: menuContext)
let (menu, actions) = notificationView.setupAuthorMenu(menuContext: menuContext)
notificationView.menuButton.menu = menu
notificationView.authorActions = actions
notificationView.menuButton.showsMenuAsPrimaryAction = true
notificationView.menuButton.isHidden = menuContext.isMyself
@ -206,5 +280,5 @@ extension NotificationView.ViewModel {
}
.store(in: &disposeBag)
}
}

View File

@ -46,7 +46,10 @@ public final class NotificationView: UIView {
var _disposeBag = Set<AnyCancellable>()
public var disposeBag = Set<AnyCancellable>()
var notificationActions = [UIAccessibilityCustomAction]()
var authorActions = [UIAccessibilityCustomAction]()
public private(set) lazy var viewModel: ViewModel = {
let viewModel = ViewModel()
viewModel.bind(notificationView: self)
@ -372,6 +375,30 @@ extension NotificationView {
statusView.delegate = self
quoteStatusView.delegate = self
isAccessibilityElement = true
}
}
extension NotificationView {
public override var accessibilityElements: [Any]? {
get { [] }
set {}
}
public override var accessibilityCustomActions: [UIAccessibilityCustomAction]? {
get {
var actions = notificationActions
actions += authorActions
if !statusView.isHidden {
actions += statusView.accessibilityCustomActions ?? []
}
if !quoteStatusViewContainerView.isHidden {
actions += quoteStatusView.accessibilityCustomActions ?? []
}
return actions
}
set {}
}
}
@ -430,7 +457,7 @@ extension NotificationView: AdaptiveContainerView {
extension NotificationView {
public typealias AuthorMenuContext = StatusAuthorView.AuthorMenuContext
public func setupAuthorMenu(menuContext: AuthorMenuContext) -> UIMenu {
public func setupAuthorMenu(menuContext: AuthorMenuContext) -> (UIMenu, [UIAccessibilityCustomAction]) {
var actions: [MastodonMenu.Action] = []
actions = [
@ -456,8 +483,13 @@ extension NotificationView {
actions: actions,
delegate: self
)
return menu
let accessibilityActions = MastodonMenu.setupAccessibilityActions(
actions: actions,
delegate: self
)
return (menu, accessibilityActions)
}
}

View File

@ -239,12 +239,11 @@ extension StatusView.ViewModel {
}
.store(in: &disposeBag)
// username
let usernamePublisher = $authorUsername
$authorUsername
.map { text -> String in
guard let text = text else { return "" }
return "@\(text)"
}
usernamePublisher
.sink { username in
let metaContent = PlaintextMetaContent(string: username)
authorView.authorUsernameLabel.configure(content: metaContent)
@ -271,18 +270,6 @@ extension StatusView.ViewModel {
authorView.dateLabel.configure(content: PlaintextMetaContent(string: text))
}
.store(in: &disposeBag)
// accessibility label
Publishers.CombineLatest4($authorName, usernamePublisher, $timestampText, $timestamp)
.map { name, username, timestampText, timestamp in
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
let longTimestamp = timestamp.map { formatter.string(from: $0) } ?? ""
return "\(name?.string ?? "") \(username), \(timestampText). \(longTimestamp)"
}
.assign(to: \.accessibilityLabel, on: authorView)
.store(in: &disposeBag)
}
private func bindContent(statusView: StatusView) {
@ -635,7 +622,7 @@ extension StatusView.ViewModel {
}
private func bindAccessibility(statusView: StatusView) {
let authorAccessibilityLabel = Publishers.CombineLatest3(
let shortAuthorAccessibilityLabel = Publishers.CombineLatest3(
$header,
$authorName,
$timestampText
@ -645,19 +632,56 @@ extension StatusView.ViewModel {
switch header {
case .none:
break
strings.append(authorName?.string)
case .reply(let info):
strings.append(authorName?.string)
strings.append(info.header.string)
case .repost(let info):
strings.append(info.header.string)
strings.append(authorName?.string)
}
strings.append(authorName?.string)
strings.append(timestamp)
return strings.compactMap { $0 }.joined(separator: ", ")
}
let longTimestampFormatter = DateFormatter()
longTimestampFormatter.dateStyle = .medium
longTimestampFormatter.timeStyle = .short
let longTimestampLabel = Publishers.CombineLatest(
$timestampText,
$timestamp.map { timestamp in
if let timestamp {
return longTimestampFormatter.string(from: timestamp)
}
return ""
}
)
.map { timestampText, longTimestamp in
"\(timestampText). \(longTimestamp)"
}
Publishers.CombineLatest4(
$header,
$authorName,
$authorUsername,
longTimestampLabel
)
.map { header, name, username, timestamp in
let nameAndUsername = "\(name?.string ?? "") @\(username ?? "")"
switch header {
case .none:
return "\(nameAndUsername), \(timestamp)"
case .repost(info: let info):
return "\(info.header.string) \(nameAndUsername), \(timestamp)"
case .reply(info: let info):
return "\(nameAndUsername) \(info.header.string), \(timestamp)"
}
}
.assign(to: \.accessibilityLabel, on: statusView.authorView)
.store(in: &disposeBag)
let contentAccessibilityLabel = Publishers.CombineLatest3(
$isContentReveal,
$spoilerContent,
@ -695,8 +719,8 @@ extension StatusView.ViewModel {
statusView.spoilerOverlayView.accessibilityLabel = contentAccessibilityLabel
}
.store(in: &disposeBag)
let meidaAccessibilityLabel = $mediaViewConfigurations
let mediaAccessibilityLabel = $mediaViewConfigurations
.map { configurations -> String? in
let count = configurations.count
return L10n.Plural.Count.media(count)
@ -705,18 +729,18 @@ extension StatusView.ViewModel {
// TODO: Toolbar
Publishers.CombineLatest3(
authorAccessibilityLabel,
shortAuthorAccessibilityLabel,
contentAccessibilityLabel,
meidaAccessibilityLabel
mediaAccessibilityLabel
)
.map { author, content, media in
let group = [
author,
content,
media
]
return group
var labels: [String?] = [content, media]
if statusView.style != .notification {
labels.insert(author, at: 0)
}
return labels
.compactMap { $0 }
.joined(separator: ", ")
}

View File

@ -241,6 +241,7 @@ extension StatusView {
// header
headerIconImageView.isUserInteractionEnabled = false
headerInfoLabel.isUserInteractionEnabled = false
headerInfoLabel.isAccessibilityElement = false
let headerTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
headerTapGestureRecognizer.addTarget(self, action: #selector(StatusView.headerDidPressed(_:)))
headerContainerView.addGestureRecognizer(headerTapGestureRecognizer)