From 8024be84b6454e603151435e76dc259df8fe1a29 Mon Sep 17 00:00:00 2001 From: ihugo Date: Wed, 28 Apr 2021 17:02:19 +0800 Subject: [PATCH 01/23] fix: Poll option placeholder not update after reorder fix #109 --- Mastodon/Scene/Compose/ComposeViewController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index a18cf9216..ca99d2e88 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -566,6 +566,7 @@ extension ComposeViewController { collectionView.updateInteractiveMovementTargetPosition(position) case .ended: collectionView.endInteractiveMovement() + collectionView.reloadData() default: collectionView.cancelInteractiveMovement() } From b63ae6800bfa0d60109655f0f90290db42aedea6 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 7 May 2021 20:02:28 +0800 Subject: [PATCH 02/23] feat: add a11y for server category picker --- Localization/app.json | 17 +++++++-- .../xcschemes/xcschememanagement.plist | 4 +-- .../Diffiable/Item/CategoryPickerItem.swift | 36 +++++++++++++++++++ .../Section/CategoryPickerSection.swift | 4 +++ Mastodon/Generated/Strings.swift | 26 ++++++++++++++ .../Resources/en.lproj/Localizable.strings | 13 +++++++ .../PickServerCategoriesCell.swift | 16 ++++++++- 7 files changed, 111 insertions(+), 5 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index fbc670da6..4e9d15a8a 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -138,7 +138,20 @@ "title": "Pick a Server,\nany server.", "button": { "category": { - "All": "All" + "all": "All", + "all_accessiblity_description": "Category: All", + "academia": "academia", + "activism": "activism", + "food": "food", + "furry": "furry", + "games": "games", + "general": "general", + "journalism": "journalism", + "lgbt": "lgbt", + "regional": "regional", + "art": "art", + "music": "music", + "tech": "tech" }, "see_less": "See Less", "see_more": "See More" @@ -423,4 +436,4 @@ "text_placeholder": "Type or paste additional comments" } } -} +} \ No newline at end of file diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index f092f9734..326857269 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -12,7 +12,7 @@ CoreDataStack.xcscheme_^#shared#^_ orderHint - 15 + 14 Mastodon - RTL.xcscheme_^#shared#^_ @@ -32,7 +32,7 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 14 + 15 SuppressBuildableAutocreation diff --git a/Mastodon/Diffiable/Item/CategoryPickerItem.swift b/Mastodon/Diffiable/Item/CategoryPickerItem.swift index 52bdaf39e..0f2cdcc21 100644 --- a/Mastodon/Diffiable/Item/CategoryPickerItem.swift +++ b/Mastodon/Diffiable/Item/CategoryPickerItem.swift @@ -50,6 +50,42 @@ extension CategoryPickerItem { } } } + + var accessibilityDescription: String { + switch self { + case .all: + return L10n.Scene.ServerPicker.Button.Category.allAccessiblityDescription + case .category(let category): + switch category.category { + case .academia: + return L10n.Scene.ServerPicker.Button.Category.academia + case .activism: + return L10n.Scene.ServerPicker.Button.Category.activism + case .food: + return L10n.Scene.ServerPicker.Button.Category.food + case .furry: + return L10n.Scene.ServerPicker.Button.Category.furry + case .games: + return L10n.Scene.ServerPicker.Button.Category.games + case .general: + return L10n.Scene.ServerPicker.Button.Category.general + case .journalism: + return L10n.Scene.ServerPicker.Button.Category.journalism + case .lgbt: + return L10n.Scene.ServerPicker.Button.Category.lgbt + case .regional: + return L10n.Scene.ServerPicker.Button.Category.regional + case .art: + return L10n.Scene.ServerPicker.Button.Category.art + case .music: + return L10n.Scene.ServerPicker.Button.Category.music + case .tech: + return L10n.Scene.ServerPicker.Button.Category.tech + case ._other: + return "❓" // FIXME: + } + } + } } extension CategoryPickerItem: Equatable { diff --git a/Mastodon/Diffiable/Section/CategoryPickerSection.swift b/Mastodon/Diffiable/Section/CategoryPickerSection.swift index 938683f99..7ab93cc5e 100644 --- a/Mastodon/Diffiable/Section/CategoryPickerSection.swift +++ b/Mastodon/Diffiable/Section/CategoryPickerSection.swift @@ -42,6 +42,10 @@ extension CategoryPickerSection { } } .store(in: &cell.observations) + + cell.isAccessibilityElement = true + cell.accessibilityLabel = item.accessibilityDescription + return cell } } diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 4ec5e7037..e53f9c082 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -638,8 +638,34 @@ internal enum L10n { /// See More internal static let seeMore = L10n.tr("Localizable", "Scene.ServerPicker.Button.SeeMore") internal enum Category { + /// academia + internal static let academia = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Academia") + /// activism + internal static let activism = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Activism") /// All internal static let all = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.All") + /// Category: All + internal static let allAccessiblityDescription = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.AllAccessiblityDescription") + /// art + internal static let art = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Art") + /// food + internal static let food = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Food") + /// furry + internal static let furry = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Furry") + /// games + internal static let games = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Games") + /// general + internal static let general = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.General") + /// journalism + internal static let journalism = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Journalism") + /// lgbt + internal static let lgbt = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Lgbt") + /// music + internal static let music = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Music") + /// regional + internal static let regional = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Regional") + /// tech + internal static let tech = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Tech") } } internal enum EmptyState { diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 59f83a5cd..16552b39e 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -204,7 +204,20 @@ tap the link to confirm your account."; "Scene.Search.Searching.Segment.All" = "All"; "Scene.Search.Searching.Segment.Hashtags" = "Hashtags"; "Scene.Search.Searching.Segment.People" = "People"; +"Scene.ServerPicker.Button.Category.Academia" = "academia"; +"Scene.ServerPicker.Button.Category.Activism" = "activism"; "Scene.ServerPicker.Button.Category.All" = "All"; +"Scene.ServerPicker.Button.Category.AllAccessiblityDescription" = "Category: All"; +"Scene.ServerPicker.Button.Category.Art" = "art"; +"Scene.ServerPicker.Button.Category.Food" = "food"; +"Scene.ServerPicker.Button.Category.Furry" = "furry"; +"Scene.ServerPicker.Button.Category.Games" = "games"; +"Scene.ServerPicker.Button.Category.General" = "general"; +"Scene.ServerPicker.Button.Category.Journalism" = "journalism"; +"Scene.ServerPicker.Button.Category.Lgbt" = "lgbt"; +"Scene.ServerPicker.Button.Category.Music" = "music"; +"Scene.ServerPicker.Button.Category.Regional" = "regional"; +"Scene.ServerPicker.Button.Category.Tech" = "tech"; "Scene.ServerPicker.Button.SeeLess" = "See Less"; "Scene.ServerPicker.Button.SeeMore" = "See More"; "Scene.ServerPicker.EmptyState.BadNetwork" = "Something went wrong while loading data. Check your internet connection."; diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCategoriesCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCategoriesCell.swift index 84ee6017c..373a90ddf 100644 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCategoriesCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCategoriesCell.swift @@ -9,7 +9,7 @@ import os.log import UIKit import MastodonSDK -protocol PickServerCategoriesCellDelegate: class { +protocol PickServerCategoriesCellDelegate: AnyObject { func pickServerCategoriesCell(_ cell: PickServerCategoriesCell, collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) } @@ -110,3 +110,17 @@ extension PickServerCategoriesCell: UICollectionViewDelegateFlowLayout { } } + +extension PickServerCategoriesCell { + + override func accessibilityElementCount() -> Int { + guard let diffableDataSource = diffableDataSource else { return 0 } + return diffableDataSource.snapshot().itemIdentifiers.count + } + + override func accessibilityElement(at index: Int) -> Any? { + guard let item = collectionView.cellForItem(at: IndexPath(item: index, section: 0)) else { return nil } + return item + } + +} From 4901b50d3bc71909b7a745c45d505ace629b3f6b Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 10 May 2021 16:06:00 +0800 Subject: [PATCH 03/23] feat: ignore smart invert for photos --- Mastodon/Protocol/AvatarConfigurableView.swift | 4 ++++ .../MediaPreview/Paging/Image/MediaPreviewImageView.swift | 2 ++ Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift | 5 ++--- .../Settings/View/Cell/SettingsAppearanceTableViewCell.swift | 2 ++ .../Share/View/Container/MosaicImageViewContainer.swift | 3 +++ .../Scene/Share/View/Container/PlayerContainerView.swift | 3 +++ ...stToMediaPreviewViewControllerAnimatedTransitioning.swift | 2 ++ 7 files changed, 18 insertions(+), 3 deletions(-) diff --git a/Mastodon/Protocol/AvatarConfigurableView.swift b/Mastodon/Protocol/AvatarConfigurableView.swift index 3d2dba802..e33c01278 100644 --- a/Mastodon/Protocol/AvatarConfigurableView.swift +++ b/Mastodon/Protocol/AvatarConfigurableView.swift @@ -47,6 +47,10 @@ extension AvatarConfigurableView { configurableAvatarButton?.layer.cornerRadius = 0 configurableAvatarButton?.layer.cornerCurve = .circular + // accessibility + configurableAvatarImageView?.accessibilityIgnoresInvertColors = true + configurableAvatarButton?.accessibilityIgnoresInvertColors = true + defer { avatarConfigurableView(self, didFinishConfiguration: configuration) } diff --git a/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageView.swift b/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageView.swift index 0f2ba82fb..0e4aa6d89 100644 --- a/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageView.swift +++ b/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageView.swift @@ -16,6 +16,8 @@ final class MediaPreviewImageView: UIScrollView { imageView.contentMode = .scaleAspectFit imageView.clipsToBounds = true imageView.isUserInteractionEnabled = true + // accessibility + imageView.accessibilityIgnoresInvertColors = true return imageView }() diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift index 1e09116d3..0bcc68bfd 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift @@ -45,9 +45,8 @@ final class ProfileHeaderView: UIView { imageView.backgroundColor = ProfileHeaderView.bannerImageViewPlaceholderColor imageView.layer.masksToBounds = true imageView.isUserInteractionEnabled = true - // #if DEBUG - // imageView.image = .placeholder(color: .red) - // #endif + // accessibility + imageView.accessibilityIgnoresInvertColors = true return imageView }() let bannerImageViewOverlayView: UIView = { diff --git a/Mastodon/Scene/Settings/View/Cell/SettingsAppearanceTableViewCell.swift b/Mastodon/Scene/Settings/View/Cell/SettingsAppearanceTableViewCell.swift index 44a7e7574..05bb4d10d 100644 --- a/Mastodon/Scene/Settings/View/Cell/SettingsAppearanceTableViewCell.swift +++ b/Mastodon/Scene/Settings/View/Cell/SettingsAppearanceTableViewCell.swift @@ -15,6 +15,8 @@ protocol SettingsAppearanceTableViewCellDelegate: class { class AppearanceView: UIView { lazy var imageView: UIImageView = { let view = UIImageView() + // accessibility + view.accessibilityIgnoresInvertColors = true return view }() lazy var titleLabel: UILabel = { diff --git a/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift b/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift index ea943fb0e..486f97e3c 100644 --- a/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift +++ b/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift @@ -65,6 +65,9 @@ extension MosaicImageViewContainer: ContentWarningOverlayViewDelegate { extension MosaicImageViewContainer { private func _init() { + // accessibility + accessibilityIgnoresInvertColors = true + container.translatesAutoresizingMaskIntoConstraints = false container.axis = .horizontal container.distribution = .fillEqually diff --git a/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift b/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift index f7a8a1546..32a4dc147 100644 --- a/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift +++ b/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift @@ -46,6 +46,9 @@ final class PlayerContainerView: UIView { extension PlayerContainerView { private func _init() { + // accessibility + accessibilityIgnoresInvertColors = true + container.translatesAutoresizingMaskIntoConstraints = false addSubview(container) containerHeightLayoutConstraint = container.heightAnchor.constraint(equalToConstant: 162).priority(.required - 1) diff --git a/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift b/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift index 74d82badd..17ba9c660 100644 --- a/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift +++ b/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift @@ -73,6 +73,8 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { imageView.contentMode = .scaleAspectFill imageView.isUserInteractionEnabled = false imageView.image = transitionItem.image + // accessibility + imageView.accessibilityIgnoresInvertColors = true return imageView }() transitionItem.targetFrame = transitionTargetFrame From 55943db9bc7e2120a9d177c67278918029f6da1f Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 10 May 2021 18:48:04 +0800 Subject: [PATCH 04/23] feat: make dynamic type font adapt accessibility level font size --- .../View/AttachmentContainerView.swift | 2 +- .../NotificationStatusTableViewCell.swift | 4 +- .../NotificationTableViewCell.swift | 4 +- .../TableViewCell/PickServerCell.swift | 16 +++--- .../MastodonRegisterViewController.swift | 13 ++--- .../Register/MastodonRegisterViewModel.swift | 4 +- .../MastodonServerRulesViewController.swift | 5 +- .../Header/View/ProfileHeaderView.swift | 4 +- .../Settings/SettingsViewController.swift | 29 ++++++++-- .../Content/ContentWarningOverlayView.swift | 4 +- .../Scene/Share/View/Content/StatusView.swift | 2 +- .../Share/View/Content/ThreadMetaView.swift | 53 ++++++++++++++----- 12 files changed, 97 insertions(+), 43 deletions(-) diff --git a/Mastodon/Scene/Compose/View/AttachmentContainerView.swift b/Mastodon/Scene/Compose/View/AttachmentContainerView.swift index cbad76830..eb5f01f41 100644 --- a/Mastodon/Scene/Compose/View/AttachmentContainerView.swift +++ b/Mastodon/Scene/Compose/View/AttachmentContainerView.swift @@ -47,7 +47,7 @@ final class AttachmentContainerView: UIView { textView.showsVerticalScrollIndicator = false textView.backgroundColor = .clear textView.textColor = .white - textView.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15)) + textView.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15), maximumPointSize: 20) textView.placeholder = L10n.Scene.Compose.Attachment.descriptionPhoto textView.placeholderColor = UIColor.white.withAlphaComponent(0.6) // force white with alpha for Light/Dark mode textView.returnKeyType = .done diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift index 7b76dd2f0..5ae5d0136 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift @@ -50,7 +50,7 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell { let actionLabel: UILabel = { let label = UILabel() label.textColor = Asset.Colors.Label.secondary.color - label.font = UIFont.preferredFont(forTextStyle: .body) + label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular), maximumPointSize: 20) label.lineBreakMode = .byTruncatingTail return label }() @@ -58,7 +58,7 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell { let nameLabel: UILabel = { let label = UILabel() label.textColor = Asset.Colors.brandBlue.color - label.font = .systemFont(ofSize: 15, weight: .semibold) + label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold), maximumPointSize: 20) label.lineBreakMode = .byTruncatingTail return label }() diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift index c049b961e..49d32530b 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift @@ -67,7 +67,7 @@ final class NotificationTableViewCell: UITableViewCell { let actionLabel: UILabel = { let label = UILabel() label.textColor = Asset.Colors.Label.secondary.color - label.font = UIFont.preferredFont(forTextStyle: .body) + label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular), maximumPointSize: 20) label.lineBreakMode = .byTruncatingTail return label }() @@ -75,7 +75,7 @@ final class NotificationTableViewCell: UITableViewCell { let nameLabel: UILabel = { let label = UILabel() label.textColor = Asset.Colors.brandBlue.color - label.font = .systemFont(ofSize: 15, weight: .semibold) + label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold), maximumPointSize: 20) label.lineBreakMode = .byTruncatingTail return label }() diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift index 9313aa5cc..85c998b8f 100644 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift @@ -34,7 +34,7 @@ class PickServerCell: UITableViewCell { let domainLabel: UILabel = { let label = UILabel() - label.font = .preferredFont(forTextStyle: .headline) + label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 22) label.textColor = Asset.Colors.Label.primary.color label.adjustsFontForContentSizeCategory = true label.translatesAutoresizingMaskIntoConstraints = false @@ -52,7 +52,7 @@ class PickServerCell: UITableViewCell { let descriptionLabel: UILabel = { let label = UILabel() - label.font = .preferredFont(forTextStyle: .subheadline) + label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)) label.numberOfLines = 0 label.textColor = Asset.Colors.Label.primary.color label.adjustsFontForContentSizeCategory = true @@ -106,7 +106,7 @@ class PickServerCell: UITableViewCell { let langValueLabel: UILabel = { let label = UILabel() label.textColor = Asset.Colors.Label.primary.color - label.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: UIFont.systemFont(ofSize: 22, weight: .semibold)) + label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 22, weight: .semibold), maximumPointSize: 27) label.textAlignment = .center label.adjustsFontForContentSizeCategory = true label.translatesAutoresizingMaskIntoConstraints = false @@ -116,7 +116,7 @@ class PickServerCell: UITableViewCell { let usersValueLabel: UILabel = { let label = UILabel() label.textColor = Asset.Colors.Label.primary.color - label.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: UIFont.systemFont(ofSize: 22, weight: .semibold)) + label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 22, weight: .semibold), maximumPointSize: 27) label.textAlignment = .center label.adjustsFontForContentSizeCategory = true label.translatesAutoresizingMaskIntoConstraints = false @@ -126,7 +126,7 @@ class PickServerCell: UITableViewCell { let categoryValueLabel: UILabel = { let label = UILabel() label.textColor = Asset.Colors.Label.primary.color - label.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: UIFont.systemFont(ofSize: 22, weight: .semibold)) + label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 22, weight: .semibold), maximumPointSize: 27) label.textAlignment = .center label.adjustsFontForContentSizeCategory = true label.translatesAutoresizingMaskIntoConstraints = false @@ -136,7 +136,7 @@ class PickServerCell: UITableViewCell { let langTitleLabel: UILabel = { let label = UILabel() label.textColor = Asset.Colors.Label.primary.color - label.font = .preferredFont(forTextStyle: .caption2) + label.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 11, weight: .regular), maximumPointSize: 16) label.text = L10n.Scene.ServerPicker.Label.language label.textAlignment = .center label.adjustsFontForContentSizeCategory = true @@ -147,7 +147,7 @@ class PickServerCell: UITableViewCell { let usersTitleLabel: UILabel = { let label = UILabel() label.textColor = Asset.Colors.Label.primary.color - label.font = .preferredFont(forTextStyle: .caption2) + label.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 11, weight: .regular), maximumPointSize: 16) label.text = L10n.Scene.ServerPicker.Label.users label.textAlignment = .center label.adjustsFontForContentSizeCategory = true @@ -158,7 +158,7 @@ class PickServerCell: UITableViewCell { let categoryTitleLabel: UILabel = { let label = UILabel() label.textColor = Asset.Colors.Label.primary.color - label.font = .preferredFont(forTextStyle: .caption2) + label.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 11, weight: .regular), maximumPointSize: 16) label.text = L10n.Scene.ServerPicker.Label.category label.textAlignment = .center label.adjustsFontForContentSizeCategory = true diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift index 007012d3c..48401c0c9 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift @@ -63,6 +63,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: UIFont.boldSystemFont(ofSize: 34)) label.textColor = Asset.Colors.Label.primary.color label.text = L10n.Scene.Register.title + label.numberOfLines = 0 return label }() @@ -99,7 +100,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O let domainLabel: UILabel = { let label = UILabel() - label.font = .preferredFont(forTextStyle: .headline) + label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 22) label.textColor = Asset.Colors.Label.primary.color return label }() @@ -113,7 +114,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O textField.textColor = Asset.Colors.Label.primary.color textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Username.placeholder, attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color, - NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) + NSAttributedString.Key.font: UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 22)]) textField.borderStyle = UITextField.BorderStyle.roundedRect let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) textField.leftView = paddingView @@ -136,7 +137,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O textField.textColor = Asset.Colors.Label.primary.color textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.DisplayName.placeholder, attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color, - NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) + NSAttributedString.Key.font: UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 22)]) textField.borderStyle = UITextField.BorderStyle.roundedRect let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) textField.leftView = paddingView @@ -153,7 +154,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O textField.textColor = Asset.Colors.Label.primary.color textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Email.placeholder, attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color, - NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) + NSAttributedString.Key.font: UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 22)]) textField.borderStyle = UITextField.BorderStyle.roundedRect let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) textField.leftView = paddingView @@ -178,7 +179,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O textField.textColor = Asset.Colors.Label.primary.color textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Password.placeholder, attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color, - NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) + NSAttributedString.Key.font: UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 22)]) textField.borderStyle = UITextField.BorderStyle.roundedRect let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) textField.leftView = paddingView @@ -208,7 +209,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O textField.textColor = Asset.Colors.Label.primary.color textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Invite.registrationUserInviteRequest, attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color, - NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) + NSAttributedString.Key.font: UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 22)]) textField.borderStyle = UITextField.BorderStyle.roundedRect let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) textField.leftView = paddingView diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift index 309204a9a..85b934a25 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift @@ -223,7 +223,7 @@ extension MastodonRegisterViewModel { } static func attributeStringForPassword(validateState: ValidateState) -> NSAttributedString { - let font = UIFont.preferredFont(forTextStyle: .caption1) + let font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 13, weight: .regular), maximumPointSize: 18) let attributeString = NSMutableAttributedString() let image = MastodonRegisterViewModel.checkmarkImage(font: font) @@ -236,7 +236,7 @@ extension MastodonRegisterViewModel { } static func errorPromptAttributedString(for prompt: String) -> NSAttributedString { - let font = UIFont.preferredFont(forTextStyle: .caption1) + let font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 13, weight: .regular), maximumPointSize: 18) let attributeString = NSMutableAttributedString() let image = MastodonRegisterViewModel.xmarkImage(font: font) diff --git a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift index d8638421a..d865c96ec 100644 --- a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift +++ b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift @@ -25,6 +25,7 @@ final class MastodonServerRulesViewController: UIViewController, NeedsDependency label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: .systemFont(ofSize: 34, weight: .bold)) label.textColor = .label label.text = L10n.Scene.ServerRules.title + label.numberOfLines = 0 return label }() @@ -54,7 +55,7 @@ final class MastodonServerRulesViewController: UIViewController, NeedsDependency private(set) lazy var bottomPromptTextView: UITextView = { let textView = UITextView() - textView.font = .preferredFont(forTextStyle: .body) + textView.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular), maximumPointSize: 22) textView.textColor = .label textView.isSelectable = true textView.isEditable = false @@ -181,7 +182,7 @@ extension MastodonServerRulesViewController { let str = NSString(string: L10n.Scene.ServerRules.prompt(viewModel.domain)) let termsOfServiceRange = str.range(of: L10n.Scene.ServerRules.termsOfService) let privacyRange = str.range(of: L10n.Scene.ServerRules.privacyPolicy) - let attributeString = NSMutableAttributedString(string: L10n.Scene.ServerRules.prompt(viewModel.domain), attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .body), NSAttributedString.Key.foregroundColor: UIColor.label]) + let attributeString = NSMutableAttributedString(string: L10n.Scene.ServerRules.prompt(viewModel.domain), attributes: [NSAttributedString.Key.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular), maximumPointSize: 22), NSAttributedString.Key.foregroundColor: UIColor.label]) attributeString.addAttribute(.link, value: Mastodon.API.serverRulesURL(domain: viewModel.domain), range: termsOfServiceRange) attributeString.addAttribute(.link, value: Mastodon.API.privacyURL(domain: viewModel.domain), range: privacyRange) let linkAttributes = [NSAttributedString.Key.foregroundColor:linkColor] diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift index 0bcc68bfd..e37427e32 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift @@ -100,7 +100,7 @@ final class ProfileHeaderView: UIView { let nameTextField: UITextField = { let textField = UITextField() - textField.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold)) + textField.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold), maximumPointSize: 28) textField.textColor = .white textField.text = "Alice" textField.autocorrectionType = .no @@ -111,7 +111,7 @@ final class ProfileHeaderView: UIView { let usernameLabel: UILabel = { let label = UILabel() - label.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)) + label.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular), maximumPointSize: 20) label.adjustsFontSizeToFitWidth = true label.minimumScaleFactor = 0.5 label.textColor = Asset.Scene.Profile.Banner.usernameGray.color diff --git a/Mastodon/Scene/Settings/SettingsViewController.swift b/Mastodon/Scene/Settings/SettingsViewController.swift index 3c4a101a0..39bafdf49 100644 --- a/Mastodon/Scene/Settings/SettingsViewController.swift +++ b/Mastodon/Scene/Settings/SettingsViewController.swift @@ -51,21 +51,27 @@ class SettingsViewController: UIViewController, NeedsDependency { return menu } - private(set) lazy var notifySectionHeader: UIView = { + private let notifySectionHeaderStackView: UIStackView = { let view = UIStackView() view.translatesAutoresizingMaskIntoConstraints = false view.isLayoutMarginsRelativeArrangement = true - //view.layoutMargins = UIEdgeInsets(top: 15, left: 4, bottom: 5, right: 4) view.axis = .horizontal view.alignment = .fill view.distribution = .equalSpacing view.spacing = 4 + return view + }() + + private(set) lazy var notifySectionHeader: UIView = { + let view = notifySectionHeaderStackView let notifyLabel = UILabel() notifyLabel.translatesAutoresizingMaskIntoConstraints = false - notifyLabel.font = UIFontMetrics(forTextStyle: .title3).scaledFont(for: UIFont.systemFont(ofSize: 20, weight: .semibold)) + notifyLabel.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: UIFont.systemFont(ofSize: 20, weight: .semibold)) notifyLabel.textColor = Asset.Colors.Label.primary.color notifyLabel.text = L10n.Scene.Settings.Section.Notifications.Trigger.title + // accessibility + notifyLabel.numberOfLines = 0 view.addArrangedSubview(notifyLabel) view.addArrangedSubview(whoButton) return view @@ -138,7 +144,22 @@ class SettingsViewController: UIViewController, NeedsDependency { } } + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + updateSectionHeaderStackViewLayout() + } + + // MAKR: - Private methods + private func updateSectionHeaderStackViewLayout() { + if traitCollection.preferredContentSizeCategory < .accessibilityMedium { + notifySectionHeaderStackView.axis = .horizontal + } else { + notifySectionHeaderStackView.axis = .vertical + } + } + private func bindViewModel() { self.whoButton.setTitle(viewModel.setting.value.activeSubscription?.policy.title, for: .normal) viewModel.setting @@ -173,6 +194,8 @@ class SettingsViewController: UIViewController, NeedsDependency { tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) setupTableView() + + updateSectionHeaderStackViewLayout() } private func setupNavigation() { diff --git a/Mastodon/Scene/Share/View/Content/ContentWarningOverlayView.swift b/Mastodon/Scene/Share/View/Content/ContentWarningOverlayView.swift index f04a56e9e..6cf62783f 100644 --- a/Mastodon/Scene/Share/View/Content/ContentWarningOverlayView.swift +++ b/Mastodon/Scene/Share/View/Content/ContentWarningOverlayView.swift @@ -36,7 +36,7 @@ class ContentWarningOverlayView: UIView { }() let blurContentWarningTitleLabel: UILabel = { let label = UILabel() - label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17)) + label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17), maximumPointSize: 23) label.text = L10n.Common.Controls.Status.mediaContentWarning label.textColor = Asset.Colors.Label.primary.color label.textAlignment = .center @@ -44,7 +44,7 @@ class ContentWarningOverlayView: UIView { }() let blurContentWarningLabel: UILabel = { let label = UILabel() - label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15)) + label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15), maximumPointSize: 20) label.text = L10n.Common.Controls.Status.mediaContentWarning label.textColor = Asset.Colors.Label.secondary.color label.textAlignment = .center diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index 20437a8d2..179373830 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -71,7 +71,7 @@ final class StatusView: UIView { let headerInfoLabel: UILabel = { let label = UILabel() - label.font = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: .systemFont(ofSize: 13, weight: .medium)) + label.font = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: .systemFont(ofSize: 13, weight: .medium), maximumPointSize: 17) label.textColor = Asset.Colors.Label.secondary.color label.text = "Bob reblogged" return label diff --git a/Mastodon/Scene/Share/View/Content/ThreadMetaView.swift b/Mastodon/Scene/Share/View/Content/ThreadMetaView.swift index 16d1b04a6..e84939d32 100644 --- a/Mastodon/Scene/Share/View/Content/ThreadMetaView.swift +++ b/Mastodon/Scene/Share/View/Content/ThreadMetaView.swift @@ -34,6 +34,14 @@ final class ThreadMetaView: UIView { return button }() + let containerStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.spacing = 20 + return stackView + }() + let actionButtonStackView = UIStackView() + override init(frame: CGRect) { super.init(frame: frame) _init() @@ -48,27 +56,48 @@ final class ThreadMetaView: UIView { extension ThreadMetaView { private func _init() { - let stackView = UIStackView() - stackView.axis = .horizontal - stackView.spacing = 20 - stackView.translatesAutoresizingMaskIntoConstraints = false - addSubview(stackView) + containerStackView.translatesAutoresizingMaskIntoConstraints = false + addSubview(containerStackView) NSLayoutConstraint.activate([ - stackView.topAnchor.constraint(equalTo: topAnchor, constant: 8), - stackView.leadingAnchor.constraint(equalTo: leadingAnchor), - stackView.trailingAnchor.constraint(equalTo: trailingAnchor), - bottomAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 12), + containerStackView.topAnchor.constraint(equalTo: topAnchor, constant: 8), + containerStackView.leadingAnchor.constraint(equalTo: leadingAnchor), + containerStackView.trailingAnchor.constraint(equalTo: trailingAnchor), + bottomAnchor.constraint(equalTo: containerStackView.bottomAnchor, constant: 12), ]) - stackView.addArrangedSubview(dateLabel) - stackView.addArrangedSubview(reblogButton) - stackView.addArrangedSubview(favoriteButton) + containerStackView.addArrangedSubview(dateLabel) + containerStackView.addArrangedSubview(actionButtonStackView) + + actionButtonStackView.axis = .horizontal + actionButtonStackView.addArrangedSubview(reblogButton) + actionButtonStackView.addArrangedSubview(favoriteButton) dateLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) reblogButton.setContentHuggingPriority(.required - 2, for: .horizontal) favoriteButton.setContentHuggingPriority(.required - 1, for: .horizontal) + + updateContainerLayout() } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + updateContainerLayout() + } + + private func updateContainerLayout() { + if traitCollection.preferredContentSizeCategory < .accessibilityMedium { + containerStackView.axis = .horizontal + containerStackView.spacing = 20 + dateLabel.numberOfLines = 1 + } else { + containerStackView.axis = .vertical + containerStackView.spacing = 4 + dateLabel.numberOfLines = 0 + } + } + } #if canImport(SwiftUI) && DEBUG From 87d93f3c9d2199972ce0b3f8cab5b1822619aaf5 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 11 May 2021 16:10:05 +0800 Subject: [PATCH 05/23] fix: remove entity raise crash issue --- Mastodon/Diffiable/Section/StatusSection.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index b5f9869e2..1ed5f0d42 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -449,7 +449,8 @@ extension StatusSection { } receiveValue: { [weak dependency, weak cell] change in guard let dependency = dependency else { return } guard case .update(let object) = change.changeType, - let status = object as? Status else { return } + let status = object as? Status, + !status.isDeleted else { return } guard let statusTableViewCell = cell as? StatusTableViewCell else { return } StatusSection.configureActionToolBar( cell: statusTableViewCell, @@ -648,7 +649,7 @@ extension StatusSection { .assertNoFailure() ) .receive(on: DispatchQueue.main) - .sink { [weak dependency, weak cell] _,change in + .sink { [weak dependency, weak cell] _, change in guard let cell = cell else { return } guard let dependency = dependency else { return } switch change.changeType { From 1eed6e49864a8f9d566552d837f12c7d07cbb65e Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 11 May 2021 16:11:00 +0800 Subject: [PATCH 06/23] fix: media photo preview transition top and bottom bar missing mask issue --- .../mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist | 4 ++-- .../Transition/MediaPreview/MediaPreviewTransitionItem.swift | 2 ++ .../MediaPreview/MediaPreviewableViewController.swift | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 326857269..f092f9734 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -12,7 +12,7 @@ CoreDataStack.xcscheme_^#shared#^_ orderHint - 14 + 15 Mastodon - RTL.xcscheme_^#shared#^_ @@ -32,7 +32,7 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 15 + 14 SuppressBuildableAutocreation diff --git a/Mastodon/Scene/Transition/MediaPreview/MediaPreviewTransitionItem.swift b/Mastodon/Scene/Transition/MediaPreview/MediaPreviewTransitionItem.swift index 47fdd215d..7024d3056 100644 --- a/Mastodon/Scene/Transition/MediaPreview/MediaPreviewTransitionItem.swift +++ b/Mastodon/Scene/Transition/MediaPreview/MediaPreviewTransitionItem.swift @@ -30,6 +30,8 @@ class MediaPreviewTransitionItem: Identifiable { var snapshotRaw: UIView? var snapshotTransitioning: UIView? var touchOffset: CGVector = CGVector.zero + var interactiveTransitionMaskView: UIView? + var interactiveTransitionMaskLayer: CAShapeLayer? init(id: UUID = UUID(), source: Source, previewableViewController: MediaPreviewableViewController) { self.id = id diff --git a/Mastodon/Scene/Transition/MediaPreview/MediaPreviewableViewController.swift b/Mastodon/Scene/Transition/MediaPreview/MediaPreviewableViewController.swift index 8029c09da..63cf10c3e 100644 --- a/Mastodon/Scene/Transition/MediaPreview/MediaPreviewableViewController.swift +++ b/Mastodon/Scene/Transition/MediaPreview/MediaPreviewableViewController.swift @@ -7,7 +7,7 @@ import UIKit -protocol MediaPreviewableViewController: AnyObject { +protocol MediaPreviewableViewController: UIViewController { var mediaPreviewTransitionController: MediaPreviewTransitionController { get } func sourceFrame(transitionItem: MediaPreviewTransitionItem, index: Int) -> CGRect? } From a349a76bdd9832aa49e584845a27db29e2768d9f Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 11 May 2021 16:22:29 +0800 Subject: [PATCH 07/23] fix: register error prompt label dynamic type font issue --- .../MastodonRegisterViewController.swift | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift index 48401c0c9..b6214d4e7 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift @@ -16,6 +16,9 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O static let avatarImageMaxSizeInPixel = CGSize(width: 400, height: 400) + static let textFieldLabelFont = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 22) + static let errorPromptLabelFont = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 13, weight: .semibold), maximumPointSize: 18) + var disposeBag = Set() weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } @@ -100,7 +103,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O let domainLabel: UILabel = { let label = UILabel() - label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 22) + label.font = MastodonRegisterViewController.textFieldLabelFont label.textColor = Asset.Colors.Label.primary.color return label }() @@ -114,7 +117,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O textField.textColor = Asset.Colors.Label.primary.color textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Username.placeholder, attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color, - NSAttributedString.Key.font: UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 22)]) + NSAttributedString.Key.font: MastodonRegisterViewController.textFieldLabelFont]) textField.borderStyle = UITextField.BorderStyle.roundedRect let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) textField.leftView = paddingView @@ -125,7 +128,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O let usernameErrorPromptLabel: UILabel = { let label = UILabel() let color = Asset.Colors.danger.color - let font = UIFont.preferredFont(forTextStyle: .caption1) + let font = MastodonRegisterViewController.errorPromptLabelFont return label }() @@ -137,7 +140,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O textField.textColor = Asset.Colors.Label.primary.color textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.DisplayName.placeholder, attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color, - NSAttributedString.Key.font: UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 22)]) + NSAttributedString.Key.font: MastodonRegisterViewController.textFieldLabelFont]) textField.borderStyle = UITextField.BorderStyle.roundedRect let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) textField.leftView = paddingView @@ -154,7 +157,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O textField.textColor = Asset.Colors.Label.primary.color textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Email.placeholder, attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color, - NSAttributedString.Key.font: UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 22)]) + NSAttributedString.Key.font: MastodonRegisterViewController.textFieldLabelFont]) textField.borderStyle = UITextField.BorderStyle.roundedRect let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) textField.leftView = paddingView @@ -165,7 +168,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O let emailErrorPromptLabel: UILabel = { let label = UILabel() let color = Asset.Colors.danger.color - let font = UIFont.preferredFont(forTextStyle: .caption1) + let font = MastodonRegisterViewController.errorPromptLabelFont return label }() @@ -179,7 +182,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O textField.textColor = Asset.Colors.Label.primary.color textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Password.placeholder, attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color, - NSAttributedString.Key.font: UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 22)]) + NSAttributedString.Key.font: MastodonRegisterViewController.textFieldLabelFont]) textField.borderStyle = UITextField.BorderStyle.roundedRect let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) textField.leftView = paddingView @@ -196,7 +199,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O let passwordErrorPromptLabel: UILabel = { let label = UILabel() let color = Asset.Colors.danger.color - let font = UIFont.preferredFont(forTextStyle: .caption1) + let font = MastodonRegisterViewController.errorPromptLabelFont return label }() @@ -209,7 +212,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O textField.textColor = Asset.Colors.Label.primary.color textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Invite.registrationUserInviteRequest, attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color, - NSAttributedString.Key.font: UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 22)]) + NSAttributedString.Key.font: MastodonRegisterViewController.textFieldLabelFont]) textField.borderStyle = UITextField.BorderStyle.roundedRect let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) textField.leftView = paddingView @@ -220,7 +223,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O let reasonErrorPromptLabel: UILabel = { let label = UILabel() let color = Asset.Colors.danger.color - let font = UIFont.preferredFont(forTextStyle: .caption1) + let font = MastodonRegisterViewController.errorPromptLabelFont return label }() From 940e69456dee68a604c5ee2757dc0574f003ff45 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 11 May 2021 16:26:42 +0800 Subject: [PATCH 08/23] fix: meta label in post thread scene layout issue --- Mastodon/Scene/Share/View/Content/ThreadMetaView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Mastodon/Scene/Share/View/Content/ThreadMetaView.swift b/Mastodon/Scene/Share/View/Content/ThreadMetaView.swift index e84939d32..a4b0886d6 100644 --- a/Mastodon/Scene/Share/View/Content/ThreadMetaView.swift +++ b/Mastodon/Scene/Share/View/Content/ThreadMetaView.swift @@ -70,6 +70,7 @@ extension ThreadMetaView { containerStackView.addArrangedSubview(actionButtonStackView) actionButtonStackView.axis = .horizontal + actionButtonStackView.spacing = 20 actionButtonStackView.addArrangedSubview(reblogButton) actionButtonStackView.addArrangedSubview(favoriteButton) From 1d552d38f584dda863f5116b3dbb908faf63e051 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 11 May 2021 16:44:23 +0800 Subject: [PATCH 09/23] fix: visibility icon layout issue --- Mastodon/Scene/Share/View/Content/StatusView.swift | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index c06c956e0..5ebdd17c5 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -295,7 +295,7 @@ extension StatusView { authorMetaContainerStackView.axis = .vertical authorMetaContainerStackView.spacing = 4 - // title container: [display name | "·" | date] + // title container: [display name | "·" | date | padding | visibility] let titleContainerStackView = UIStackView() authorMetaContainerStackView.addArrangedSubview(titleContainerStackView) titleContainerStackView.axis = .horizontal @@ -308,12 +308,15 @@ extension StatusView { titleContainerStackView.alignment = .firstBaseline titleContainerStackView.addArrangedSubview(nameTrialingDotLabel) titleContainerStackView.addArrangedSubview(dateLabel) + titleContainerStackView.addArrangedSubview(UIView()) // padding + titleContainerStackView.addArrangedSubview(visibilityImageView) nameLabel.setContentHuggingPriority(.defaultHigh + 1, for: .horizontal) nameTrialingDotLabel.setContentHuggingPriority(.defaultHigh + 2, for: .horizontal) nameTrialingDotLabel.setContentCompressionResistancePriority(.required - 2, for: .horizontal) dateLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) dateLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal) - + visibilityImageView.setContentHuggingPriority(.defaultHigh + 3, for: .horizontal) + // subtitle container: [username] let subtitleContainerStackView = UIStackView() authorMetaContainerStackView.addArrangedSubview(subtitleContainerStackView) @@ -324,10 +327,6 @@ extension StatusView { authorContainerStackView.addArrangedSubview(revealContentWarningButton) revealContentWarningButton.setContentHuggingPriority(.required - 2, for: .horizontal) - // visibility ImageView - authorContainerStackView.addArrangedSubview(visibilityImageView) - visibilityImageView.setContentHuggingPriority(.required - 2, for: .horizontal) - authorContainerStackView.translatesAutoresizingMaskIntoConstraints = false authorContainerView.addSubview(authorContainerStackView) NSLayoutConstraint.activate([ From c11b0bec1ee59f4d242110260caaabd73400a06c Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 11 May 2021 16:44:41 +0800 Subject: [PATCH 10/23] fix: profile timeline loader missing when paging issue --- .../Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift index 8e6f1314f..7e4ec8728 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift @@ -32,6 +32,7 @@ extension UserTimelineViewModel { // set empty section to make update animation top-to-bottom style var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) + snapshot.appendItems([.bottomLoader], toSection: .main) diffableDataSource?.apply(snapshot) } From 108c6af575ee84c12d5ade94eaaad14a491cb459 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 11 May 2021 17:07:08 +0800 Subject: [PATCH 11/23] fix: notification setting label accessibility layout issue --- .../Scene/Settings/SettingsViewController.swift | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Mastodon/Scene/Settings/SettingsViewController.swift b/Mastodon/Scene/Settings/SettingsViewController.swift index 3d6ebc470..1c69ef6c5 100644 --- a/Mastodon/Scene/Settings/SettingsViewController.swift +++ b/Mastodon/Scene/Settings/SettingsViewController.swift @@ -56,24 +56,22 @@ class SettingsViewController: UIViewController, NeedsDependency { view.translatesAutoresizingMaskIntoConstraints = false view.isLayoutMarginsRelativeArrangement = true view.axis = .horizontal - view.alignment = .fill - view.distribution = .equalSpacing view.spacing = 4 return view }() + let notifyLabel = UILabel() private(set) lazy var notifySectionHeader: UIView = { let view = notifySectionHeaderStackView - - let notifyLabel = UILabel() notifyLabel.translatesAutoresizingMaskIntoConstraints = false + notifyLabel.adjustsFontForContentSizeCategory = true notifyLabel.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: UIFont.systemFont(ofSize: 20, weight: .semibold)) notifyLabel.textColor = Asset.Colors.Label.primary.color notifyLabel.text = L10n.Scene.Settings.Section.Notifications.Trigger.title - // accessibility - notifyLabel.numberOfLines = 0 view.addArrangedSubview(notifyLabel) view.addArrangedSubview(whoButton) + whoButton.setContentHuggingPriority(.defaultHigh + 1, for: .horizontal) + whoButton.setContentHuggingPriority(.defaultHigh + 1, for: .vertical) return view }() @@ -83,6 +81,7 @@ class SettingsViewController: UIViewController, NeedsDependency { whoButton.showsMenuAsPrimaryAction = true whoButton.setBackgroundColor(Asset.Colors.battleshipGrey.color, for: .normal) whoButton.setTitleColor(Asset.Colors.Label.primary.color, for: .normal) + whoButton.titleLabel?.adjustsFontForContentSizeCategory = true whoButton.titleLabel?.font = UIFontMetrics(forTextStyle: .title3).scaledFont(for: UIFont.systemFont(ofSize: 20, weight: .semibold)) whoButton.contentEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5) whoButton.layer.cornerRadius = 10 @@ -113,6 +112,7 @@ class SettingsViewController: UIViewController, NeedsDependency { view.alignment = .center let label = ActiveLabel(style: .default) + label.adjustsFontForContentSizeCategory = true label.textAlignment = .center label.configure(content: "Mastodon is open source software. You can contribute or report issues on GitHub at tootsuite/mastodon (v3.3.0).", emojiDict: [:]) label.delegate = self @@ -153,10 +153,13 @@ class SettingsViewController: UIViewController, NeedsDependency { // MAKR: - Private methods private func updateSectionHeaderStackViewLayout() { + // accessibility if traitCollection.preferredContentSizeCategory < .accessibilityMedium { notifySectionHeaderStackView.axis = .horizontal + notifyLabel.numberOfLines = 1 } else { notifySectionHeaderStackView.axis = .vertical + notifyLabel.numberOfLines = 0 } } From 6ba6598b96b05ef095cc759c80f5eaabfcfec619 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 12 May 2021 18:26:53 +0800 Subject: [PATCH 12/23] feat: add accessibility supports for timeline --- Localization/app.json | 21 ++++ Mastodon.xcodeproj/project.pbxproj | 2 + .../Diffiable/Section/StatusSection.swift | 40 ++++++- Mastodon/Extension/ActiveLabel.swift | 109 ++++++++++++++++++ Mastodon/Generated/Strings.swift | 42 +++++++ ...Provider+StatusTableViewCellDelegate.swift | 7 +- .../Resources/en.lproj/Localizable.strings | 15 +++ .../MediaPreview/MediaPreviewViewModel.swift | 6 +- .../Paging/Image/MediaPreviewImageView.swift | 1 + .../MediaPreviewImageViewController.swift | 1 + .../Image/MediaPreviewImageViewModel.swift | 4 + .../Container/MosaicImageViewContainer.swift | 1 + .../Content/ContentWarningOverlayView.swift | 3 + .../Scene/Share/View/Content/StatusView.swift | 2 + .../Share/View/Content/ThreadMetaView.swift | 4 + .../TableviewCell/StatusTableViewCell.swift | 8 ++ .../View/ToolBar/ActionToolBarContainer.swift | 13 +++ .../ViewModel/MosaicImageViewModel.swift | 4 +- 18 files changed, 275 insertions(+), 8 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index 3528153ca..98e9accd4 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -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" } } }, diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 506e32dea..0abd7ad95 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -756,6 +756,7 @@ DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveUserInterfaceStyleBarButtonItem.swift; sourceTree = ""; }; DB084B5625CBC56C00F898ED /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = ""; }; DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = ""; }; + DB0F814C264BBDD900F2A12B /* ActiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ActiveLabel.swift; path = ../ActiveLabel.swift; sourceTree = ""; }; DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDimmableButton.swift; sourceTree = ""; }; DB1D186B25EF5BA7003F1F23 /* PollTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollTableView.swift; sourceTree = ""; }; @@ -1645,6 +1646,7 @@ children = ( DBF53F5F25C14E88008AAC7B /* Mastodon.xctestplan */, DBF53F6025C14E9D008AAC7B /* MastodonSDK.xctestplan */, + DB0F814C264BBDD900F2A12B /* ActiveLabel.swift */, DB3D0FED25BAA42200EAA174 /* MastodonSDK */, DB427DD425BAA00100D1B89D /* Mastodon */, DB427DEB25BAA00100D1B89D /* MastodonTests */, diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index ef5ccb502..e7ca8182a 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -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) diff --git a/Mastodon/Extension/ActiveLabel.swift b/Mastodon/Extension/ActiveLabel.swift index a26838850..4e1d855b1 100644 --- a/Mastodon/Extension/ActiveLabel.swift +++ b/Mastodon/Extension/ActiveLabel.swift @@ -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 +// } + +} diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index a4457b2f5..3785a79d9 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -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") diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift index bc0a8b2d9..3b96299d2 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift @@ -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) + } } } diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 685d11188..b685e9651 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -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 diff --git a/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift b/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift index f3037c080..cd019fc9b 100644 --- a/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift +++ b/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift @@ -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 diff --git a/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageView.swift b/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageView.swift index 0e4aa6d89..aa11e494d 100644 --- a/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageView.swift +++ b/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageView.swift @@ -18,6 +18,7 @@ final class MediaPreviewImageView: UIScrollView { imageView.isUserInteractionEnabled = true // accessibility imageView.accessibilityIgnoresInvertColors = true + imageView.isAccessibilityElement = true return imageView }() diff --git a/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewController.swift b/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewController.swift index 7ac3c2024..e1f2736ff 100644 --- a/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewController.swift +++ b/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewController.swift @@ -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) } diff --git a/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewModel.swift b/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewModel.swift index 6be61dfc4..c9afac8c7 100644 --- a/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewModel.swift +++ b/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewModel.swift @@ -17,10 +17,12 @@ class MediaPreviewImageViewModel { // output let image: CurrentValueSubject + 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 { diff --git a/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift b/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift index 486f97e3c..0dccd5930 100644 --- a/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift +++ b/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift @@ -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 } } } diff --git a/Mastodon/Scene/Share/View/Content/ContentWarningOverlayView.swift b/Mastodon/Scene/Share/View/Content/ContentWarningOverlayView.swift index 3dc091aa0..9d9f627dc 100644 --- a/Mastodon/Scene/Share/View/Content/ContentWarningOverlayView.swift +++ b/Mastodon/Scene/Share/View/Content/ContentWarningOverlayView.swift @@ -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 }() diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index 5ebdd17c5..ffe56b8b8 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -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 }() diff --git a/Mastodon/Scene/Share/View/Content/ThreadMetaView.swift b/Mastodon/Scene/Share/View/Content/ThreadMetaView.swift index a4b0886d6..ff5fa58d6 100644 --- a/Mastodon/Scene/Share/View/Content/ThreadMetaView.swift +++ b/Mastodon/Scene/Share/View/Content/ThreadMetaView.swift @@ -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?) { diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift index 462577a4b..ca6a9e7eb 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift @@ -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 { } + } +} diff --git a/Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift b/Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift index 2ed31abb4..f10e55941 100644 --- a/Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift +++ b/Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift @@ -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 diff --git a/Mastodon/Scene/Share/ViewModel/MosaicImageViewModel.swift b/Mastodon/Scene/Share/ViewModel/MosaicImageViewModel.swift index c2ad3d4f6..9563a19cd 100644 --- a/Mastodon/Scene/Share/ViewModel/MosaicImageViewModel.swift +++ b/Mastodon/Scene/Share/ViewModel/MosaicImageViewModel.swift @@ -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) From 8d16a1cec418ec89559d388108f007217835d30e Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 13 May 2021 11:41:35 +0800 Subject: [PATCH 13/23] chore: update package version --- Mastodon.xcodeproj/project.pbxproj | 4 +--- Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 0abd7ad95..0feed3c97 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -756,7 +756,6 @@ DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveUserInterfaceStyleBarButtonItem.swift; sourceTree = ""; }; DB084B5625CBC56C00F898ED /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = ""; }; DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = ""; }; - DB0F814C264BBDD900F2A12B /* ActiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ActiveLabel.swift; path = ../ActiveLabel.swift; sourceTree = ""; }; DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDimmableButton.swift; sourceTree = ""; }; DB1D186B25EF5BA7003F1F23 /* PollTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollTableView.swift; sourceTree = ""; }; @@ -1646,7 +1645,6 @@ children = ( DBF53F5F25C14E88008AAC7B /* Mastodon.xctestplan */, DBF53F6025C14E9D008AAC7B /* MastodonSDK.xctestplan */, - DB0F814C264BBDD900F2A12B /* ActiveLabel.swift */, DB3D0FED25BAA42200EAA174 /* MastodonSDK */, DB427DD425BAA00100D1B89D /* Mastodon */, DB427DEB25BAA00100D1B89D /* MastodonTests */, @@ -3918,7 +3916,7 @@ repositoryURL = "https://github.com/TwidereProject/ActiveLabel.swift"; requirement = { kind = exactVersion; - version = 5.0.1; + version = 5.0.2; }; }; 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */ = { diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0c80f20e5..38325ae97 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/TwidereProject/ActiveLabel.swift", "state": { "branch": null, - "revision": "40e104063d825d1125ef4b8eeb6460eba8a57483", - "version": "5.0.1" + "revision": "2132a7bf8da2bea74bb49b0b815950ea4d8f6a7e", + "version": "5.0.2" } }, { @@ -69,7 +69,7 @@ "repositoryURL": "https://github.com/onevcat/Kingfisher.git", "state": { "branch": null, - "revision": "15d199e84677303a7004ed2c5ecaa1a90f3863f8", + "revision": "bbc4bc4def7eb05a7ba8e1219f80ee9be327334e", "version": "6.2.1" } }, From ec2be58952211549acfc5b88324b690464413aad Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 13 May 2021 14:27:57 +0800 Subject: [PATCH 14/23] feat: add accessibility supports for compose scene --- Localization/app.json | 25 +++++++++- .../Section/CustomEmojiPickerSection.swift | 1 + .../Diffiable/Section/StatusSection.swift | 1 + Mastodon/Generated/Strings.swift | 50 +++++++++++++++++++ .../Resources/en.lproj/Localizable.strings | 17 +++++++ ...tomEmojiPickerItemCollectionViewCell.swift | 4 ++ .../Scene/Compose/ComposeViewController.swift | 26 ++++++++++ .../Compose/View/ComposeToolbarView.swift | 6 +++ .../Scene/MainTab/MainTabBarController.swift | 9 ++-- .../Scene/Profile/ProfileViewController.swift | 6 +++ .../Scene/Share/View/Content/StatusView.swift | 8 ++- 11 files changed, 147 insertions(+), 6 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index 98e9accd4..327c72625 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -74,10 +74,17 @@ "settings": "Settings", "delete": "Delete" }, + "tabs": { + "home": "Home", + "search": "Search", + "notification": "Notification", + "profile": "Profile" + }, "status": { "user_reblogged": "%s reblogged", "user_replied_to": "Replied to %s", "show_post": "Show Post", + "show_user_profile": "Show user profile", "content_warning": "content warning", "content_warning_text": "cw: %s", "media_content_warning": "Tap to reveal that may be sensitive", @@ -331,6 +338,17 @@ "unlisted": "Unlisted", "private": "Followers only", "direct": "Only people I mention" + }, + "accessibility": { + "append_attachment": "Append attachment", + "append_poll": "Append poll", + "remove_poll": "Remove poll", + "custom_emoji_picker": "Custom emoji picker", + "enable_content_warning": "Enable content warning", + "disable_content_warning": "Disable content warning", + "post_visibility_menu": "Post visibility menu", + "input_limit_remains_count": "Input limit remains %ld", + "input_limit_exceeds_count": "Input limit exceeds %ld" } }, "profile": { @@ -338,7 +356,12 @@ "dashboard": { "posts": "posts", "following": "following", - "followers": "followers" + "followers": "followers", + "accessibility": { + "count_posts": "%ld posts", + "count_following": "%ld following", + "count_followers": "%ld followers" + } }, "segmented_control": { "posts": "Posts", diff --git a/Mastodon/Diffiable/Section/CustomEmojiPickerSection.swift b/Mastodon/Diffiable/Section/CustomEmojiPickerSection.swift index 06b626d0e..20dc5b809 100644 --- a/Mastodon/Diffiable/Section/CustomEmojiPickerSection.swift +++ b/Mastodon/Diffiable/Section/CustomEmojiPickerSection.swift @@ -32,6 +32,7 @@ extension CustomEmojiPickerSection { ], completionHandler: nil ) + cell.accessibilityLabel = attribute.emoji.shortcode return cell } } diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index e7ca8182a..d139e061f 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -102,6 +102,7 @@ extension StatusSection { case .root: cell.statusView.activeTextLabel.isAccessibilityElement = false var accessibilityElements: [Any] = [] + accessibilityElements.append(cell.statusView.avatarView) accessibilityElements.append(cell.statusView.nameLabel) accessibilityElements.append(cell.statusView.dateLabel) accessibilityElements.append(contentsOf: cell.statusView.activeTextLabel.createAccessibilityElements()) diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 3785a79d9..de69f016e 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -200,6 +200,8 @@ internal enum L10n { internal static let mediaContentWarning = L10n.tr("Localizable", "Common.Controls.Status.MediaContentWarning") /// Show Post internal static let showPost = L10n.tr("Localizable", "Common.Controls.Status.ShowPost") + /// Show user profile + internal static let showUserProfile = L10n.tr("Localizable", "Common.Controls.Status.ShowUserProfile") /// %@ reblogged internal static func userReblogged(_ p1: Any) -> String { return L10n.tr("Localizable", "Common.Controls.Status.UserReblogged", String(describing: p1)) @@ -267,6 +269,16 @@ internal enum L10n { internal static let url = L10n.tr("Localizable", "Common.Controls.Status.Tag.Url") } } + internal enum Tabs { + /// Home + internal static let home = L10n.tr("Localizable", "Common.Controls.Tabs.Home") + /// Notification + internal static let notification = L10n.tr("Localizable", "Common.Controls.Tabs.Notification") + /// Profile + internal static let profile = L10n.tr("Localizable", "Common.Controls.Tabs.Profile") + /// Search + internal static let search = L10n.tr("Localizable", "Common.Controls.Tabs.Search") + } internal enum Timeline { internal enum Accessibility { /// %@ favorites @@ -326,6 +338,30 @@ internal enum L10n { internal static func replyingToUser(_ p1: Any) -> String { return L10n.tr("Localizable", "Scene.Compose.ReplyingToUser", String(describing: p1)) } + internal enum Accessibility { + /// Append attachment + internal static let appendAttachment = L10n.tr("Localizable", "Scene.Compose.Accessibility.AppendAttachment") + /// Append poll + internal static let appendPoll = L10n.tr("Localizable", "Scene.Compose.Accessibility.AppendPoll") + /// Custom emoji picker + internal static let customEmojiPicker = L10n.tr("Localizable", "Scene.Compose.Accessibility.CustomEmojiPicker") + /// Disable content warning + internal static let disableContentWarning = L10n.tr("Localizable", "Scene.Compose.Accessibility.DisableContentWarning") + /// Enable content warning + internal static let enableContentWarning = L10n.tr("Localizable", "Scene.Compose.Accessibility.EnableContentWarning") + /// Input limit exceeds %ld + internal static func inputLimitExceedsCount(_ p1: Int) -> String { + return L10n.tr("Localizable", "Scene.Compose.Accessibility.InputLimitExceedsCount", p1) + } + /// Input limit remains %ld + internal static func inputLimitRemainsCount(_ p1: Int) -> String { + return L10n.tr("Localizable", "Scene.Compose.Accessibility.InputLimitRemainsCount", p1) + } + /// Post visibility menu + internal static let postVisibilityMenu = L10n.tr("Localizable", "Scene.Compose.Accessibility.PostVisibilityMenu") + /// Remove poll + internal static let removePoll = L10n.tr("Localizable", "Scene.Compose.Accessibility.RemovePoll") + } internal enum Attachment { /// This %@ is broken and can't be\nuploaded to Mastodon. internal static func attachmentBroken(_ p1: Any) -> String { @@ -481,6 +517,20 @@ internal enum L10n { internal static let following = L10n.tr("Localizable", "Scene.Profile.Dashboard.Following") /// posts internal static let posts = L10n.tr("Localizable", "Scene.Profile.Dashboard.Posts") + internal enum Accessibility { + /// %ld followers + internal static func countFollowers(_ p1: Int) -> String { + return L10n.tr("Localizable", "Scene.Profile.Dashboard.Accessibility.CountFollowers", p1) + } + /// %ld following + internal static func countFollowing(_ p1: Int) -> String { + return L10n.tr("Localizable", "Scene.Profile.Dashboard.Accessibility.CountFollowing", p1) + } + /// %ld posts + internal static func countPosts(_ p1: Int) -> String { + return L10n.tr("Localizable", "Scene.Profile.Dashboard.Accessibility.CountPosts", p1) + } + } } internal enum RelationshipActionAlert { internal enum ConfirmUnblockUsre { diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index b685e9651..0549c4a3b 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -81,6 +81,7 @@ 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.ShowUserProfile" = "Show user profile"; "Common.Controls.Status.Tag.Email" = "Email"; "Common.Controls.Status.Tag.Emoji" = "Emoji"; "Common.Controls.Status.Tag.Hashtag" = "Hashtag"; @@ -89,6 +90,10 @@ Please check your internet connection."; "Common.Controls.Status.Tag.Url" = "URL"; "Common.Controls.Status.UserReblogged" = "%@ reblogged"; "Common.Controls.Status.UserRepliedTo" = "Replied to %@"; +"Common.Controls.Tabs.Home" = "Home"; +"Common.Controls.Tabs.Notification" = "Notification"; +"Common.Controls.Tabs.Profile" = "Profile"; +"Common.Controls.Tabs.Search" = "Search"; "Common.Controls.Timeline.Accessibility.CountFavorites" = "%@ favorites"; "Common.Controls.Timeline.Accessibility.CountReblogs" = "%@ reblogs"; "Common.Controls.Timeline.Accessibility.CountReplies" = "%@ replies"; @@ -105,6 +110,15 @@ Your account looks like this to them."; "Common.Controls.Timeline.Loader.ShowMoreReplies" = "Show more replies"; "Common.Countable.Photo.Multiple" = "photos"; "Common.Countable.Photo.Single" = "photo"; +"Scene.Compose.Accessibility.AppendAttachment" = "Append attachment"; +"Scene.Compose.Accessibility.AppendPoll" = "Append poll"; +"Scene.Compose.Accessibility.CustomEmojiPicker" = "Custom emoji picker"; +"Scene.Compose.Accessibility.DisableContentWarning" = "Disable content warning"; +"Scene.Compose.Accessibility.EnableContentWarning" = "Enable content warning"; +"Scene.Compose.Accessibility.InputLimitExceedsCount" = "Input limit exceeds %ld"; +"Scene.Compose.Accessibility.InputLimitRemainsCount" = "Input limit remains %ld"; +"Scene.Compose.Accessibility.PostVisibilityMenu" = "Post visibility menu"; +"Scene.Compose.Accessibility.RemovePoll" = "Remove poll"; "Scene.Compose.Attachment.AttachmentBroken" = "This %@ is broken and can't be uploaded to Mastodon."; "Scene.Compose.Attachment.DescriptionPhoto" = "Describe photo for low vision people..."; @@ -159,6 +173,9 @@ tap the link to confirm your account."; "Scene.Notification.Action.Reblog" = "rebloged your post"; "Scene.Notification.Title.Everything" = "Everything"; "Scene.Notification.Title.Mentions" = "Mentions"; +"Scene.Profile.Dashboard.Accessibility.CountFollowers" = "%ld followers"; +"Scene.Profile.Dashboard.Accessibility.CountFollowing" = "%ld following"; +"Scene.Profile.Dashboard.Accessibility.CountPosts" = "%ld posts"; "Scene.Profile.Dashboard.Followers" = "followers"; "Scene.Profile.Dashboard.Following" = "following"; "Scene.Profile.Dashboard.Posts" = "posts"; diff --git a/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerItemCollectionViewCell.swift b/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerItemCollectionViewCell.swift index 7acc49aeb..49e6c1fe2 100644 --- a/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerItemCollectionViewCell.swift +++ b/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerItemCollectionViewCell.swift @@ -47,6 +47,10 @@ extension CustomEmojiPickerItemCollectionViewCell { emojiImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), emojiImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), ]) + + isAccessibilityElement = true + accessibilityTraits = .button + accessibilityHint = "emoji" } } diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index dedcd4050..3f43588fd 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -260,6 +260,21 @@ extension ComposeViewController { .receive(on: DispatchQueue.main) .assign(to: \.isEnabled, on: composeToolbarView.pollButton) .store(in: &disposeBag) + + Publishers.CombineLatest( + viewModel.isPollComposing, + viewModel.isPollToolbarButtonEnabled + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] isPollComposing, isPollToolbarButtonEnabled in + guard let self = self else { return } + guard isPollToolbarButtonEnabled else { + self.composeToolbarView.pollButton.accessibilityLabel = L10n.Scene.Compose.Accessibility.appendPoll + return + } + self.composeToolbarView.pollButton.accessibilityLabel = isPollComposing ? L10n.Scene.Compose.Accessibility.removePoll : L10n.Scene.Compose.Accessibility.appendPoll + } + .store(in: &disposeBag) // bind image picker toolbar state viewModel.attachmentServices @@ -271,6 +286,15 @@ extension ComposeViewController { } .store(in: &disposeBag) + // bind content warning button state + viewModel.isContentWarningComposing + .receive(on: DispatchQueue.main) + .sink { [weak self] isContentWarningComposing in + guard let self = self else { return } + self.composeToolbarView.contentWarningButton.accessibilityLabel = isContentWarningComposing ? L10n.Scene.Compose.Accessibility.disableContentWarning : L10n.Scene.Compose.Accessibility.enableContentWarning + } + .store(in: &disposeBag) + // bind visibility toolbar UI Publishers.CombineLatest( viewModel.selectedStatusVisibility, @@ -294,9 +318,11 @@ extension ComposeViewController { case _ where count < 0: self.composeToolbarView.characterCountLabel.font = .systemFont(ofSize: 24, weight: .bold) self.composeToolbarView.characterCountLabel.textColor = Asset.Colors.danger.color + self.composeToolbarView.characterCountLabel.accessibilityLabel = L10n.Scene.Compose.Accessibility.inputLimitExceedsCount(abs(count)) default: self.composeToolbarView.characterCountLabel.font = .systemFont(ofSize: 15, weight: .regular) self.composeToolbarView.characterCountLabel.textColor = Asset.Colors.Label.secondary.color + self.composeToolbarView.characterCountLabel.accessibilityLabel = L10n.Scene.Compose.Accessibility.inputLimitRemainsCount(count) } } .store(in: &disposeBag) diff --git a/Mastodon/Scene/Compose/View/ComposeToolbarView.swift b/Mastodon/Scene/Compose/View/ComposeToolbarView.swift index 6aabc4572..68f5eed06 100644 --- a/Mastodon/Scene/Compose/View/ComposeToolbarView.swift +++ b/Mastodon/Scene/Compose/View/ComposeToolbarView.swift @@ -28,6 +28,7 @@ final class ComposeToolbarView: UIView { let button = HighlightDimmableButton() ComposeToolbarView.configureToolbarButtonAppearance(button: button) button.setImage(UIImage(systemName: "photo", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal) + button.accessibilityLabel = L10n.Scene.Compose.Accessibility.appendAttachment return button }() @@ -35,6 +36,7 @@ final class ComposeToolbarView: UIView { let button = HighlightDimmableButton(type: .custom) ComposeToolbarView.configureToolbarButtonAppearance(button: button) button.setImage(UIImage(systemName: "list.bullet", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .medium)), for: .normal) + button.accessibilityLabel = L10n.Scene.Compose.Accessibility.appendPoll return button }() @@ -45,6 +47,7 @@ final class ComposeToolbarView: UIView { .af.imageScaled(to: CGSize(width: 20, height: 20)) .withRenderingMode(.alwaysTemplate) button.setImage(image, for: .normal) + button.accessibilityLabel = L10n.Scene.Compose.Accessibility.customEmojiPicker return button }() @@ -52,6 +55,7 @@ final class ComposeToolbarView: UIView { let button = HighlightDimmableButton() ComposeToolbarView.configureToolbarButtonAppearance(button: button) button.setImage(UIImage(systemName: "exclamationmark.shield", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal) + button.accessibilityLabel = L10n.Scene.Compose.Accessibility.enableContentWarning return button }() @@ -59,6 +63,7 @@ final class ComposeToolbarView: UIView { let button = HighlightDimmableButton() ComposeToolbarView.configureToolbarButtonAppearance(button: button) button.setImage(UIImage(systemName: "person.3", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium)), for: .normal) + button.accessibilityLabel = L10n.Scene.Compose.Accessibility.postVisibilityMenu return button }() @@ -67,6 +72,7 @@ final class ComposeToolbarView: UIView { label.font = .systemFont(ofSize: 15, weight: .regular) label.text = "500" label.textColor = Asset.Colors.Label.secondary.color + label.accessibilityLabel = L10n.Scene.Compose.Accessibility.inputLimitRemainsCount(500) return label }() diff --git a/Mastodon/Scene/MainTab/MainTabBarController.swift b/Mastodon/Scene/MainTab/MainTabBarController.swift index 5fd4c8256..8b1701578 100644 --- a/Mastodon/Scene/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/MainTab/MainTabBarController.swift @@ -25,10 +25,10 @@ class MainTabBarController: UITabBarController { var title: String { switch self { - case .home: return "Home" - case .search: return "Search" - case .notification: return "Notification" - case .me: return "Me" + case .home: return L10n.Common.Controls.Tabs.home + case .search: return L10n.Common.Controls.Tabs.search + case .notification: return L10n.Common.Controls.Tabs.notification + case .me: return L10n.Common.Controls.Tabs.profile } } @@ -99,6 +99,7 @@ extension MainTabBarController { let viewController = tab.viewController(context: context, coordinator: coordinator) viewController.tabBarItem.title = "" // set text to empty string for image only style (SDK failed to layout when set to nil) viewController.tabBarItem.image = tab.image + viewController.tabBarItem.accessibilityLabel = tab.title return viewController } setViewControllers(viewControllers, animated: false) diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index d186d13df..c60c20400 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -479,6 +479,8 @@ extension ProfileViewController { guard let self = self else { return } let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-" self.profileHeaderViewController.profileHeaderView.statusDashboardView.postDashboardMeterView.numberLabel.text = text + self.profileHeaderViewController.profileHeaderView.statusDashboardView.postDashboardMeterView.isAccessibilityElement = true + self.profileHeaderViewController.profileHeaderView.statusDashboardView.postDashboardMeterView.accessibilityLabel = L10n.Scene.Profile.Dashboard.Accessibility.countPosts(count ?? 0) } .store(in: &disposeBag) viewModel.followingCount @@ -486,6 +488,8 @@ extension ProfileViewController { guard let self = self else { return } let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-" self.profileHeaderViewController.profileHeaderView.statusDashboardView.followingDashboardMeterView.numberLabel.text = text + self.profileHeaderViewController.profileHeaderView.statusDashboardView.followingDashboardMeterView.isAccessibilityElement = true + self.profileHeaderViewController.profileHeaderView.statusDashboardView.followingDashboardMeterView.accessibilityLabel = L10n.Scene.Profile.Dashboard.Accessibility.countFollowing(count ?? 0) } .store(in: &disposeBag) viewModel.followersCount @@ -493,6 +497,8 @@ extension ProfileViewController { guard let self = self else { return } let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-" self.profileHeaderViewController.profileHeaderView.statusDashboardView.followersDashboardMeterView.numberLabel.text = text + self.profileHeaderViewController.profileHeaderView.statusDashboardView.followersDashboardMeterView.isAccessibilityElement = true + self.profileHeaderViewController.profileHeaderView.statusDashboardView.followersDashboardMeterView.accessibilityLabel = L10n.Scene.Profile.Dashboard.Accessibility.countFollowers(count ?? 0) } .store(in: &disposeBag) diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index ffe56b8b8..46930f33d 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -75,7 +75,13 @@ final class StatusView: UIView { return label }() - let avatarView = UIView() + let avatarView: UIView = { + let view = UIView() + view.isAccessibilityElement = true + view.accessibilityTraits = .button + view.accessibilityLabel = L10n.Common.Controls.Status.showUserProfile + return view + }() let avatarButton: UIButton = { let button = HighlightDimmableButton(type: .custom) let placeholderImage = UIImage.placeholder(size: avatarImageSize, color: .systemFill) From 78eaf226a4cca690b0ab91e0a2aaafe9face2ed7 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 13 May 2021 14:46:40 +0800 Subject: [PATCH 15/23] chore: add ar language code supports --- .../Sources/StringsConvertor/main.swift | 1 + .../StringsConvertor/scripts/build.sh | 4 + Mastodon.xcodeproj/project.pbxproj | 7 + Mastodon/Resources/ar.lproj/InfoPlist.strings | 2 + .../Resources/ar.lproj/Localizable.strings | 302 ++++++++++++++++++ 5 files changed, 316 insertions(+) create mode 100644 Mastodon/Resources/ar.lproj/InfoPlist.strings create mode 100644 Mastodon/Resources/ar.lproj/Localizable.strings diff --git a/Localization/StringsConvertor/Sources/StringsConvertor/main.swift b/Localization/StringsConvertor/Sources/StringsConvertor/main.swift index 4ccbb3072..c60266f88 100644 --- a/Localization/StringsConvertor/Sources/StringsConvertor/main.swift +++ b/Localization/StringsConvertor/Sources/StringsConvertor/main.swift @@ -47,6 +47,7 @@ private func map(language: String) -> String? { case "ja_JP": return "ja" case "de_DE": return "de" case "pt_BR": return "pt-BR" + case "ar_SA": return "ar" default: return nil } } diff --git a/Localization/StringsConvertor/scripts/build.sh b/Localization/StringsConvertor/scripts/build.sh index 81e17745c..87087c3a0 100755 --- a/Localization/StringsConvertor/scripts/build.sh +++ b/Localization/StringsConvertor/scripts/build.sh @@ -21,6 +21,10 @@ mkdir -p input/en_US cp ../app.json ./input/en_US cp ../ios-infoPlist.json ./input/en_US +mkdir -p input/ar_SA +cp ../app.json ./input/ar_SA +cp ../ios-infoPlist.json ./input/ar_SA + # curl -o .zip -L ${Crowin_Latest_Build} # unzip -o -q .zip -d input # rm -rf .zip diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 0feed3c97..b274f5c7c 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -756,6 +756,8 @@ DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveUserInterfaceStyleBarButtonItem.swift; sourceTree = ""; }; DB084B5625CBC56C00F898ED /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = ""; }; DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = ""; }; + DB0F814D264CFFD300F2A12B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; }; + DB0F814E264CFFD300F2A12B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = ""; }; DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDimmableButton.swift; sourceTree = ""; }; DB1D186B25EF5BA7003F1F23 /* PollTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollTableView.swift; sourceTree = ""; }; @@ -2595,6 +2597,7 @@ knownRegions = ( en, Base, + ar, ); mainGroup = DB427DC925BAA00100D1B89D; packageReferences = ( @@ -3353,6 +3356,7 @@ isa = PBXVariantGroup; children = ( DB2B3ABD25E37E15007045F9 /* en */, + DB0F814E264CFFD300F2A12B /* ar */, ); name = InfoPlist.strings; sourceTree = ""; @@ -3361,6 +3365,7 @@ isa = PBXVariantGroup; children = ( DB3D100E25BAA75E00EAA174 /* en */, + DB0F814D264CFFD300F2A12B /* ar */, ); name = Localizable.strings; sourceTree = ""; @@ -3388,6 +3393,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -3449,6 +3455,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; diff --git a/Mastodon/Resources/ar.lproj/InfoPlist.strings b/Mastodon/Resources/ar.lproj/InfoPlist.strings new file mode 100644 index 000000000..48566ae36 --- /dev/null +++ b/Mastodon/Resources/ar.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +"NSCameraUsageDescription" = "Used to take photo for post status"; +"NSPhotoLibraryAddUsageDescription" = "Used to save photo into the Photo Library"; \ No newline at end of file diff --git a/Mastodon/Resources/ar.lproj/Localizable.strings b/Mastodon/Resources/ar.lproj/Localizable.strings new file mode 100644 index 000000000..0549c4a3b --- /dev/null +++ b/Mastodon/Resources/ar.lproj/Localizable.strings @@ -0,0 +1,302 @@ +"Common.Alerts.BlockDomain.BlockEntireDomain" = "Block entire domain"; +"Common.Alerts.BlockDomain.Title" = "Are you really, really sure you want to block the entire %@? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed."; +"Common.Alerts.Common.PleaseTryAgain" = "Please try again."; +"Common.Alerts.Common.PleaseTryAgainLater" = "Please try again later."; +"Common.Alerts.DeletePost.Delete" = "Delete"; +"Common.Alerts.DeletePost.Title" = "Are you sure you want to delete this post?"; +"Common.Alerts.DiscardPostContent.Message" = "Confirm discard composed post content."; +"Common.Alerts.DiscardPostContent.Title" = "Discard Publish"; +"Common.Alerts.PublishPostFailure.Message" = "Failed to publish the post. +Please check your internet connection."; +"Common.Alerts.PublishPostFailure.Title" = "Publish Failure"; +"Common.Alerts.SavePhotoFailure.Message" = "Please enable photo libaray access permission to save photo."; +"Common.Alerts.SavePhotoFailure.Title" = "Save Photo Failure"; +"Common.Alerts.ServerError.Title" = "Server Error"; +"Common.Alerts.SignOut.Confirm" = "Sign Out"; +"Common.Alerts.SignOut.Message" = "Are you sure you want to sign out?"; +"Common.Alerts.SignOut.Title" = "Sign out"; +"Common.Alerts.SignUpFailure.Title" = "Sign Up Failure"; +"Common.Alerts.VoteFailure.PollExpired" = "The poll has expired"; +"Common.Alerts.VoteFailure.Title" = "Vote Failure"; +"Common.Controls.Actions.Add" = "Add"; +"Common.Controls.Actions.Back" = "Back"; +"Common.Controls.Actions.BlockDomain" = "Block %@"; +"Common.Controls.Actions.Cancel" = "Cancel"; +"Common.Controls.Actions.Confirm" = "Confirm"; +"Common.Controls.Actions.Continue" = "Continue"; +"Common.Controls.Actions.Delete" = "Delete"; +"Common.Controls.Actions.Discard" = "Discard"; +"Common.Controls.Actions.Done" = "Done"; +"Common.Controls.Actions.Edit" = "Edit"; +"Common.Controls.Actions.FindPeople" = "Find people to follow"; +"Common.Controls.Actions.ManuallySearch" = "Manually search instead"; +"Common.Controls.Actions.Ok" = "OK"; +"Common.Controls.Actions.OpenInSafari" = "Open in Safari"; +"Common.Controls.Actions.Preview" = "Preview"; +"Common.Controls.Actions.Remove" = "Remove"; +"Common.Controls.Actions.ReportUser" = "Report %@"; +"Common.Controls.Actions.Save" = "Save"; +"Common.Controls.Actions.SavePhoto" = "Save photo"; +"Common.Controls.Actions.SeeMore" = "See More"; +"Common.Controls.Actions.Settings" = "Settings"; +"Common.Controls.Actions.Share" = "Share"; +"Common.Controls.Actions.SharePost" = "Share post"; +"Common.Controls.Actions.ShareUser" = "Share %@"; +"Common.Controls.Actions.SignIn" = "Sign In"; +"Common.Controls.Actions.SignUp" = "Sign Up"; +"Common.Controls.Actions.Skip" = "Skip"; +"Common.Controls.Actions.TakePhoto" = "Take photo"; +"Common.Controls.Actions.TryAgain" = "Try Again"; +"Common.Controls.Actions.UnblockDomain" = "Unblock %@"; +"Common.Controls.Firendship.Block" = "Block"; +"Common.Controls.Firendship.BlockDomain" = "Block %@"; +"Common.Controls.Firendship.BlockUser" = "Block %@"; +"Common.Controls.Firendship.Blocked" = "Blocked"; +"Common.Controls.Firendship.EditInfo" = "Edit info"; +"Common.Controls.Firendship.Follow" = "Follow"; +"Common.Controls.Firendship.Following" = "Following"; +"Common.Controls.Firendship.Mute" = "Mute"; +"Common.Controls.Firendship.MuteUser" = "Mute %@"; +"Common.Controls.Firendship.Muted" = "Muted"; +"Common.Controls.Firendship.Pending" = "Pending"; +"Common.Controls.Firendship.Request" = "Request"; +"Common.Controls.Firendship.Unblock" = "Unblock"; +"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"; +"Common.Controls.Status.Poll.Closed" = "Closed"; +"Common.Controls.Status.Poll.TimeLeft" = "%@ left"; +"Common.Controls.Status.Poll.Vote" = "Vote"; +"Common.Controls.Status.Poll.VoteCount.Multiple" = "%d votes"; +"Common.Controls.Status.Poll.VoteCount.Single" = "%d vote"; +"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.ShowUserProfile" = "Show user profile"; +"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.Tabs.Home" = "Home"; +"Common.Controls.Tabs.Notification" = "Notification"; +"Common.Controls.Tabs.Profile" = "Profile"; +"Common.Controls.Tabs.Search" = "Search"; +"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 + until you unblock them. +Your account looks like this to them."; +"Common.Controls.Timeline.Header.NoStatusFound" = "No Status Found"; +"Common.Controls.Timeline.Header.SuspendedWarning" = "This account has been suspended."; +"Common.Controls.Timeline.Header.UserSuspendedWarning" = "%@'s account has been suspended."; +"Common.Controls.Timeline.Loader.LoadMissingPosts" = "Load missing posts"; +"Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Loading missing posts..."; +"Common.Controls.Timeline.Loader.ShowMoreReplies" = "Show more replies"; +"Common.Countable.Photo.Multiple" = "photos"; +"Common.Countable.Photo.Single" = "photo"; +"Scene.Compose.Accessibility.AppendAttachment" = "Append attachment"; +"Scene.Compose.Accessibility.AppendPoll" = "Append poll"; +"Scene.Compose.Accessibility.CustomEmojiPicker" = "Custom emoji picker"; +"Scene.Compose.Accessibility.DisableContentWarning" = "Disable content warning"; +"Scene.Compose.Accessibility.EnableContentWarning" = "Enable content warning"; +"Scene.Compose.Accessibility.InputLimitExceedsCount" = "Input limit exceeds %ld"; +"Scene.Compose.Accessibility.InputLimitRemainsCount" = "Input limit remains %ld"; +"Scene.Compose.Accessibility.PostVisibilityMenu" = "Post visibility menu"; +"Scene.Compose.Accessibility.RemovePoll" = "Remove poll"; +"Scene.Compose.Attachment.AttachmentBroken" = "This %@ is broken and can't be +uploaded to Mastodon."; +"Scene.Compose.Attachment.DescriptionPhoto" = "Describe photo for low vision people..."; +"Scene.Compose.Attachment.DescriptionVideo" = "Describe what’s happening for low vision people..."; +"Scene.Compose.Attachment.Photo" = "photo"; +"Scene.Compose.Attachment.Video" = "video"; +"Scene.Compose.ComposeAction" = "Publish"; +"Scene.Compose.ContentInputPlaceholder" = "Type or paste what's on your mind"; +"Scene.Compose.ContentWarning.Placeholder" = "Write an accurate warning here..."; +"Scene.Compose.MediaSelection.Browse" = "Browse"; +"Scene.Compose.MediaSelection.Camera" = "Take Photo"; +"Scene.Compose.MediaSelection.PhotoLibrary" = "Photo Library"; +"Scene.Compose.Poll.DurationTime" = "Duration: %@"; +"Scene.Compose.Poll.OneDay" = "1 Day"; +"Scene.Compose.Poll.OneHour" = "1 Hour"; +"Scene.Compose.Poll.OptionNumber" = "Option %ld"; +"Scene.Compose.Poll.SevenDays" = "7 Days"; +"Scene.Compose.Poll.SixHours" = "6 Hours"; +"Scene.Compose.Poll.ThirtyMinutes" = "30 minutes"; +"Scene.Compose.Poll.ThreeDays" = "3 Days"; +"Scene.Compose.ReplyingToUser" = "replying to %@"; +"Scene.Compose.Title.NewPost" = "New Post"; +"Scene.Compose.Title.NewReply" = "New Reply"; +"Scene.Compose.Visibility.Direct" = "Only people I mention"; +"Scene.Compose.Visibility.Private" = "Followers only"; +"Scene.Compose.Visibility.Public" = "Public"; +"Scene.Compose.Visibility.Unlisted" = "Unlisted"; +"Scene.ConfirmEmail.Button.DontReceiveEmail" = "I never got an email"; +"Scene.ConfirmEmail.Button.OpenEmailApp" = "Open Email App"; +"Scene.ConfirmEmail.DontReceiveEmail.Description" = "Check if your email address is correct as well as your junk folder if you haven’t."; +"Scene.ConfirmEmail.DontReceiveEmail.ResendEmail" = "Resend Email"; +"Scene.ConfirmEmail.DontReceiveEmail.Title" = "Check your email"; +"Scene.ConfirmEmail.OpenEmailApp.Description" = "We just sent you an email. Check your junk folder if you haven’t."; +"Scene.ConfirmEmail.OpenEmailApp.Mail" = "Mail"; +"Scene.ConfirmEmail.OpenEmailApp.OpenEmailClient" = "Open Email Client"; +"Scene.ConfirmEmail.OpenEmailApp.Title" = "Check your inbox."; +"Scene.ConfirmEmail.Subtitle" = "We just sent an email to %@, +tap the link to confirm your account."; +"Scene.ConfirmEmail.Title" = "One last thing."; +"Scene.Favorite.Title" = "Your Favorites"; +"Scene.Hashtag.Prompt" = "%@ people talking"; +"Scene.HomeTimeline.NavigationBarState.NewPosts" = "See new posts"; +"Scene.HomeTimeline.NavigationBarState.Offline" = "Offline"; +"Scene.HomeTimeline.NavigationBarState.Published" = "Published!"; +"Scene.HomeTimeline.NavigationBarState.Publishing" = "Publishing post..."; +"Scene.HomeTimeline.Title" = "Home"; +"Scene.Notification.Action.Favourite" = "favorited your post"; +"Scene.Notification.Action.Follow" = "followed you"; +"Scene.Notification.Action.FollowRequest" = "request to follow you"; +"Scene.Notification.Action.Mention" = "mentioned you"; +"Scene.Notification.Action.Poll" = "Your poll has ended"; +"Scene.Notification.Action.Reblog" = "rebloged your post"; +"Scene.Notification.Title.Everything" = "Everything"; +"Scene.Notification.Title.Mentions" = "Mentions"; +"Scene.Profile.Dashboard.Accessibility.CountFollowers" = "%ld followers"; +"Scene.Profile.Dashboard.Accessibility.CountFollowing" = "%ld following"; +"Scene.Profile.Dashboard.Accessibility.CountPosts" = "%ld posts"; +"Scene.Profile.Dashboard.Followers" = "followers"; +"Scene.Profile.Dashboard.Following" = "following"; +"Scene.Profile.Dashboard.Posts" = "posts"; +"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.Message" = "Confirm unblock %@"; +"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.Title" = "Unblock Account"; +"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Confirm unmute %@"; +"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Title" = "Unmute Account"; +"Scene.Profile.SegmentedControl.Media" = "Media"; +"Scene.Profile.SegmentedControl.Posts" = "Posts"; +"Scene.Profile.SegmentedControl.Replies" = "Replies"; +"Scene.Profile.Subtitle" = "%@ posts"; +"Scene.PublicTimeline.Title" = "Public"; +"Scene.Register.Error.Item.Agreement" = "Agreement"; +"Scene.Register.Error.Item.Email" = "Email"; +"Scene.Register.Error.Item.Locale" = "Locale"; +"Scene.Register.Error.Item.Password" = "Password"; +"Scene.Register.Error.Item.Reason" = "Reason"; +"Scene.Register.Error.Item.Username" = "Username"; +"Scene.Register.Error.Reason.Accepted" = "%@ must be accepted"; +"Scene.Register.Error.Reason.Blank" = "%@ is required"; +"Scene.Register.Error.Reason.Blocked" = "%@ contains a disallowed e-mail provider"; +"Scene.Register.Error.Reason.Inclusion" = "%@ is not a supported value"; +"Scene.Register.Error.Reason.Invalid" = "%@ is invalid"; +"Scene.Register.Error.Reason.Reserved" = "%@ is a reserved keyword"; +"Scene.Register.Error.Reason.Taken" = "%@ is already in use"; +"Scene.Register.Error.Reason.TooLong" = "%@ is too long"; +"Scene.Register.Error.Reason.TooShort" = "%@ is too short"; +"Scene.Register.Error.Reason.Unreachable" = "%@ does not seem to exist"; +"Scene.Register.Error.Special.EmailInvalid" = "This is not a valid e-mail address"; +"Scene.Register.Error.Special.PasswordTooShort" = "Password is too short (must be at least 8 characters)"; +"Scene.Register.Error.Special.UsernameInvalid" = "Username must only contain alphanumeric characters and underscores"; +"Scene.Register.Error.Special.UsernameTooLong" = "Username is too long (can't be longer than 30 characters)"; +"Scene.Register.Input.Avatar.Delete" = "Delete"; +"Scene.Register.Input.DisplayName.Placeholder" = "display name"; +"Scene.Register.Input.Email.Placeholder" = "email"; +"Scene.Register.Input.Invite.RegistrationUserInviteRequest" = "Why do you want to join?"; +"Scene.Register.Input.Password.Hint" = "Your password needs at least eight characters"; +"Scene.Register.Input.Password.Placeholder" = "password"; +"Scene.Register.Input.Username.DuplicatePrompt" = "This username is taken."; +"Scene.Register.Input.Username.Placeholder" = "username"; +"Scene.Register.Title" = "Tell us about you."; +"Scene.Report.Content1" = "Are there any other posts you’d like to add to the report?"; +"Scene.Report.Content2" = "Is there anything the moderators should know about this report?"; +"Scene.Report.Send" = "Send Report"; +"Scene.Report.SkipToSend" = "Send without comment"; +"Scene.Report.Step1" = "Step 1 of 2"; +"Scene.Report.Step2" = "Step 2 of 2"; +"Scene.Report.TextPlaceholder" = "Type or paste additional comments"; +"Scene.Report.Title" = "Report %@"; +"Scene.Search.Recommend.Accounts.Description" = "Except for Sam, you will not like his account."; +"Scene.Search.Recommend.Accounts.Follow" = "Follow"; +"Scene.Search.Recommend.Accounts.Title" = "Accounts you might like"; +"Scene.Search.Recommend.ButtonText" = "See All"; +"Scene.Search.Recommend.HashTag.Description" = "Hashtags that are getting quite a bit of attention among people you follow"; +"Scene.Search.Recommend.HashTag.PeopleTalking" = "%@ people are talking"; +"Scene.Search.Recommend.HashTag.Title" = "Trending in your timeline"; +"Scene.Search.Searchbar.Cancel" = "Cancel"; +"Scene.Search.Searchbar.Placeholder" = "Search hashtags and users"; +"Scene.Search.Searching.Clear" = "clear"; +"Scene.Search.Searching.RecentSearch" = "Recent searches"; +"Scene.Search.Searching.Segment.All" = "All"; +"Scene.Search.Searching.Segment.Hashtags" = "Hashtags"; +"Scene.Search.Searching.Segment.People" = "People"; +"Scene.ServerPicker.Button.Category.Academia" = "academia"; +"Scene.ServerPicker.Button.Category.Activism" = "activism"; +"Scene.ServerPicker.Button.Category.All" = "All"; +"Scene.ServerPicker.Button.Category.AllAccessiblityDescription" = "Category: All"; +"Scene.ServerPicker.Button.Category.Art" = "art"; +"Scene.ServerPicker.Button.Category.Food" = "food"; +"Scene.ServerPicker.Button.Category.Furry" = "furry"; +"Scene.ServerPicker.Button.Category.Games" = "games"; +"Scene.ServerPicker.Button.Category.General" = "general"; +"Scene.ServerPicker.Button.Category.Journalism" = "journalism"; +"Scene.ServerPicker.Button.Category.Lgbt" = "lgbt"; +"Scene.ServerPicker.Button.Category.Music" = "music"; +"Scene.ServerPicker.Button.Category.Regional" = "regional"; +"Scene.ServerPicker.Button.Category.Tech" = "tech"; +"Scene.ServerPicker.Button.SeeLess" = "See Less"; +"Scene.ServerPicker.Button.SeeMore" = "See More"; +"Scene.ServerPicker.EmptyState.BadNetwork" = "Something went wrong while loading data. Check your internet connection."; +"Scene.ServerPicker.EmptyState.FindingServers" = "Finding available servers..."; +"Scene.ServerPicker.Input.Placeholder" = "Find a server or join your own..."; +"Scene.ServerPicker.Label.Category" = "CATEGORY"; +"Scene.ServerPicker.Label.Language" = "LANGUAGE"; +"Scene.ServerPicker.Label.Users" = "USERS"; +"Scene.ServerPicker.Title" = "Pick a Server, +any server."; +"Scene.ServerRules.Button.Confirm" = "I Agree"; +"Scene.ServerRules.PrivacyPolicy" = "privacy policy"; +"Scene.ServerRules.Prompt" = "By continuing, you're subject to the terms of service and privacy policy for %@."; +"Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@."; +"Scene.ServerRules.TermsOfService" = "terms of service"; +"Scene.ServerRules.Title" = "Some ground rules."; +"Scene.Settings.Section.Appearance.Automatic" = "Automatic"; +"Scene.Settings.Section.Appearance.Dark" = "Always Dark"; +"Scene.Settings.Section.Appearance.Light" = "Always Light"; +"Scene.Settings.Section.Appearance.Title" = "Appearance"; +"Scene.Settings.Section.Boringzone.Privacy" = "Privacy Policy"; +"Scene.Settings.Section.Boringzone.Terms" = "Terms of Service"; +"Scene.Settings.Section.Boringzone.Title" = "The Boring zone"; +"Scene.Settings.Section.Notifications.Boosts" = "Reblogs my post"; +"Scene.Settings.Section.Notifications.Favorites" = "Favorites my post"; +"Scene.Settings.Section.Notifications.Follows" = "Follows me"; +"Scene.Settings.Section.Notifications.Mentions" = "Mentions me"; +"Scene.Settings.Section.Notifications.Title" = "Notifications"; +"Scene.Settings.Section.Notifications.Trigger.Anyone" = "anyone"; +"Scene.Settings.Section.Notifications.Trigger.Follow" = "anyone I follow"; +"Scene.Settings.Section.Notifications.Trigger.Follower" = "a follower"; +"Scene.Settings.Section.Notifications.Trigger.Noone" = "no one"; +"Scene.Settings.Section.Notifications.Trigger.Title" = "Notify me when"; +"Scene.Settings.Section.Spicyzone.Clear" = "Clear Media Cache"; +"Scene.Settings.Section.Spicyzone.Signout" = "Sign Out"; +"Scene.Settings.Section.Spicyzone.Title" = "The spicy zone"; +"Scene.Settings.Title" = "Settings"; +"Scene.SuggestionAccount.FollowExplain" = "When you follow someone, you’ll see their posts in your home feed."; +"Scene.SuggestionAccount.Title" = "Find People to Follow"; +"Scene.Thread.BackTitle" = "Post"; +"Scene.Thread.Favorite.Multiple" = "%@ favorites"; +"Scene.Thread.Favorite.Single" = "%@ favorite"; +"Scene.Thread.Reblog.Multiple" = "%@ reblogs"; +"Scene.Thread.Reblog.Single" = "%@ reblog"; +"Scene.Thread.Title" = "Post from %@"; +"Scene.Welcome.Slogan" = "Social networking +back in your hands."; \ No newline at end of file From eb86247419aeb3f110d274ba8ed6dea3f9fb024c Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 13 May 2021 15:57:29 +0800 Subject: [PATCH 16/23] fix: avatar placeholder image corner set with scale issue --- Mastodon/Protocol/AvatarConfigurableView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mastodon/Protocol/AvatarConfigurableView.swift b/Mastodon/Protocol/AvatarConfigurableView.swift index e33c01278..40ef91153 100644 --- a/Mastodon/Protocol/AvatarConfigurableView.swift +++ b/Mastodon/Protocol/AvatarConfigurableView.swift @@ -26,7 +26,7 @@ extension AvatarConfigurableView { if Self.configurableAvatarImageCornerRadius < Self.configurableAvatarImageSize.width * 0.5 { return placeholderImage .af.imageAspectScaled(toFill: Self.configurableAvatarImageSize) - .af.imageRounded(withCornerRadius: Self.configurableAvatarImageCornerRadius, divideRadiusByImageScale: true) + .af.imageRounded(withCornerRadius: Self.configurableAvatarImageCornerRadius, divideRadiusByImageScale: false) } else { return placeholderImage.af.imageRoundedIntoCircle() } From 93c9289b1f8e25e998658e833dd3bf8db8ba9b9e Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 13 May 2021 16:02:00 +0800 Subject: [PATCH 17/23] fix: player content overlay layout issue. resolve #132 --- .../View/Container/PlayerContainerView.swift | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift b/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift index d018dfbe5..32ee48df9 100644 --- a/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift +++ b/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift @@ -75,16 +75,6 @@ extension PlayerContainerView { mediaTypeIndicotorView.widthAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.width).priority(.required - 1), ]) - contentWarningOverlayView.translatesAutoresizingMaskIntoConstraints = false - addSubview(contentWarningOverlayView) - NSLayoutConstraint.activate([ - contentWarningOverlayView.topAnchor.constraint(equalTo: topAnchor), - contentWarningOverlayView.leadingAnchor.constraint(equalTo: leadingAnchor), - contentWarningOverlayView.trailingAnchor.constraint(equalTo: trailingAnchor), - contentWarningOverlayView.bottomAnchor.constraint(equalTo: bottomAnchor) - ]) - contentWarningOverlayView.delegate = self - mediaTypeIndicotorViewInContentWarningOverlay.translatesAutoresizingMaskIntoConstraints = false contentWarningOverlayView.addSubview(mediaTypeIndicotorViewInContentWarningOverlay) NSLayoutConstraint.activate([ @@ -93,6 +83,8 @@ extension PlayerContainerView { mediaTypeIndicotorViewInContentWarningOverlay.heightAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.height).priority(.required - 1), mediaTypeIndicotorViewInContentWarningOverlay.widthAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.width).priority(.required - 1), ]) + + contentWarningOverlayView.delegate = self } } @@ -147,6 +139,16 @@ extension PlayerContainerView { containerHeightLayoutConstraint.constant = floor(rect.height) containerHeightLayoutConstraint.isActive = true + contentWarningOverlayView.removeFromSuperview() + contentWarningOverlayView.translatesAutoresizingMaskIntoConstraints = false + addSubview(contentWarningOverlayView) + NSLayoutConstraint.activate([ + contentWarningOverlayView.topAnchor.constraint(equalTo: touchBlockingView.topAnchor), + contentWarningOverlayView.leadingAnchor.constraint(equalTo: touchBlockingView.leadingAnchor), + contentWarningOverlayView.trailingAnchor.constraint(equalTo: touchBlockingView.trailingAnchor), + contentWarningOverlayView.bottomAnchor.constraint(equalTo: touchBlockingView.bottomAnchor) + ]) + bringSubviewToFront(mediaTypeIndicotorView) return playerViewController From d8ce63df80f001522ed1798870822739ff84c0af Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 13 May 2021 16:06:28 +0800 Subject: [PATCH 18/23] chore: update i18n. resolve #94 --- Localization/app.json | 2 +- Mastodon/Generated/Strings.swift | 2 +- Mastodon/Resources/ar.lproj/Localizable.strings | 2 +- Mastodon/Resources/en.lproj/Localizable.strings | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index 327c72625..ad35fed5f 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -393,7 +393,7 @@ }, "accounts": { "title": "Accounts you might like", - "description": "Except for Sam, you will not like his account.", + "description": "You may like to follow these accounts", "follow": "Follow" } }, diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index de69f016e..d179be707 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -690,7 +690,7 @@ internal enum L10n { /// See All internal static let buttonText = L10n.tr("Localizable", "Scene.Search.Recommend.ButtonText") internal enum Accounts { - /// Except for Sam, you will not like his account. + /// You may like to follow these accounts internal static let description = L10n.tr("Localizable", "Scene.Search.Recommend.Accounts.Description") /// Follow internal static let follow = L10n.tr("Localizable", "Scene.Search.Recommend.Accounts.Follow") diff --git a/Mastodon/Resources/ar.lproj/Localizable.strings b/Mastodon/Resources/ar.lproj/Localizable.strings index 0549c4a3b..7b9e0e43c 100644 --- a/Mastodon/Resources/ar.lproj/Localizable.strings +++ b/Mastodon/Resources/ar.lproj/Localizable.strings @@ -225,7 +225,7 @@ tap the link to confirm your account."; "Scene.Report.Step2" = "Step 2 of 2"; "Scene.Report.TextPlaceholder" = "Type or paste additional comments"; "Scene.Report.Title" = "Report %@"; -"Scene.Search.Recommend.Accounts.Description" = "Except for Sam, you will not like his account."; +"Scene.Search.Recommend.Accounts.Description" = "You may like to follow these accounts"; "Scene.Search.Recommend.Accounts.Follow" = "Follow"; "Scene.Search.Recommend.Accounts.Title" = "Accounts you might like"; "Scene.Search.Recommend.ButtonText" = "See All"; diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 0549c4a3b..7b9e0e43c 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -225,7 +225,7 @@ tap the link to confirm your account."; "Scene.Report.Step2" = "Step 2 of 2"; "Scene.Report.TextPlaceholder" = "Type or paste additional comments"; "Scene.Report.Title" = "Report %@"; -"Scene.Search.Recommend.Accounts.Description" = "Except for Sam, you will not like his account."; +"Scene.Search.Recommend.Accounts.Description" = "You may like to follow these accounts"; "Scene.Search.Recommend.Accounts.Follow" = "Follow"; "Scene.Search.Recommend.Accounts.Title" = "Accounts you might like"; "Scene.Search.Recommend.ButtonText" = "See All"; From 5e0eb598d1d2aec3427c347d63fc0597ca517179 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 13 May 2021 16:21:06 +0800 Subject: [PATCH 19/23] fix: separator line display issue. resolve #123 --- .../NotificationViewController.swift | 6 +- .../NotificationStatusTableViewCell.swift | 54 ++++++++++++++++++ .../NotificationTableViewCell.swift | 55 +++++++++++++++++++ 3 files changed, 112 insertions(+), 3 deletions(-) diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index 73e385ef8..aea9d3318 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -28,14 +28,14 @@ final class NotificationViewController: UIViewController, NeedsDependency { let tableView: UITableView = { let tableView = ControlContainableTableView() - tableView.rowHeight = UITableView.automaticDimension - tableView.separatorStyle = .singleLine tableView.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) tableView.register(NotificationTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationTableViewCell.self)) tableView.register(NotificationStatusTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationStatusTableViewCell.self)) tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) - tableView.tableFooterView = UIView() + tableView.rowHeight = UITableView.automaticDimension tableView.estimatedRowHeight = UITableView.automaticDimension + tableView.separatorStyle = .none + tableView.tableFooterView = UIView() tableView.backgroundColor = .clear return tableView }() diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift index 5ae5d0136..a950ede46 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift @@ -76,6 +76,14 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell { let statusView = StatusView() + let separatorLine = UIView.separatorLine + + var separatorLineToEdgeLeadingLayoutConstraint: NSLayoutConstraint! + var separatorLineToEdgeTrailingLayoutConstraint: NSLayoutConstraint! + + var separatorLineToMarginLeadingLayoutConstraint: NSLayoutConstraint! + var separatorLineToMarginTrailingLayoutConstraint: NSLayoutConstraint! + override func prepareForReuse() { super.prepareForReuse() avatatImageView.af.cancelImageRequest() @@ -197,6 +205,18 @@ extension NotificationStatusTableViewCell { containerStackView.addArrangedSubview(statusStackView) + separatorLine.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(separatorLine) + separatorLineToEdgeLeadingLayoutConstraint = separatorLine.leadingAnchor.constraint(equalTo: contentView.leadingAnchor) + separatorLineToEdgeTrailingLayoutConstraint = separatorLine.trailingAnchor.constraint(equalTo: contentView.trailingAnchor) + separatorLineToMarginLeadingLayoutConstraint = separatorLine.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor) + separatorLineToMarginTrailingLayoutConstraint = separatorLine.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor) + NSLayoutConstraint.activate([ + separatorLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)), + ]) + resetSeparatorLineLayout() + // remove item don't display statusView.actionToolbarContainer.removeFromStackView() // it affect stackView's height,need remove @@ -206,6 +226,8 @@ extension NotificationStatusTableViewCell { override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) + + resetSeparatorLineLayout() statusBorder.layer.borderColor = Asset.Colors.Border.notification.color.cgColor actionImageBackground.layer.borderColor = Asset.Colors.Background.systemBackground.color.cgColor } @@ -258,5 +280,37 @@ extension NotificationStatusTableViewCell: StatusViewDelegate { // do nothing } +} + +extension NotificationStatusTableViewCell { + + private func resetSeparatorLineLayout() { + separatorLineToEdgeLeadingLayoutConstraint.isActive = false + separatorLineToEdgeTrailingLayoutConstraint.isActive = false + separatorLineToMarginLeadingLayoutConstraint.isActive = false + separatorLineToMarginTrailingLayoutConstraint.isActive = false + + if traitCollection.userInterfaceIdiom == .phone { + // to edge + NSLayoutConstraint.activate([ + separatorLineToEdgeLeadingLayoutConstraint, + separatorLineToEdgeTrailingLayoutConstraint, + ]) + } else { + if traitCollection.horizontalSizeClass == .compact { + // to edge + NSLayoutConstraint.activate([ + separatorLineToEdgeLeadingLayoutConstraint, + separatorLineToEdgeTrailingLayoutConstraint, + ]) + } else { + // to margin + NSLayoutConstraint.activate([ + separatorLineToMarginLeadingLayoutConstraint, + separatorLineToMarginTrailingLayoutConstraint, + ]) + } + } + } } diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift index 49d32530b..067283935 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift @@ -98,6 +98,14 @@ final class NotificationTableViewCell: UITableViewCell { let buttonStackView = UIStackView() + let separatorLine = UIView.separatorLine + + var separatorLineToEdgeLeadingLayoutConstraint: NSLayoutConstraint! + var separatorLineToEdgeTrailingLayoutConstraint: NSLayoutConstraint! + + var separatorLineToMarginLeadingLayoutConstraint: NSLayoutConstraint! + var separatorLineToMarginTrailingLayoutConstraint: NSLayoutConstraint! + override func prepareForReuse() { super.prepareForReuse() avatatImageView.af.cancelImageRequest() @@ -187,10 +195,57 @@ extension NotificationTableViewCell { buttonStackView.addArrangedSubview(acceptButton) buttonStackView.addArrangedSubview(rejectButton) containerStackView.addArrangedSubview(buttonStackView) + + separatorLine.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(separatorLine) + separatorLineToEdgeLeadingLayoutConstraint = separatorLine.leadingAnchor.constraint(equalTo: contentView.leadingAnchor) + separatorLineToEdgeTrailingLayoutConstraint = separatorLine.trailingAnchor.constraint(equalTo: contentView.trailingAnchor) + separatorLineToMarginLeadingLayoutConstraint = separatorLine.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor) + separatorLineToMarginTrailingLayoutConstraint = separatorLine.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor) + NSLayoutConstraint.activate([ + separatorLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)), + ]) + resetSeparatorLineLayout() } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) + actionImageBackground.layer.borderColor = Asset.Colors.Background.systemBackground.color.cgColor + resetSeparatorLineLayout() } } + +extension NotificationTableViewCell { + + private func resetSeparatorLineLayout() { + separatorLineToEdgeLeadingLayoutConstraint.isActive = false + separatorLineToEdgeTrailingLayoutConstraint.isActive = false + separatorLineToMarginLeadingLayoutConstraint.isActive = false + separatorLineToMarginTrailingLayoutConstraint.isActive = false + + if traitCollection.userInterfaceIdiom == .phone { + // to edge + NSLayoutConstraint.activate([ + separatorLineToEdgeLeadingLayoutConstraint, + separatorLineToEdgeTrailingLayoutConstraint, + ]) + } else { + if traitCollection.horizontalSizeClass == .compact { + // to edge + NSLayoutConstraint.activate([ + separatorLineToEdgeLeadingLayoutConstraint, + separatorLineToEdgeTrailingLayoutConstraint, + ]) + } else { + // to margin + NSLayoutConstraint.activate([ + separatorLineToMarginLeadingLayoutConstraint, + separatorLineToMarginTrailingLayoutConstraint, + ]) + } + } + } + +} From d31061890884526350ad4ca33deb5d6c460c5ae2 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 13 May 2021 16:28:02 +0800 Subject: [PATCH 20/23] fix: content warning overlay clip author view issue --- Mastodon/Scene/Share/View/Content/StatusView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index 46930f33d..1aea1bcc3 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -360,7 +360,7 @@ extension StatusView { // only layout to top and left & right then draw image to fit size ]) // avoid overlay clip author view - containerStackView.bringSubviewToFront(authorContainerStackView) + containerStackView.bringSubviewToFront(authorContainerView) // status statusContainerStackView.addArrangedSubview(activeTextLabel) From 46baa59d3705c0303e7cc06df9b09bb3ac9fd333 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 13 May 2021 17:50:37 +0800 Subject: [PATCH 21/23] feat: add picker server loader. Set chevron image for expand button --- Localization/app.json | 3 +- Mastodon.xcodeproj/project.pbxproj | 6 +- Mastodon/Diffiable/Item/PickServerItem.swift | 25 ++++++ .../Diffiable/Section/PickServerSection.swift | 24 ++++++ Mastodon/Generated/Strings.swift | 2 + .../Resources/ar.lproj/Localizable.strings | 1 + .../Resources/en.lproj/Localizable.strings | 1 + .../MastodonPickServerViewController.swift | 1 + .../MastodonPickServerViewModel.swift | 45 +++++++--- .../TableViewCell/PickServerCell.swift | 11 ++- .../PickServerLoaderTableViewCell.swift | 86 +++++++++++++++++++ .../TimelineLoaderTableViewCell.swift | 7 +- .../Webview/WebViewController.swift | 0 .../{ => Share}/Webview/WebViewModel.swift | 0 14 files changed, 192 insertions(+), 20 deletions(-) create mode 100644 Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerLoaderTableViewCell.swift rename Mastodon/Scene/{ => Share}/Webview/WebViewController.swift (100%) rename Mastodon/Scene/{ => Share}/Webview/WebViewModel.swift (100%) diff --git a/Localization/app.json b/Localization/app.json index ad35fed5f..21304d564 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -199,7 +199,8 @@ }, "empty_state": { "finding_servers": "Finding available servers...", - "bad_network": "Something went wrong while loading data. Check your internet connection." + "bad_network": "Something went wrong while loading data. Check your internet connection.", + "no_results": "No results" } }, "register": { diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index b274f5c7c..0eb170fc5 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -191,6 +191,7 @@ DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */; }; DB084B5725CBC56C00F898ED /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB084B5625CBC56C00F898ED /* Status.swift */; }; DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */; }; + DB0F8150264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0F814F264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift */; }; DB118A8225E4B6E600FAB162 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */; }; DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */; }; DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D186B25EF5BA7003F1F23 /* PollTableView.swift */; }; @@ -758,6 +759,7 @@ DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = ""; }; DB0F814D264CFFD300F2A12B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; }; DB0F814E264CFFD300F2A12B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = ""; }; + DB0F814F264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerLoaderTableViewCell.swift; sourceTree = ""; }; DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDimmableButton.swift; sourceTree = ""; }; DB1D186B25EF5BA7003F1F23 /* PollTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollTableView.swift; sourceTree = ""; }; @@ -1136,6 +1138,7 @@ 0FB3D30725E524C600AAD544 /* PickServerCategoriesCell.swift */, 0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */, 0FB3D33725E6401400AAD544 /* PickServerCell.swift */, + DB0F814F264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift */, ); path = TableViewCell; sourceTree = ""; @@ -1390,6 +1393,7 @@ 2D7631A425C1532200929FB9 /* Share */ = { isa = PBXGroup; children = ( + 5D03938E2612D200007FE196 /* Webview */, DB68A04F25E9028800CFDF14 /* NavigationController */, DB9D6C2025E502C60051B173 /* ViewModel */, 2D7631A525C1532D00929FB9 /* View */, @@ -2047,7 +2051,6 @@ DB8AF55525C1379F002E6C99 /* Scene */ = { isa = PBXGroup; children = ( - 5D03938E2612D200007FE196 /* Webview */, 2D7631A425C1532200929FB9 /* Share */, DB6180E426391A500018D199 /* Transition */, DB8AF54E25C13703002E6C99 /* MainTab */, @@ -2983,6 +2986,7 @@ DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */, DBE3CE0D261D767100430CC6 /* FavoriteViewController+Provider.swift in Sources */, 2D084B9326259545003AA3AF /* NotificationViewModel+LoadLatestState.swift in Sources */, + DB0F8150264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift in Sources */, DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */, DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */, DBB9759C262462E1004620BD /* ThreadMetaView.swift in Sources */, diff --git a/Mastodon/Diffiable/Item/PickServerItem.swift b/Mastodon/Diffiable/Item/PickServerItem.swift index 13acefeae..1ae38ba1c 100644 --- a/Mastodon/Diffiable/Item/PickServerItem.swift +++ b/Mastodon/Diffiable/Item/PickServerItem.swift @@ -14,6 +14,7 @@ enum PickServerItem { case categoryPicker(items: [CategoryPickerItem]) case search case server(server: Mastodon.Entity.Server, attribute: ServerItemAttribute) + case loader(attribute: LoaderItemAttribute) } extension PickServerItem { @@ -34,6 +35,26 @@ extension PickServerItem { hasher.combine(isExpand) } } + + final class LoaderItemAttribute: Equatable, Hashable { + let id = UUID() + + var isLast: Bool + var isNoResult: Bool + + init(isLast: Bool, isEmptyResult: Bool) { + self.isLast = isLast + self.isNoResult = isEmptyResult + } + + static func == (lhs: PickServerItem.LoaderItemAttribute, rhs: PickServerItem.LoaderItemAttribute) -> Bool { + return lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + } } extension PickServerItem: Equatable { @@ -47,6 +68,8 @@ extension PickServerItem: Equatable { return true case (.server(let serverLeft, _), .server(let serverRight, _)): return serverLeft.domain == serverRight.domain + case (.loader(let attributeLeft), loader(let attributeRight)): + return attributeLeft == attributeRight default: return false } @@ -64,6 +87,8 @@ extension PickServerItem: Hashable { hasher.combine(String(describing: PickServerItem.search.self)) case .server(let server, _): hasher.combine(server.domain) + case .loader(let attribute): + hasher.combine(attribute) } } } diff --git a/Mastodon/Diffiable/Section/PickServerSection.swift b/Mastodon/Diffiable/Section/PickServerSection.swift index f5b1ee500..aaafb8ce7 100644 --- a/Mastodon/Diffiable/Section/PickServerSection.swift +++ b/Mastodon/Diffiable/Section/PickServerSection.swift @@ -57,6 +57,10 @@ extension PickServerSection { PickServerSection.configure(cell: cell, server: server, attribute: attribute) cell.delegate = pickServerCellDelegate return cell + case .loader(let attribute): + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerLoaderTableViewCell.self), for: indexPath) as! PickServerLoaderTableViewCell + PickServerSection.configure(cell: cell, attribute: attribute) + return cell } } } @@ -137,3 +141,23 @@ extension PickServerSection { } } + +extension PickServerSection { + + static func configure(cell: PickServerLoaderTableViewCell, attribute: PickServerItem.LoaderItemAttribute) { + if attribute.isLast { + cell.containerView.layer.maskedCorners = [ + .layerMinXMaxYCorner, + .layerMaxXMaxYCorner + ] + cell.containerView.layer.cornerCurve = .continuous + cell.containerView.layer.cornerRadius = MastodonPickServerAppearance.tableViewCornerRadius + } else { + cell.containerView.layer.cornerRadius = 0 + } + + attribute.isNoResult ? cell.stopAnimating() : cell.startAnimating() + cell.emptyStatusLabel.isHidden = !attribute.isNoResult + } + +} diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index d179be707..8f6c13f9e 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -773,6 +773,8 @@ internal enum L10n { internal static let badNetwork = L10n.tr("Localizable", "Scene.ServerPicker.EmptyState.BadNetwork") /// Finding available servers... internal static let findingServers = L10n.tr("Localizable", "Scene.ServerPicker.EmptyState.FindingServers") + /// No results + internal static let noResults = L10n.tr("Localizable", "Scene.ServerPicker.EmptyState.NoResults") } internal enum Input { /// Find a server or join your own... diff --git a/Mastodon/Resources/ar.lproj/Localizable.strings b/Mastodon/Resources/ar.lproj/Localizable.strings index 7b9e0e43c..c9ed556c3 100644 --- a/Mastodon/Resources/ar.lproj/Localizable.strings +++ b/Mastodon/Resources/ar.lproj/Localizable.strings @@ -257,6 +257,7 @@ tap the link to confirm your account."; "Scene.ServerPicker.Button.SeeMore" = "See More"; "Scene.ServerPicker.EmptyState.BadNetwork" = "Something went wrong while loading data. Check your internet connection."; "Scene.ServerPicker.EmptyState.FindingServers" = "Finding available servers..."; +"Scene.ServerPicker.EmptyState.NoResults" = "No results"; "Scene.ServerPicker.Input.Placeholder" = "Find a server or join your own..."; "Scene.ServerPicker.Label.Category" = "CATEGORY"; "Scene.ServerPicker.Label.Language" = "LANGUAGE"; diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 7b9e0e43c..c9ed556c3 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -257,6 +257,7 @@ tap the link to confirm your account."; "Scene.ServerPicker.Button.SeeMore" = "See More"; "Scene.ServerPicker.EmptyState.BadNetwork" = "Something went wrong while loading data. Check your internet connection."; "Scene.ServerPicker.EmptyState.FindingServers" = "Finding available servers..."; +"Scene.ServerPicker.EmptyState.NoResults" = "No results"; "Scene.ServerPicker.Input.Placeholder" = "Find a server or join your own..."; "Scene.ServerPicker.Label.Category" = "CATEGORY"; "Scene.ServerPicker.Label.Language" = "LANGUAGE"; diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift index 638734c11..71e74d56b 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift @@ -31,6 +31,7 @@ final class MastodonPickServerViewController: UIViewController, NeedsDependency tableView.register(PickServerCategoriesCell.self, forCellReuseIdentifier: String(describing: PickServerCategoriesCell.self)) tableView.register(PickServerSearchCell.self, forCellReuseIdentifier: String(describing: PickServerSearchCell.self)) tableView.register(PickServerCell.self, forCellReuseIdentifier: String(describing: PickServerCell.self)) + tableView.register(PickServerLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: PickServerLoaderTableViewCell.self)) tableView.rowHeight = UITableView.automaticDimension tableView.separatorStyle = .none tableView.backgroundColor = .clear diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift index ed804afd9..0edc0a350 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift @@ -39,7 +39,7 @@ class MastodonPickServerViewModel: NSObject { let selectCategoryItem = CurrentValueSubject(.all) let searchText = CurrentValueSubject("") let indexedServers = CurrentValueSubject<[Mastodon.Entity.Server], Never>([]) - let unindexedServers = CurrentValueSubject<[Mastodon.Entity.Server], Never>([]) + let unindexedServers = CurrentValueSubject<[Mastodon.Entity.Server]?, Never>([]) // set nil when loading let viewWillAppear = PassthroughSubject() // output @@ -85,8 +85,8 @@ extension MastodonPickServerViewModel { private func configure() { Publishers.CombineLatest( - filteredIndexedServers.eraseToAnyPublisher(), - unindexedServers.eraseToAnyPublisher() + filteredIndexedServers, + unindexedServers ) .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak self] indexedServers, unindexedServers in @@ -114,16 +114,31 @@ extension MastodonPickServerViewModel { guard !serverItems.contains(item) else { continue } serverItems.append(item) } - for server in unindexedServers { - let attribute = oldSnapshotServerItemAttributeDict[server.domain] ?? PickServerItem.ServerItemAttribute(isLast: false, isExpand: false) - attribute.isLast = false - let item = PickServerItem.server(server: server, attribute: attribute) - guard !serverItems.contains(item) else { continue } - serverItems.append(item) + + if let unindexedServers = unindexedServers { + if !unindexedServers.isEmpty { + for server in unindexedServers { + let attribute = oldSnapshotServerItemAttributeDict[server.domain] ?? PickServerItem.ServerItemAttribute(isLast: false, isExpand: false) + attribute.isLast = false + let item = PickServerItem.server(server: server, attribute: attribute) + guard !serverItems.contains(item) else { continue } + serverItems.append(item) + } + } else { + if indexedServers.isEmpty && !self.isLoadingIndexedServers.value { + serverItems.append(.loader(attribute: PickServerItem.LoaderItemAttribute(isLast: false, isEmptyResult: true))) + } + } + } else { + serverItems.append(.loader(attribute: PickServerItem.LoaderItemAttribute(isLast: false, isEmptyResult: false))) } + if case let .server(_, attribute) = serverItems.last { attribute.isLast = true } + if case let .loader(attribute) = serverItems.last { + attribute.isLast = true + } snapshot.appendItems(serverItems, toSection: .servers) diffableDataSource.defaultRowAnimation = .fade @@ -168,6 +183,7 @@ extension MastodonPickServerViewModel { guard let domain = AuthenticationViewModel.parseDomain(from: searchText) else { return Just(Result.failure(APIService.APIError.implicit(.badRequest))).eraseToAnyPublisher() } + self.unindexedServers.value = nil return self.context.apiService.instance(domain: domain) .map { response -> Result, Error>in let newResponse = response.map { [Mastodon.Entity.Server(instance: $0)] } @@ -184,9 +200,14 @@ extension MastodonPickServerViewModel { switch result { case .success(let response): self.unindexedServers.send(response.value) - case .failure: - // TODO: What should be presented when user inputs invalid search text? - self.unindexedServers.send([]) + case .failure(let error): + if let error = error as? APIService.APIError, + case let .implicit(reason) = error, + case .badRequest = reason { + self.unindexedServers.send([]) + } else { + self.unindexedServers.send(nil) + } } }) .store(in: &disposeBag) diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift index a93dcfebf..8eb0cb771 100644 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift @@ -88,11 +88,14 @@ class PickServerCell: UITableViewCell { let expandButton: UIButton = { let button = UIButton(type: .custom) + button.setImage(UIImage(systemName: "chevron.down", withConfiguration: UIImage.SymbolConfiguration(pointSize: 13)), for: .normal) button.setTitle(L10n.Scene.ServerPicker.Button.seeMore, for: .normal) - button.setTitle(L10n.Scene.ServerPicker.Button.seeLess, for: .selected) button.setTitleColor(Asset.Colors.brandBlue.color, for: .normal) - button.titleLabel?.font = .preferredFont(forTextStyle: .footnote) + button.titleLabel?.font = .systemFont(ofSize: 13, weight: .regular) button.translatesAutoresizingMaskIntoConstraints = false + button.imageView?.transform = CGAffineTransform(scaleX: -1, y: 1) + button.titleLabel?.transform = CGAffineTransform(scaleX: -1, y: 1) + button.transform = CGAffineTransform(scaleX: -1, y: 1) return button }() @@ -325,11 +328,15 @@ extension PickServerCell { func updateExpandMode(mode: ExpandMode) { switch mode { case .collapse: + expandButton.setImage(UIImage(systemName: "chevron.down", withConfiguration: UIImage.SymbolConfiguration(pointSize: 13)), for: .normal) + expandButton.setTitle(L10n.Scene.ServerPicker.Button.seeMore, for: .normal) expandBox.isHidden = true expandButton.isSelected = false NSLayoutConstraint.deactivate(expandConstraints) NSLayoutConstraint.activate(collapseConstraints) case .expand: + expandButton.setImage(UIImage(systemName: "chevron.up", withConfiguration: UIImage.SymbolConfiguration(pointSize: 13)), for: .normal) + expandButton.setTitle(L10n.Scene.ServerPicker.Button.seeLess, for: .normal) expandBox.isHidden = false expandButton.isSelected = true NSLayoutConstraint.activate(expandConstraints) diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerLoaderTableViewCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerLoaderTableViewCell.swift new file mode 100644 index 000000000..37135fa9b --- /dev/null +++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerLoaderTableViewCell.swift @@ -0,0 +1,86 @@ +// +// PickServerLoaderTableViewCell.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-5-13. +// + +import UIKit +import Combine + +final class PickServerLoaderTableViewCell: TimelineLoaderTableViewCell { + + let containerView: UIView = { + let view = UIView() + view.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 10, right: 16) + view.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + let seperator: UIView = { + let view = UIView() + view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + let emptyStatusLabel: UILabel = { + let label = UILabel() + label.text = L10n.Scene.ServerPicker.EmptyState.noResults + label.textColor = Asset.Colors.Label.secondary.color + label.textAlignment = .center + label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 14, weight: .semibold), maximumPointSize: 19) + return label + }() + + override func _init() { + super._init() + + contentView.addSubview(containerView) + contentView.addSubview(seperator) + + NSLayoutConstraint.activate([ + // Set background view + containerView.topAnchor.constraint(equalTo: contentView.topAnchor), + containerView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + contentView.readableContentGuide.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + contentView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 1), + + // Set bottom separator + seperator.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + containerView.trailingAnchor.constraint(equalTo: seperator.trailingAnchor), + containerView.topAnchor.constraint(equalTo: seperator.topAnchor), + seperator.heightAnchor.constraint(equalToConstant: 1).priority(.defaultHigh), + ]) + + emptyStatusLabel.translatesAutoresizingMaskIntoConstraints = false + containerView.addSubview(emptyStatusLabel) + NSLayoutConstraint.activate([ + emptyStatusLabel.leadingAnchor.constraint(equalTo: containerView.readableContentGuide.leadingAnchor), + containerView.readableContentGuide.trailingAnchor.constraint(equalTo: emptyStatusLabel.trailingAnchor), + emptyStatusLabel.centerYAnchor.constraint(equalTo: containerView.centerYAnchor), + ]) + emptyStatusLabel.isHidden = true + + contentView.bringSubviewToFront(stackView) + activityIndicatorView.isHidden = false + startAnimating() + } +} + +#if canImport(SwiftUI) && DEBUG +import SwiftUI + +struct PickServerLoaderTableViewCell_Previews: PreviewProvider { + + static var previews: some View { + UIViewPreview(width: 375) { + PickServerLoaderTableViewCell() + } + .previewLayout(.fixed(width: 375, height: 100)) + } + +} + +#endif diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift index da7420e43..ded8fa49b 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift @@ -16,9 +16,9 @@ class TimelineLoaderTableViewCell: UITableViewCell { static let labelFont = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .medium)) var disposeBag = Set() - - var stateBindDispose: AnyCancellable? - + + let stackView = UIStackView() + let loadMoreButton: UIButton = { let button = HighlightDimmableButton() button.titleLabel?.font = TimelineLoaderTableViewCell.labelFont @@ -86,7 +86,6 @@ class TimelineLoaderTableViewCell: UITableViewCell { ]) // use stack view to alignlment content center - let stackView = UIStackView() stackView.spacing = 4 stackView.axis = .horizontal stackView.alignment = .center diff --git a/Mastodon/Scene/Webview/WebViewController.swift b/Mastodon/Scene/Share/Webview/WebViewController.swift similarity index 100% rename from Mastodon/Scene/Webview/WebViewController.swift rename to Mastodon/Scene/Share/Webview/WebViewController.swift diff --git a/Mastodon/Scene/Webview/WebViewModel.swift b/Mastodon/Scene/Share/Webview/WebViewModel.swift similarity index 100% rename from Mastodon/Scene/Webview/WebViewModel.swift rename to Mastodon/Scene/Share/Webview/WebViewModel.swift From 1a48b38b09a4c510c5ada451ad64d7ba90367ab9 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 13 May 2021 17:51:05 +0800 Subject: [PATCH 22/23] chore: update version to 0.4.0 (4) --- Mastodon.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 0eb170fc5..1d41296d1 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -3520,7 +3520,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = Mastodon/Info.plist; @@ -3528,7 +3528,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.3.0; + MARKETING_VERSION = 0.4.0; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3547,7 +3547,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = Mastodon/Info.plist; @@ -3555,7 +3555,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.3.0; + MARKETING_VERSION = 0.4.0; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From ff465ac18c7ac4c703a23a5329b928e8dbd012bb Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 13 May 2021 18:46:19 +0800 Subject: [PATCH 23/23] chore: update project settings for App Store --- Mastodon.xcodeproj/project.pbxproj | 28 ++----------------- .../xcschemes/xcschememanagement.plist | 4 +-- Mastodon/Info.plist | 2 ++ 3 files changed, 6 insertions(+), 28 deletions(-) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 1d41296d1..df5d04df6 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -285,13 +285,11 @@ DB6804872637CD4C00430867 /* AppShared.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; DB6804922637CD8700430867 /* AppName.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6804912637CD8700430867 /* AppName.swift */; }; DB6804A52637CDCC00430867 /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; }; - DB6804A62637CDCC00430867 /* AppShared.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; DB6804C82637CE2F00430867 /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; }; DB6804D12637CE4700430867 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6804D02637CE4700430867 /* UserDefaults.swift */; }; DB6804FD2637CFEC00430867 /* AppSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6804FC2637CFEC00430867 /* AppSecret.swift */; }; DB6805102637D0F800430867 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = DB68050F2637D0F800430867 /* KeychainAccess */; }; DB6805262637D7DD00430867 /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; }; - DB6805272637D7DD00430867 /* AppShared.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */; }; DB68A04A25E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A04925E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift */; }; DB68A05D25E9055900CFDF14 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DB68A05C25E9055900CFDF14 /* Settings.bundle */; }; @@ -521,28 +519,6 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ - DB6804A92637CDCC00430867 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - DB6804A62637CDCC00430867 /* AppShared.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; - DB68052A2637D7DD00430867 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - DB6805272637D7DD00430867 /* AppShared.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; DB89BA0825C10FD0008580ED /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -2499,7 +2475,6 @@ DB89B9EA25C10FD0008580ED /* Sources */, DB89B9EB25C10FD0008580ED /* Frameworks */, DB89B9EC25C10FD0008580ED /* Resources */, - DB68052A2637D7DD00430867 /* Embed Frameworks */, ); buildRules = ( ); @@ -2538,7 +2513,6 @@ DBF8AE0F263293E400C9C23C /* Sources */, DBF8AE10263293E400C9C23C /* Frameworks */, DBF8AE11263293E400C9C23C /* Resources */, - DB6804A92637CDCC00430867 /* Embed Frameworks */, ); buildRules = ( ); @@ -3650,6 +3624,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = D7D7CF93E262178800077512 /* Pods-Mastodon-AppShared.debug.xcconfig */; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -3680,6 +3655,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = B31D44635FCF6452F7E1B865 /* Pods-Mastodon-AppShared.release.xcconfig */; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index f092f9734..326857269 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -12,7 +12,7 @@ CoreDataStack.xcscheme_^#shared#^_ orderHint - 15 + 14 Mastodon - RTL.xcscheme_^#shared#^_ @@ -32,7 +32,7 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 14 + 15 SuppressBuildableAutocreation diff --git a/Mastodon/Info.plist b/Mastodon/Info.plist index 266cc9424..38651b0ef 100644 --- a/Mastodon/Info.plist +++ b/Mastodon/Info.plist @@ -2,6 +2,8 @@ + ITSAppUsesNonExemptEncryption + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable