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/Localization/app.json b/Localization/app.json index ab888f8f8..21304d564 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", @@ -93,6 +100,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 +148,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" } } }, @@ -143,7 +171,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" @@ -158,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": { @@ -297,6 +339,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": { @@ -304,7 +357,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", @@ -336,7 +394,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" } }, @@ -428,4 +486,4 @@ "text_placeholder": "Type or paste additional comments" } } -} +} \ No newline at end of file diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 506e32dea..df5d04df6 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 */; }; @@ -284,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 */; }; @@ -520,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; @@ -756,6 +733,9 @@ 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 = ""; }; + 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 = ""; }; @@ -1134,6 +1114,7 @@ 0FB3D30725E524C600AAD544 /* PickServerCategoriesCell.swift */, 0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */, 0FB3D33725E6401400AAD544 /* PickServerCell.swift */, + DB0F814F264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift */, ); path = TableViewCell; sourceTree = ""; @@ -1388,6 +1369,7 @@ 2D7631A425C1532200929FB9 /* Share */ = { isa = PBXGroup; children = ( + 5D03938E2612D200007FE196 /* Webview */, DB68A04F25E9028800CFDF14 /* NavigationController */, DB9D6C2025E502C60051B173 /* ViewModel */, 2D7631A525C1532D00929FB9 /* View */, @@ -2045,7 +2027,6 @@ DB8AF55525C1379F002E6C99 /* Scene */ = { isa = PBXGroup; children = ( - 5D03938E2612D200007FE196 /* Webview */, 2D7631A425C1532200929FB9 /* Share */, DB6180E426391A500018D199 /* Transition */, DB8AF54E25C13703002E6C99 /* MainTab */, @@ -2494,7 +2475,6 @@ DB89B9EA25C10FD0008580ED /* Sources */, DB89B9EB25C10FD0008580ED /* Frameworks */, DB89B9EC25C10FD0008580ED /* Resources */, - DB68052A2637D7DD00430867 /* Embed Frameworks */, ); buildRules = ( ); @@ -2533,7 +2513,6 @@ DBF8AE0F263293E400C9C23C /* Sources */, DBF8AE10263293E400C9C23C /* Frameworks */, DBF8AE11263293E400C9C23C /* Resources */, - DB6804A92637CDCC00430867 /* Embed Frameworks */, ); buildRules = ( ); @@ -2595,6 +2574,7 @@ knownRegions = ( en, Base, + ar, ); mainGroup = DB427DC925BAA00100D1B89D; packageReferences = ( @@ -2980,6 +2960,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 */, @@ -3353,6 +3334,7 @@ isa = PBXVariantGroup; children = ( DB2B3ABD25E37E15007045F9 /* en */, + DB0F814E264CFFD300F2A12B /* ar */, ); name = InfoPlist.strings; sourceTree = ""; @@ -3361,6 +3343,7 @@ isa = PBXVariantGroup; children = ( DB3D100E25BAA75E00EAA174 /* en */, + DB0F814D264CFFD300F2A12B /* ar */, ); name = Localizable.strings; sourceTree = ""; @@ -3388,6 +3371,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 +3433,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"; @@ -3509,7 +3494,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; @@ -3517,7 +3502,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 = ""; @@ -3536,7 +3521,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; @@ -3544,7 +3529,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 = ""; @@ -3639,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; @@ -3669,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; @@ -3916,7 +3903,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.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.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" } }, 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/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/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/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/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/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 5989d4f80..d139e061f 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,23 @@ extension StatusSection { } } cell.delegate = statusTableViewCellDelegate - + switch item { + 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()) + 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 +196,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 +214,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 +294,7 @@ extension StatusSection { break } } + imageView.accessibilityLabel = meta.altText Publishers.CombineLatest( statusItemAttribute.isImageLoaded, statusItemAttribute.isRevealing @@ -452,6 +472,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) @@ -463,7 +484,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, @@ -571,6 +593,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 { @@ -608,6 +631,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) @@ -620,8 +644,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 } } @@ -639,6 +665,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 = { @@ -647,6 +676,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 = { @@ -655,14 +689,18 @@ 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) .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 { diff --git a/Mastodon/Extension/ActiveLabel.swift b/Mastodon/Extension/ActiveLabel.swift index d929cb571..4e1d855b1 100644 --- a/Mastodon/Extension/ActiveLabel.swift +++ b/Mastodon/Extension/ActiveLabel.swift @@ -32,12 +32,14 @@ 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) textColor = Asset.Colors.Label.primary.color case .statusHeader: - font = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: .systemFont(ofSize: 13, weight: .medium)) + font = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: .systemFont(ofSize: 13, weight: .medium), maximumPointSize: 17) textColor = Asset.Colors.Label.secondary.color numberOfLines = 1 case .statusName: @@ -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 6e8c8e39d..8f6c13f9e 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)) @@ -208,6 +210,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 +254,46 @@ 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 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 + 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") @@ -284,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 { @@ -439,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 { @@ -598,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") @@ -646,8 +738,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 { @@ -655,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/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 diff --git a/Mastodon/Protocol/AvatarConfigurableView.swift b/Mastodon/Protocol/AvatarConfigurableView.swift index 3d2dba802..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() } @@ -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/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/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..c9ed556c3 --- /dev/null +++ b/Mastodon/Resources/ar.lproj/Localizable.strings @@ -0,0 +1,303 @@ +"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" = "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"; +"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.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"; +"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 diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 7b11194a3..c9ed556c3 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,22 @@ 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"; +"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 @@ -90,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..."; @@ -144,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"; @@ -193,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"; @@ -207,11 +239,25 @@ 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."; "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/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..2a0d46a16 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) @@ -552,6 +578,7 @@ extension ComposeViewController { collectionView.updateInteractiveMovementTargetPosition(position) case .ended: collectionView.endInteractiveMovement() + collectionView.reloadData() default: collectionView.cancelInteractiveMovement() } 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/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/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 0f2ba82fb..aa11e494d 100644 --- a/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageView.swift +++ b/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageView.swift @@ -16,6 +16,9 @@ final class MediaPreviewImageView: UIScrollView { imageView.contentMode = .scaleAspectFit imageView.clipsToBounds = true 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/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 7b76dd2f0..a950ede46 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 }() @@ -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 c049b961e..067283935 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 }() @@ -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, + ]) + } + } + } + +} 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/PickServerCategoriesCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCategoriesCell.swift index 5dd0e4008..373a90ddf 100644 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCategoriesCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCategoriesCell.swift @@ -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 + } + +} diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift index 3bf65756c..8eb0cb771 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 @@ -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 }() @@ -106,7 +109,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 +119,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 +129,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 +139,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 +150,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 +161,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 @@ -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/Onboarding/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift index 007012d3c..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) } } @@ -63,6 +66,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 +103,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O let domainLabel: UILabel = { let label = UILabel() - label.font = .preferredFont(forTextStyle: .headline) + label.font = MastodonRegisterViewController.textFieldLabelFont label.textColor = Asset.Colors.Label.primary.color return label }() @@ -113,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: UIFont.preferredFont(forTextStyle: .headline)]) + 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 @@ -124,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 }() @@ -136,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: UIFont.preferredFont(forTextStyle: .headline)]) + 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 @@ -153,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: UIFont.preferredFont(forTextStyle: .headline)]) + 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 @@ -164,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 }() @@ -178,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: UIFont.preferredFont(forTextStyle: .headline)]) + 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 @@ -195,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 }() @@ -208,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: UIFont.preferredFont(forTextStyle: .headline)]) + 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 @@ -219,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 }() 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 1e09116d3..e37427e32 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 = { @@ -101,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 @@ -112,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/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/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) } diff --git a/Mastodon/Scene/Settings/SettingsViewController.swift b/Mastodon/Scene/Settings/SettingsViewController.swift index e3b186026..1c69ef6c5 100644 --- a/Mastodon/Scene/Settings/SettingsViewController.swift +++ b/Mastodon/Scene/Settings/SettingsViewController.swift @@ -51,23 +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 - - let notifyLabel = UILabel() + return view + }() + + let notifyLabel = UILabel() + private(set) lazy var notifySectionHeader: UIView = { + let view = notifySectionHeaderStackView notifyLabel.translatesAutoresizingMaskIntoConstraints = false - notifyLabel.font = UIFontMetrics(forTextStyle: .title3).scaledFont(for: UIFont.systemFont(ofSize: 20, weight: .semibold)) + 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 view.addArrangedSubview(notifyLabel) view.addArrangedSubview(whoButton) + whoButton.setContentHuggingPriority(.defaultHigh + 1, for: .horizontal) + whoButton.setContentHuggingPriority(.defaultHigh + 1, for: .vertical) return view }() @@ -77,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 @@ -107,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 @@ -138,7 +144,25 @@ class SettingsViewController: UIViewController, NeedsDependency { } } + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + updateSectionHeaderStackViewLayout() + } + + // MAKR: - Private methods + private func updateSectionHeaderStackViewLayout() { + // accessibility + if traitCollection.preferredContentSizeCategory < .accessibilityMedium { + notifySectionHeaderStackView.axis = .horizontal + notifyLabel.numberOfLines = 1 + } else { + notifySectionHeaderStackView.axis = .vertical + notifyLabel.numberOfLines = 0 + } + } + private func bindViewModel() { self.whoButton.setTitle(viewModel.setting.value.activeSubscription?.policy.title, for: .normal) viewModel.setting @@ -173,6 +197,8 @@ class SettingsViewController: UIViewController, NeedsDependency { tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) setupTableView() + + updateSectionHeaderStackViewLayout() } private func setupNavigation() { diff --git a/Mastodon/Scene/Settings/View/Cell/SettingsAppearanceTableViewCell.swift b/Mastodon/Scene/Settings/View/Cell/SettingsAppearanceTableViewCell.swift index 5b7c53b02..a58bebf8c 100644 --- a/Mastodon/Scene/Settings/View/Cell/SettingsAppearanceTableViewCell.swift +++ b/Mastodon/Scene/Settings/View/Cell/SettingsAppearanceTableViewCell.swift @@ -15,6 +15,8 @@ protocol SettingsAppearanceTableViewCellDelegate: AnyObject { 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..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 } } } @@ -65,6 +66,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 272abd37e..32ee48df9 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) @@ -72,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([ @@ -90,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 } } @@ -144,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 diff --git a/Mastodon/Scene/Share/View/Content/ContentWarningOverlayView.swift b/Mastodon/Scene/Share/View/Content/ContentWarningOverlayView.swift index ba598a9a0..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 }() @@ -36,19 +37,21 @@ 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 + label.isAccessibilityElement = false return label }() 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 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 c06c956e0..1aea1bcc3 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) @@ -96,6 +102,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 +111,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 }() @@ -295,7 +303,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 +316,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 +335,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([ @@ -353,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) diff --git a/Mastodon/Scene/Share/View/Content/ThreadMetaView.swift b/Mastodon/Scene/Share/View/Content/ThreadMetaView.swift index 16d1b04a6..ff5fa58d6 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,53 @@ 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.spacing = 20 + actionButtonStackView.addArrangedSubview(reblogButton) + actionButtonStackView.addArrangedSubview(favoriteButton) dateLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) reblogButton.setContentHuggingPriority(.required - 2, for: .horizontal) favoriteButton.setContentHuggingPriority(.required - 1, for: .horizontal) + + updateContainerLayout() + + // TODO: + reblogButton.isAccessibilityElement = false + favoriteButton.isAccessibilityElement = false } + + 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 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/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/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) 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 diff --git a/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift b/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift index 74d82badd..eb4a43c1c 100644 --- a/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift +++ b/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift @@ -73,22 +73,36 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { imageView.contentMode = .scaleAspectFill imageView.isUserInteractionEnabled = false imageView.image = transitionItem.image + // accessibility + imageView.accessibilityIgnoresInvertColors = true return imageView }() transitionItem.targetFrame = transitionTargetFrame transitionItem.imageView = transitionImageView transitionContext.containerView.addSubview(transitionImageView) - let animator = MediaHostToMediaPreviewViewControllerAnimatedTransitioning.animator(initialVelocity: .zero) + toVC.closeButtonBackground.alpha = 0 + + if UIAccessibility.isReduceTransparencyEnabled { + toVC.visualEffectView.alpha = 0 + } + let animator = MediaHostToMediaPreviewViewControllerAnimatedTransitioning.animator(initialVelocity: .zero) + animator.addAnimations { transitionImageView.frame = transitionTargetFrame toView.alpha = 1 + if UIAccessibility.isReduceTransparencyEnabled { + toVC.visualEffectView.alpha = 1 + } } animator.addCompletion { position in toVC.pagingViewConttroller.view.alpha = 1 transitionImageView.removeFromSuperview() + UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseInOut]) { + toVC.closeButtonBackground.alpha = 1 + } transitionContext.completeTransition(position == .end) } @@ -119,9 +133,20 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { transitionContext.completeTransition(false) fatalError() } - mediaPreviewImageViewController.view.insertSubview(snapshot, aboveSubview: mediaPreviewImageViewController.previewImageView) - - snapshot.center = transitionContext.containerView.center + + let transitionMaskView = UIView(frame: transitionContext.containerView.bounds) + transitionMaskView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + transitionContext.containerView.addSubview(transitionMaskView) + + let maskLayer = CAShapeLayer() + maskLayer.frame = transitionMaskView.bounds + let maskLayerFromPath = UIBezierPath(rect: maskLayer.bounds).cgPath + maskLayer.path = maskLayerFromPath + transitionMaskView.layer.mask = maskLayer + + transitionMaskView.addSubview(snapshot) + snapshot.center = transitionMaskView.center + fromVC.view.bringSubviewToFront(fromVC.closeButtonBackground) transitionItem.imageView = imageView transitionItem.snapshotTransitioning = snapshot @@ -134,6 +159,38 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { let animator = popInteractiveTransitionAnimator self.transitionItem.snapshotRaw?.alpha = 0.0 + + var needsMaskWithAnimation = true + let maskLayerToRect: CGRect? = { + guard case .mosaic = transitionItem.source else { return nil } + guard let navigationBar = toVC.navigationController?.navigationBar else { return nil } + let navigationBarFrameInWindow = toVC.view.convert(navigationBar.frame, to: nil) + var rect = transitionMaskView.frame + rect.origin.y = navigationBarFrameInWindow.maxY + UIView.separatorLineHeight(of: toVC.view) // extra hairline + + if rect.minY < snapshot.frame.minY { + needsMaskWithAnimation = false + } + + return rect + }() + let maskLayerToPath = maskLayerToRect.flatMap { UIBezierPath(rect: $0) }?.cgPath + let maskLayerToFinalRect: CGRect? = { + guard case .mosaic = transitionItem.source else { return nil } + guard let tabBarController = toVC.tabBarController else { return nil } + let tabBarFrameInWindow = toVC.view.convert(tabBarController.tabBar.frame, to: nil) + var rect = maskLayerToRect ?? transitionMaskView.frame + let offset = rect.maxY - tabBarFrameInWindow.minY + guard offset > 0 else { return rect } + rect.size.height -= offset + return rect + }() + let maskLayerToFinalPath = maskLayerToFinalRect.flatMap { UIBezierPath(rect: $0) }?.cgPath + + if !needsMaskWithAnimation, let maskLayerToPath = maskLayerToPath { + maskLayer.path = maskLayerToPath + } + animator.addAnimations { if let targetFrame = targetFrame { self.transitionItem.snapshotTransitioning?.frame = targetFrame @@ -143,6 +200,12 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { self.transitionItem.sourceImageViewCornerRadius.flatMap { self.transitionItem.snapshotTransitioning?.layer.cornerRadius = $0 } fromVC.closeButtonBackground.alpha = 0 fromVC.visualEffectView.effect = nil + if let maskLayerToFinalPath = maskLayerToFinalPath { + maskLayer.path = maskLayerToFinalPath + } + if UIAccessibility.isReduceTransparencyEnabled { + fromVC.visualEffectView.alpha = 0 + } } animator.addCompletion { position in @@ -197,9 +260,21 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { transitionContext.completeTransition(false) return } - mediaPreviewImageViewController.view.insertSubview(snapshot, aboveSubview: mediaPreviewImageViewController.previewImageView) - - snapshot.center = transitionContext.containerView.center + + let transitionMaskView = UIView(frame: transitionContext.containerView.bounds) + transitionMaskView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + transitionContext.containerView.addSubview(transitionMaskView) + transitionItem.interactiveTransitionMaskView = transitionMaskView + + let maskLayer = CAShapeLayer() + maskLayer.frame = transitionMaskView.bounds + maskLayer.path = UIBezierPath(rect: maskLayer.bounds).cgPath + transitionMaskView.layer.mask = maskLayer + transitionItem.interactiveTransitionMaskLayer = maskLayer + + transitionMaskView.addSubview(snapshot) + snapshot.center = transitionMaskView.center + fromVC.view.bringSubviewToFront(fromVC.closeButtonBackground) transitionItem.imageView = imageView transitionItem.snapshotTransitioning = snapshot @@ -214,6 +289,10 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { let blurEffect = fromVC.visualEffectView.effect self.transitionItem.snapshotRaw?.alpha = 0.0 + UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseInOut]) { + fromVC.closeButtonBackground.alpha = 0 + } + animator.addAnimations { switch self.transitionItem.source { case .profileBanner: @@ -221,9 +300,11 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { default: break } - fromVC.closeButtonBackground.alpha = 0 fromVC.visualEffectView.effect = nil self.transitionItem.sourceImageViewCornerRadius.flatMap { self.transitionItem.snapshotTransitioning?.layer.cornerRadius = $0 } + if UIAccessibility.isReduceTransparencyEnabled { + fromVC.visualEffectView.alpha = 0 + } } animator.addCompletion { position in @@ -237,6 +318,13 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { self.transitionItem.source.updateAppearance(position: position, index: nil) } fromVC.visualEffectView.effect = position == .end ? nil : blurEffect + transitionMaskView.removeFromSuperview() + UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseInOut]) { + fromVC.closeButtonBackground.alpha = position == .end ? 0 : 1 + } + if UIAccessibility.isReduceTransparencyEnabled { + fromVC.visualEffectView.alpha = position == .end ? 0 : 1 + } transitionContext.completeTransition(position == .end) } } @@ -313,8 +401,51 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { let gestureVelocity = panGestureRecognizer.velocity(in: transitionContext.containerView) let velocity = convert(gestureVelocity, for: transitionItem) let itemAnimator = MediaHostToMediaPreviewViewControllerAnimatedTransitioning.animator(initialVelocity: velocity) + + var maskLayerToFinalPath: CGPath? + if toPosition == .end, + let transitionMaskView = transitionItem.interactiveTransitionMaskView, + let snapshot = transitionItem.snapshotTransitioning { + let toVC = transitionItem.previewableViewController + + var needsMaskWithAnimation = true + let maskLayerToRect: CGRect? = { + guard case .mosaic = transitionItem.source else { return nil } + guard let navigationBar = toVC.navigationController?.navigationBar else { return nil } + let navigationBarFrameInWindow = toVC.view.convert(navigationBar.frame, to: nil) + var rect = transitionMaskView.frame + rect.origin.y = navigationBarFrameInWindow.maxY + + if rect.minY < snapshot.frame.minY { + needsMaskWithAnimation = false + } + + return rect + }() + let maskLayerToPath = maskLayerToRect.flatMap { UIBezierPath(rect: $0) }?.cgPath + + if let maskLayer = transitionItem.interactiveTransitionMaskLayer, !needsMaskWithAnimation { + maskLayer.path = maskLayerToPath + } + + let maskLayerToFinalRect: CGRect? = { + guard case .mosaic = transitionItem.source else { return nil } + guard let tabBarController = toVC.tabBarController else { return nil } + let tabBarFrameInWindow = toVC.view.convert(tabBarController.tabBar.frame, to: nil) + var rect = maskLayerToRect ?? transitionMaskView.frame + let offset = rect.maxY - tabBarFrameInWindow.minY + guard offset > 0 else { return rect } + rect.size.height -= offset + return rect + }() + maskLayerToFinalPath = maskLayerToFinalRect.flatMap { UIBezierPath(rect: $0) }?.cgPath + } itemAnimator.addAnimations { + if let maskLayer = self.transitionItem.interactiveTransitionMaskLayer, + let maskLayerToFinalPath = maskLayerToFinalPath { + maskLayer.path = maskLayerToFinalPath + } if toPosition == .end { switch self.transitionItem.source { case .profileBanner where toPosition == .end: 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? }