Merge tag '0.4.0' into develop

no message
This commit is contained in:
CMK 2021-05-13 18:54:22 +08:00
commit 761d094832
59 changed files with 1459 additions and 161 deletions

View File

@ -47,6 +47,7 @@ private func map(language: String) -> String? {
case "ja_JP": return "ja" case "ja_JP": return "ja"
case "de_DE": return "de" case "de_DE": return "de"
case "pt_BR": return "pt-BR" case "pt_BR": return "pt-BR"
case "ar_SA": return "ar"
default: return nil default: return nil
} }
} }

View File

@ -21,6 +21,10 @@ mkdir -p input/en_US
cp ../app.json ./input/en_US cp ../app.json ./input/en_US
cp ../ios-infoPlist.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 <TBD>.zip -L ${Crowin_Latest_Build} # curl -o <TBD>.zip -L ${Crowin_Latest_Build}
# unzip -o -q <TBD>.zip -d input # unzip -o -q <TBD>.zip -d input
# rm -rf <TBD>.zip # rm -rf <TBD>.zip

View File

@ -74,10 +74,17 @@
"settings": "Settings", "settings": "Settings",
"delete": "Delete" "delete": "Delete"
}, },
"tabs": {
"home": "Home",
"search": "Search",
"notification": "Notification",
"profile": "Profile"
},
"status": { "status": {
"user_reblogged": "%s reblogged", "user_reblogged": "%s reblogged",
"user_replied_to": "Replied to %s", "user_replied_to": "Replied to %s",
"show_post": "Show Post", "show_post": "Show Post",
"show_user_profile": "Show user profile",
"content_warning": "content warning", "content_warning": "content warning",
"content_warning_text": "cw: %s", "content_warning_text": "cw: %s",
"media_content_warning": "Tap to reveal that may be sensitive", "media_content_warning": "Tap to reveal that may be sensitive",
@ -93,6 +100,22 @@
}, },
"time_left": "%s left", "time_left": "%s left",
"closed": "Closed" "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": { "firendship": {
@ -125,6 +148,11 @@
"blocked_warning": "You cant view Artbots profile\n until they unblock you.", "blocked_warning": "You cant view Artbots profile\n until they unblock you.",
"suspended_warning": "This account has been suspended.", "suspended_warning": "This account has been suspended.",
"user_suspended_warning": "%s's 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.", "title": "Pick a Server,\nany server.",
"button": { "button": {
"category": { "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_less": "See Less",
"see_more": "See More" "see_more": "See More"
@ -158,7 +199,8 @@
}, },
"empty_state": { "empty_state": {
"finding_servers": "Finding available servers...", "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": { "register": {
@ -297,6 +339,17 @@
"unlisted": "Unlisted", "unlisted": "Unlisted",
"private": "Followers only", "private": "Followers only",
"direct": "Only people I mention" "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": { "profile": {
@ -304,7 +357,12 @@
"dashboard": { "dashboard": {
"posts": "posts", "posts": "posts",
"following": "following", "following": "following",
"followers": "followers" "followers": "followers",
"accessibility": {
"count_posts": "%ld posts",
"count_following": "%ld following",
"count_followers": "%ld followers"
}
}, },
"segmented_control": { "segmented_control": {
"posts": "Posts", "posts": "Posts",
@ -336,7 +394,7 @@
}, },
"accounts": { "accounts": {
"title": "Accounts you might like", "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" "follow": "Follow"
} }
}, },

View File

@ -191,6 +191,7 @@
DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */; }; DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */; };
DB084B5725CBC56C00F898ED /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB084B5625CBC56C00F898ED /* Status.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 */; }; 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 */; }; DB118A8225E4B6E600FAB162 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */; };
DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */; }; DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */; };
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D186B25EF5BA7003F1F23 /* PollTableView.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, ); }; }; 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 */; }; DB6804922637CD8700430867 /* AppName.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6804912637CD8700430867 /* AppName.swift */; };
DB6804A52637CDCC00430867 /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; }; 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 */; }; DB6804C82637CE2F00430867 /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; };
DB6804D12637CE4700430867 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6804D02637CE4700430867 /* UserDefaults.swift */; }; DB6804D12637CE4700430867 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6804D02637CE4700430867 /* UserDefaults.swift */; };
DB6804FD2637CFEC00430867 /* AppSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6804FC2637CFEC00430867 /* AppSecret.swift */; }; DB6804FD2637CFEC00430867 /* AppSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6804FC2637CFEC00430867 /* AppSecret.swift */; };
DB6805102637D0F800430867 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = DB68050F2637D0F800430867 /* KeychainAccess */; }; DB6805102637D0F800430867 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = DB68050F2637D0F800430867 /* KeychainAccess */; };
DB6805262637D7DD00430867 /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; }; 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 */; }; DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */; };
DB68A04A25E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A04925E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift */; }; DB68A04A25E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A04925E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift */; };
DB68A05D25E9055900CFDF14 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DB68A05C25E9055900CFDF14 /* Settings.bundle */; }; DB68A05D25E9055900CFDF14 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DB68A05C25E9055900CFDF14 /* Settings.bundle */; };
@ -520,28 +519,6 @@
/* End PBXContainerItemProxy section */ /* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase 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 */ = { DB89BA0825C10FD0008580ED /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase; isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@ -756,6 +733,9 @@
DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveUserInterfaceStyleBarButtonItem.swift; sourceTree = "<group>"; }; DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveUserInterfaceStyleBarButtonItem.swift; sourceTree = "<group>"; };
DB084B5625CBC56C00F898ED /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = "<group>"; }; DB084B5625CBC56C00F898ED /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = "<group>"; };
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = "<group>"; }; DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = "<group>"; };
DB0F814D264CFFD300F2A12B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = "<group>"; };
DB0F814E264CFFD300F2A12B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = "<group>"; };
DB0F814F264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerLoaderTableViewCell.swift; sourceTree = "<group>"; };
DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; }; DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDimmableButton.swift; sourceTree = "<group>"; }; DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDimmableButton.swift; sourceTree = "<group>"; };
DB1D186B25EF5BA7003F1F23 /* PollTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollTableView.swift; sourceTree = "<group>"; }; DB1D186B25EF5BA7003F1F23 /* PollTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollTableView.swift; sourceTree = "<group>"; };
@ -1134,6 +1114,7 @@
0FB3D30725E524C600AAD544 /* PickServerCategoriesCell.swift */, 0FB3D30725E524C600AAD544 /* PickServerCategoriesCell.swift */,
0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */, 0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */,
0FB3D33725E6401400AAD544 /* PickServerCell.swift */, 0FB3D33725E6401400AAD544 /* PickServerCell.swift */,
DB0F814F264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift */,
); );
path = TableViewCell; path = TableViewCell;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1388,6 +1369,7 @@
2D7631A425C1532200929FB9 /* Share */ = { 2D7631A425C1532200929FB9 /* Share */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
5D03938E2612D200007FE196 /* Webview */,
DB68A04F25E9028800CFDF14 /* NavigationController */, DB68A04F25E9028800CFDF14 /* NavigationController */,
DB9D6C2025E502C60051B173 /* ViewModel */, DB9D6C2025E502C60051B173 /* ViewModel */,
2D7631A525C1532D00929FB9 /* View */, 2D7631A525C1532D00929FB9 /* View */,
@ -2045,7 +2027,6 @@
DB8AF55525C1379F002E6C99 /* Scene */ = { DB8AF55525C1379F002E6C99 /* Scene */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
5D03938E2612D200007FE196 /* Webview */,
2D7631A425C1532200929FB9 /* Share */, 2D7631A425C1532200929FB9 /* Share */,
DB6180E426391A500018D199 /* Transition */, DB6180E426391A500018D199 /* Transition */,
DB8AF54E25C13703002E6C99 /* MainTab */, DB8AF54E25C13703002E6C99 /* MainTab */,
@ -2494,7 +2475,6 @@
DB89B9EA25C10FD0008580ED /* Sources */, DB89B9EA25C10FD0008580ED /* Sources */,
DB89B9EB25C10FD0008580ED /* Frameworks */, DB89B9EB25C10FD0008580ED /* Frameworks */,
DB89B9EC25C10FD0008580ED /* Resources */, DB89B9EC25C10FD0008580ED /* Resources */,
DB68052A2637D7DD00430867 /* Embed Frameworks */,
); );
buildRules = ( buildRules = (
); );
@ -2533,7 +2513,6 @@
DBF8AE0F263293E400C9C23C /* Sources */, DBF8AE0F263293E400C9C23C /* Sources */,
DBF8AE10263293E400C9C23C /* Frameworks */, DBF8AE10263293E400C9C23C /* Frameworks */,
DBF8AE11263293E400C9C23C /* Resources */, DBF8AE11263293E400C9C23C /* Resources */,
DB6804A92637CDCC00430867 /* Embed Frameworks */,
); );
buildRules = ( buildRules = (
); );
@ -2595,6 +2574,7 @@
knownRegions = ( knownRegions = (
en, en,
Base, Base,
ar,
); );
mainGroup = DB427DC925BAA00100D1B89D; mainGroup = DB427DC925BAA00100D1B89D;
packageReferences = ( packageReferences = (
@ -2980,6 +2960,7 @@
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */, DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */,
DBE3CE0D261D767100430CC6 /* FavoriteViewController+Provider.swift in Sources */, DBE3CE0D261D767100430CC6 /* FavoriteViewController+Provider.swift in Sources */,
2D084B9326259545003AA3AF /* NotificationViewModel+LoadLatestState.swift in Sources */, 2D084B9326259545003AA3AF /* NotificationViewModel+LoadLatestState.swift in Sources */,
DB0F8150264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift in Sources */,
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */, DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */, DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */,
DBB9759C262462E1004620BD /* ThreadMetaView.swift in Sources */, DBB9759C262462E1004620BD /* ThreadMetaView.swift in Sources */,
@ -3353,6 +3334,7 @@
isa = PBXVariantGroup; isa = PBXVariantGroup;
children = ( children = (
DB2B3ABD25E37E15007045F9 /* en */, DB2B3ABD25E37E15007045F9 /* en */,
DB0F814E264CFFD300F2A12B /* ar */,
); );
name = InfoPlist.strings; name = InfoPlist.strings;
sourceTree = "<group>"; sourceTree = "<group>";
@ -3361,6 +3343,7 @@
isa = PBXVariantGroup; isa = PBXVariantGroup;
children = ( children = (
DB3D100E25BAA75E00EAA174 /* en */, DB3D100E25BAA75E00EAA174 /* en */,
DB0F814D264CFFD300F2A12B /* ar */,
); );
name = Localizable.strings; name = Localizable.strings;
sourceTree = "<group>"; sourceTree = "<group>";
@ -3388,6 +3371,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
@ -3449,6 +3433,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
@ -3509,7 +3494,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
DEVELOPMENT_TEAM = 7LFDZ96332; DEVELOPMENT_TEAM = 7LFDZ96332;
INFOPLIST_FILE = Mastodon/Info.plist; INFOPLIST_FILE = Mastodon/Info.plist;
@ -3517,7 +3502,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.3.0; MARKETING_VERSION = 0.4.0;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3536,7 +3521,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
DEVELOPMENT_TEAM = 7LFDZ96332; DEVELOPMENT_TEAM = 7LFDZ96332;
INFOPLIST_FILE = Mastodon/Info.plist; INFOPLIST_FILE = Mastodon/Info.plist;
@ -3544,7 +3529,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.3.0; MARKETING_VERSION = 0.4.0;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3639,6 +3624,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = D7D7CF93E262178800077512 /* Pods-Mastodon-AppShared.debug.xcconfig */; baseConfigurationReference = D7D7CF93E262178800077512 /* Pods-Mastodon-AppShared.debug.xcconfig */;
buildSettings = { buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
@ -3669,6 +3655,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = B31D44635FCF6452F7E1B865 /* Pods-Mastodon-AppShared.release.xcconfig */; baseConfigurationReference = B31D44635FCF6452F7E1B865 /* Pods-Mastodon-AppShared.release.xcconfig */;
buildSettings = { buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
@ -3916,7 +3903,7 @@
repositoryURL = "https://github.com/TwidereProject/ActiveLabel.swift"; repositoryURL = "https://github.com/TwidereProject/ActiveLabel.swift";
requirement = { requirement = {
kind = exactVersion; kind = exactVersion;
version = 5.0.1; version = 5.0.2;
}; };
}; };
2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */ = { 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */ = {

View File

@ -12,7 +12,7 @@
<key>CoreDataStack.xcscheme_^#shared#^_</key> <key>CoreDataStack.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>15</integer> <integer>14</integer>
</dict> </dict>
<key>Mastodon - RTL.xcscheme_^#shared#^_</key> <key>Mastodon - RTL.xcscheme_^#shared#^_</key>
<dict> <dict>
@ -32,7 +32,7 @@
<key>NotificationService.xcscheme_^#shared#^_</key> <key>NotificationService.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>14</integer> <integer>15</integer>
</dict> </dict>
</dict> </dict>
<key>SuppressBuildableAutocreation</key> <key>SuppressBuildableAutocreation</key>

View File

@ -6,8 +6,8 @@
"repositoryURL": "https://github.com/TwidereProject/ActiveLabel.swift", "repositoryURL": "https://github.com/TwidereProject/ActiveLabel.swift",
"state": { "state": {
"branch": null, "branch": null,
"revision": "40e104063d825d1125ef4b8eeb6460eba8a57483", "revision": "2132a7bf8da2bea74bb49b0b815950ea4d8f6a7e",
"version": "5.0.1" "version": "5.0.2"
} }
}, },
{ {
@ -69,7 +69,7 @@
"repositoryURL": "https://github.com/onevcat/Kingfisher.git", "repositoryURL": "https://github.com/onevcat/Kingfisher.git",
"state": { "state": {
"branch": null, "branch": null,
"revision": "15d199e84677303a7004ed2c5ecaa1a90f3863f8", "revision": "bbc4bc4def7eb05a7ba8e1219f80ee9be327334e",
"version": "6.2.1" "version": "6.2.1"
} }
}, },

View File

@ -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 { extension CategoryPickerItem: Equatable {

View File

@ -14,6 +14,7 @@ enum PickServerItem {
case categoryPicker(items: [CategoryPickerItem]) case categoryPicker(items: [CategoryPickerItem])
case search case search
case server(server: Mastodon.Entity.Server, attribute: ServerItemAttribute) case server(server: Mastodon.Entity.Server, attribute: ServerItemAttribute)
case loader(attribute: LoaderItemAttribute)
} }
extension PickServerItem { extension PickServerItem {
@ -34,6 +35,26 @@ extension PickServerItem {
hasher.combine(isExpand) 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 { extension PickServerItem: Equatable {
@ -47,6 +68,8 @@ extension PickServerItem: Equatable {
return true return true
case (.server(let serverLeft, _), .server(let serverRight, _)): case (.server(let serverLeft, _), .server(let serverRight, _)):
return serverLeft.domain == serverRight.domain return serverLeft.domain == serverRight.domain
case (.loader(let attributeLeft), loader(let attributeRight)):
return attributeLeft == attributeRight
default: default:
return false return false
} }
@ -64,6 +87,8 @@ extension PickServerItem: Hashable {
hasher.combine(String(describing: PickServerItem.search.self)) hasher.combine(String(describing: PickServerItem.search.self))
case .server(let server, _): case .server(let server, _):
hasher.combine(server.domain) hasher.combine(server.domain)
case .loader(let attribute):
hasher.combine(attribute)
} }
} }
} }

View File

@ -42,6 +42,10 @@ extension CategoryPickerSection {
} }
} }
.store(in: &cell.observations) .store(in: &cell.observations)
cell.isAccessibilityElement = true
cell.accessibilityLabel = item.accessibilityDescription
return cell return cell
} }
} }

View File

@ -32,6 +32,7 @@ extension CustomEmojiPickerSection {
], ],
completionHandler: nil completionHandler: nil
) )
cell.accessibilityLabel = attribute.emoji.shortcode
return cell return cell
} }
} }

View File

@ -57,6 +57,10 @@ extension PickServerSection {
PickServerSection.configure(cell: cell, server: server, attribute: attribute) PickServerSection.configure(cell: cell, server: server, attribute: attribute)
cell.delegate = pickServerCellDelegate cell.delegate = pickServerCellDelegate
return cell 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
}
}

View File

@ -58,6 +58,7 @@ extension StatusSection {
) )
} }
cell.delegate = statusTableViewCellDelegate cell.delegate = statusTableViewCellDelegate
cell.isAccessibilityElement = true
return cell return cell
case .status(let objectID, let attribute), case .status(let objectID, let attribute),
.root(let objectID, let attribute), .root(let objectID, let attribute),
@ -97,7 +98,23 @@ extension StatusSection {
} }
} }
cell.delegate = statusTableViewCellDelegate 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 return cell
case .leafBottomLoader: case .leafBottomLoader:
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ThreadReplyLoaderTableViewCell.self), for: indexPath) as! ThreadReplyLoaderTableViewCell 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.nameLabel.configure(content: nameText, emojiDict: (status.reblog ?? status).author.emojiDict)
cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct
// set avatar // set avatar
if let reblog = status.reblog { if let reblog = status.reblog {
cell.statusView.avatarButton.isHidden = true cell.statusView.avatarButton.isHidden = true
@ -196,6 +214,7 @@ extension StatusSection {
content: (status.reblog ?? status).content, content: (status.reblog ?? status).content,
emojiDict: (status.reblog ?? status).emojiDict emojiDict: (status.reblog ?? status).emojiDict
) )
cell.statusView.activeTextLabel.accessibilityLanguage = (status.reblog ?? status).language
// set visibility // set visibility
if let visibility = (status.reblog ?? status).visibility { if let visibility = (status.reblog ?? status).visibility {
@ -275,6 +294,7 @@ extension StatusSection {
break break
} }
} }
imageView.accessibilityLabel = meta.altText
Publishers.CombineLatest( Publishers.CombineLatest(
statusItemAttribute.isImageLoaded, statusItemAttribute.isImageLoaded,
statusItemAttribute.isRevealing statusItemAttribute.isRevealing
@ -452,6 +472,7 @@ extension StatusSection {
.sink { [weak cell] _ in .sink { [weak cell] _ in
guard let cell = cell else { return } guard let cell = cell else { return }
cell.statusView.dateLabel.text = createdAt.shortTimeAgoSinceNow cell.statusView.dateLabel.text = createdAt.shortTimeAgoSinceNow
cell.statusView.dateLabel.accessibilityLabel = createdAt.timeAgoSinceNow
} }
.store(in: &cell.disposeBag) .store(in: &cell.disposeBag)
@ -463,7 +484,8 @@ extension StatusSection {
} receiveValue: { [weak dependency, weak cell] change in } receiveValue: { [weak dependency, weak cell] change in
guard let dependency = dependency else { return } guard let dependency = dependency else { return }
guard case .update(let object) = change.changeType, 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 } guard let statusTableViewCell = cell as? StatusTableViewCell else { return }
StatusSection.configureActionToolBar( StatusSection.configureActionToolBar(
cell: statusTableViewCell, cell: statusTableViewCell,
@ -571,6 +593,7 @@ extension StatusSection {
formatter.timeStyle = .short formatter.timeStyle = .short
return formatter.string(from: status.createdAt) return formatter.string(from: status.createdAt)
}() }()
cell.threadMetaView.dateLabel.accessibilityLabel = DateFormatter.localizedString(from: status.createdAt, dateStyle: .medium, timeStyle: .short)
let reblogCountTitle: String = { let reblogCountTitle: String = {
let count = status.reblogsCount.intValue let count = status.reblogsCount.intValue
if count > 1 { if count > 1 {
@ -608,6 +631,7 @@ extension StatusSection {
return L10n.Common.Controls.Status.userReblogged(name) return L10n.Common.Controls.Status.userReblogged(name)
}() }()
cell.statusView.headerInfoLabel.configure(content: headerText, emojiDict: status.author.emojiDict) cell.statusView.headerInfoLabel.configure(content: headerText, emojiDict: status.author.emojiDict)
cell.statusView.headerInfoLabel.isAccessibilityElement = true
} else if status.inReplyToID != nil { } else if status.inReplyToID != nil {
cell.statusView.headerContainerView.isHidden = false cell.statusView.headerContainerView.isHidden = false
cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage) cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage)
@ -620,8 +644,10 @@ extension StatusSection {
return L10n.Common.Controls.Status.userRepliedTo(name) return L10n.Common.Controls.Status.userRepliedTo(name)
}() }()
cell.statusView.headerInfoLabel.configure(content: headerText, emojiDict: status.replyTo?.author.emojiDict ?? [:]) cell.statusView.headerInfoLabel.configure(content: headerText, emojiDict: status.replyTo?.author.emojiDict ?? [:])
cell.statusView.headerInfoLabel.isAccessibilityElement = true
} else { } else {
cell.statusView.headerContainerView.isHidden = true cell.statusView.headerContainerView.isHidden = true
cell.statusView.headerInfoLabel.isAccessibilityElement = false
} }
} }
@ -639,6 +665,9 @@ extension StatusSection {
return StatusSection.formattedNumberTitleForActionButton(count) return StatusSection.formattedNumberTitleForActionButton(count)
}() }()
cell.statusView.actionToolbarContainer.replyButton.setTitle(replyCountTitle, for: .normal) 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 // set reblog
let isReblogged = status.rebloggedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false let isReblogged = status.rebloggedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false
let reblogCountTitle: String = { let reblogCountTitle: String = {
@ -647,6 +676,11 @@ extension StatusSection {
}() }()
cell.statusView.actionToolbarContainer.reblogButton.setTitle(reblogCountTitle, for: .normal) cell.statusView.actionToolbarContainer.reblogButton.setTitle(reblogCountTitle, for: .normal)
cell.statusView.actionToolbarContainer.isReblogButtonHighlight = isReblogged 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 // set like
let isLike = status.favouritedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false let isLike = status.favouritedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false
let favoriteCountTitle: String = { let favoriteCountTitle: String = {
@ -655,14 +689,18 @@ extension StatusSection {
}() }()
cell.statusView.actionToolbarContainer.favoriteButton.setTitle(favoriteCountTitle, for: .normal) cell.statusView.actionToolbarContainer.favoriteButton.setTitle(favoriteCountTitle, for: .normal)
cell.statusView.actionToolbarContainer.isFavoriteButtonHighlight = isLike 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( Publishers.CombineLatest(
dependency.context.blockDomainService.blockedDomains, dependency.context.blockDomainService.blockedDomains,
ManagedObjectObserver.observe(object: status.authorForUserProvider) ManagedObjectObserver.observe(object: status.authorForUserProvider)
.assertNoFailure() .assertNoFailure()
) )
.receive(on: DispatchQueue.main) .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 cell = cell else { return }
guard let dependency = dependency else { return } guard let dependency = dependency else { return }
switch change.changeType { switch change.changeType {

View File

@ -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." text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
#endif #endif
accessibilityContainerType = .semanticGroup
switch style { switch style {
case .default: case .default:
font = .preferredFont(forTextStyle: .body) font = .preferredFont(forTextStyle: .body)
textColor = Asset.Colors.Label.primary.color textColor = Asset.Colors.Label.primary.color
case .statusHeader: 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 textColor = Asset.Colors.Label.secondary.color
numberOfLines = 1 numberOfLines = 1
case .statusName: case .statusName:
@ -61,8 +63,10 @@ extension ActiveLabel {
if let parseResult = try? MastodonStatusContent.parse(content: content, emojiDict: emojiDict) { if let parseResult = try? MastodonStatusContent.parse(content: content, emojiDict: emojiDict) {
text = parseResult.trimmed text = parseResult.trimmed
activeEntities = parseResult.activeEntities activeEntities = parseResult.activeEntities
accessibilityLabel = parseResult.original
} else { } else {
text = "" text = ""
accessibilityLabel = nil
} }
} }
@ -79,5 +83,110 @@ extension ActiveLabel {
let parseResult = MastodonField.parse(field: field) let parseResult = MastodonField.parse(field: field)
text = parseResult.value text = parseResult.value
activeEntities = parseResult.activeEntities 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
// }
}

View File

@ -200,6 +200,8 @@ internal enum L10n {
internal static let mediaContentWarning = L10n.tr("Localizable", "Common.Controls.Status.MediaContentWarning") internal static let mediaContentWarning = L10n.tr("Localizable", "Common.Controls.Status.MediaContentWarning")
/// Show Post /// Show Post
internal static let showPost = L10n.tr("Localizable", "Common.Controls.Status.ShowPost") 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 /// %@ reblogged
internal static func userReblogged(_ p1: Any) -> String { internal static func userReblogged(_ p1: Any) -> String {
return L10n.tr("Localizable", "Common.Controls.Status.UserReblogged", String(describing: p1)) 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 { internal static func userRepliedTo(_ p1: Any) -> String {
return L10n.tr("Localizable", "Common.Controls.Status.UserRepliedTo", String(describing: p1)) 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 { internal enum Poll {
/// Closed /// Closed
internal static let closed = L10n.tr("Localizable", "Common.Controls.Status.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 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 { internal enum Header {
/// You cant view Artbots profile\n until they unblock you. /// You cant view Artbots profile\n until they unblock you.
internal static let blockedWarning = L10n.tr("Localizable", "Common.Controls.Timeline.Header.BlockedWarning") 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 { internal static func replyingToUser(_ p1: Any) -> String {
return L10n.tr("Localizable", "Scene.Compose.ReplyingToUser", String(describing: p1)) 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 { internal enum Attachment {
/// This %@ is broken and can't be\nuploaded to Mastodon. /// This %@ is broken and can't be\nuploaded to Mastodon.
internal static func attachmentBroken(_ p1: Any) -> String { 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") internal static let following = L10n.tr("Localizable", "Scene.Profile.Dashboard.Following")
/// posts /// posts
internal static let posts = L10n.tr("Localizable", "Scene.Profile.Dashboard.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 RelationshipActionAlert {
internal enum ConfirmUnblockUsre { internal enum ConfirmUnblockUsre {
@ -598,7 +690,7 @@ internal enum L10n {
/// See All /// See All
internal static let buttonText = L10n.tr("Localizable", "Scene.Search.Recommend.ButtonText") internal static let buttonText = L10n.tr("Localizable", "Scene.Search.Recommend.ButtonText")
internal enum Accounts { 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") internal static let description = L10n.tr("Localizable", "Scene.Search.Recommend.Accounts.Description")
/// Follow /// Follow
internal static let follow = L10n.tr("Localizable", "Scene.Search.Recommend.Accounts.Follow") internal static let follow = L10n.tr("Localizable", "Scene.Search.Recommend.Accounts.Follow")
@ -646,8 +738,34 @@ internal enum L10n {
/// See More /// See More
internal static let seeMore = L10n.tr("Localizable", "Scene.ServerPicker.Button.SeeMore") internal static let seeMore = L10n.tr("Localizable", "Scene.ServerPicker.Button.SeeMore")
internal enum Category { 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 /// All
internal static let all = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.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 { internal enum EmptyState {
@ -655,6 +773,8 @@ internal enum L10n {
internal static let badNetwork = L10n.tr("Localizable", "Scene.ServerPicker.EmptyState.BadNetwork") internal static let badNetwork = L10n.tr("Localizable", "Scene.ServerPicker.EmptyState.BadNetwork")
/// Finding available servers... /// Finding available servers...
internal static let findingServers = L10n.tr("Localizable", "Scene.ServerPicker.EmptyState.FindingServers") 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 { internal enum Input {
/// Find a server or join your own... /// Find a server or join your own...

View File

@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>

View File

@ -26,7 +26,7 @@ extension AvatarConfigurableView {
if Self.configurableAvatarImageCornerRadius < Self.configurableAvatarImageSize.width * 0.5 { if Self.configurableAvatarImageCornerRadius < Self.configurableAvatarImageSize.width * 0.5 {
return placeholderImage return placeholderImage
.af.imageAspectScaled(toFill: Self.configurableAvatarImageSize) .af.imageAspectScaled(toFill: Self.configurableAvatarImageSize)
.af.imageRounded(withCornerRadius: Self.configurableAvatarImageCornerRadius, divideRadiusByImageScale: true) .af.imageRounded(withCornerRadius: Self.configurableAvatarImageCornerRadius, divideRadiusByImageScale: false)
} else { } else {
return placeholderImage.af.imageRoundedIntoCircle() return placeholderImage.af.imageRoundedIntoCircle()
} }
@ -47,6 +47,10 @@ extension AvatarConfigurableView {
configurableAvatarButton?.layer.cornerRadius = 0 configurableAvatarButton?.layer.cornerRadius = 0
configurableAvatarButton?.layer.cornerCurve = .circular configurableAvatarButton?.layer.cornerCurve = .circular
// accessibility
configurableAvatarImageView?.accessibilityIgnoresInvertColors = true
configurableAvatarButton?.accessibilityIgnoresInvertColors = true
defer { defer {
avatarConfigurableView(self, didFinishConfiguration: configuration) avatarConfigurableView(self, didFinishConfiguration: configuration)
} }

View File

@ -59,7 +59,6 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
extension StatusTableViewCellDelegate where Self: StatusProvider { extension StatusTableViewCellDelegate where Self: StatusProvider {
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) { func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) {
StatusProviderFacade.responseToStatusContentWarningRevealAction(provider: self, cell: cell) StatusProviderFacade.responseToStatusContentWarningRevealAction(provider: self, cell: cell)
} }
@ -76,8 +75,12 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
extension StatusTableViewCellDelegate where Self: StatusProvider & MediaPreviewableViewController { extension StatusTableViewCellDelegate where Self: StatusProvider & MediaPreviewableViewController {
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) { func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) {
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) StatusProviderFacade.coordinateToStatusMediaPreviewScene(provider: self, cell: cell, mosaicImageView: mosaicImageViewContainer, didTapImageView: imageView, atIndex: index)
} }
}
} }
// MARK: - PollTableView // MARK: - PollTableView

View File

@ -0,0 +1,2 @@
"NSCameraUsageDescription" = "Used to take photo for post status";
"NSPhotoLibraryAddUsageDescription" = "Used to save photo into the Photo Library";

View File

@ -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 cant view Artbots profile
until they unblock you.";
"Common.Controls.Timeline.Header.BlockingWarning" = "You cant view Artbots 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 whats 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 havent.";
"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 havent.";
"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 youd 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, youll 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.";

View File

@ -64,6 +64,12 @@ Please check your internet connection.";
"Common.Controls.Firendship.UnblockUser" = "Unblock %@"; "Common.Controls.Firendship.UnblockUser" = "Unblock %@";
"Common.Controls.Firendship.Unmute" = "Unmute"; "Common.Controls.Firendship.Unmute" = "Unmute";
"Common.Controls.Firendship.UnmuteUser" = "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.ContentWarning" = "content warning";
"Common.Controls.Status.ContentWarningText" = "cw: %@"; "Common.Controls.Status.ContentWarningText" = "cw: %@";
"Common.Controls.Status.MediaContentWarning" = "Tap to reveal that may be sensitive"; "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.Multiple" = "%d voters";
"Common.Controls.Status.Poll.VoterCount.Single" = "%d voter"; "Common.Controls.Status.Poll.VoterCount.Single" = "%d voter";
"Common.Controls.Status.ShowPost" = "Show Post"; "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.UserReblogged" = "%@ reblogged";
"Common.Controls.Status.UserRepliedTo" = "Replied to %@"; "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 cant view Artbots profile "Common.Controls.Timeline.Header.BlockedWarning" = "You cant view Artbots profile
until they unblock you."; until they unblock you.";
"Common.Controls.Timeline.Header.BlockingWarning" = "You cant view Artbots profile "Common.Controls.Timeline.Header.BlockingWarning" = "You cant view Artbots profile
@ -90,6 +110,15 @@ Your account looks like this to them.";
"Common.Controls.Timeline.Loader.ShowMoreReplies" = "Show more replies"; "Common.Controls.Timeline.Loader.ShowMoreReplies" = "Show more replies";
"Common.Countable.Photo.Multiple" = "photos"; "Common.Countable.Photo.Multiple" = "photos";
"Common.Countable.Photo.Single" = "photo"; "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 "Scene.Compose.Attachment.AttachmentBroken" = "This %@ is broken and can't be
uploaded to Mastodon."; uploaded to Mastodon.";
"Scene.Compose.Attachment.DescriptionPhoto" = "Describe photo for low vision people..."; "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.Action.Reblog" = "rebloged your post";
"Scene.Notification.Title.Everything" = "Everything"; "Scene.Notification.Title.Everything" = "Everything";
"Scene.Notification.Title.Mentions" = "Mentions"; "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.Followers" = "followers";
"Scene.Profile.Dashboard.Following" = "following"; "Scene.Profile.Dashboard.Following" = "following";
"Scene.Profile.Dashboard.Posts" = "posts"; "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.Step2" = "Step 2 of 2";
"Scene.Report.TextPlaceholder" = "Type or paste additional comments"; "Scene.Report.TextPlaceholder" = "Type or paste additional comments";
"Scene.Report.Title" = "Report %@"; "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.Follow" = "Follow";
"Scene.Search.Recommend.Accounts.Title" = "Accounts you might like"; "Scene.Search.Recommend.Accounts.Title" = "Accounts you might like";
"Scene.Search.Recommend.ButtonText" = "See All"; "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.All" = "All";
"Scene.Search.Searching.Segment.Hashtags" = "Hashtags"; "Scene.Search.Searching.Segment.Hashtags" = "Hashtags";
"Scene.Search.Searching.Segment.People" = "People"; "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.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.SeeLess" = "See Less";
"Scene.ServerPicker.Button.SeeMore" = "See More"; "Scene.ServerPicker.Button.SeeMore" = "See More";
"Scene.ServerPicker.EmptyState.BadNetwork" = "Something went wrong while loading data. Check your internet connection."; "Scene.ServerPicker.EmptyState.BadNetwork" = "Something went wrong while loading data. Check your internet connection.";
"Scene.ServerPicker.EmptyState.FindingServers" = "Finding available servers..."; "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.Input.Placeholder" = "Find a server or join your own...";
"Scene.ServerPicker.Label.Category" = "CATEGORY"; "Scene.ServerPicker.Label.Category" = "CATEGORY";
"Scene.ServerPicker.Label.Language" = "LANGUAGE"; "Scene.ServerPicker.Label.Language" = "LANGUAGE";

View File

@ -47,6 +47,10 @@ extension CustomEmojiPickerItemCollectionViewCell {
emojiImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), emojiImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
emojiImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), emojiImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
]) ])
isAccessibilityElement = true
accessibilityTraits = .button
accessibilityHint = "emoji"
} }
} }

View File

@ -261,6 +261,21 @@ extension ComposeViewController {
.assign(to: \.isEnabled, on: composeToolbarView.pollButton) .assign(to: \.isEnabled, on: composeToolbarView.pollButton)
.store(in: &disposeBag) .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 // bind image picker toolbar state
viewModel.attachmentServices viewModel.attachmentServices
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
@ -271,6 +286,15 @@ extension ComposeViewController {
} }
.store(in: &disposeBag) .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 // bind visibility toolbar UI
Publishers.CombineLatest( Publishers.CombineLatest(
viewModel.selectedStatusVisibility, viewModel.selectedStatusVisibility,
@ -294,9 +318,11 @@ extension ComposeViewController {
case _ where count < 0: case _ where count < 0:
self.composeToolbarView.characterCountLabel.font = .systemFont(ofSize: 24, weight: .bold) self.composeToolbarView.characterCountLabel.font = .systemFont(ofSize: 24, weight: .bold)
self.composeToolbarView.characterCountLabel.textColor = Asset.Colors.danger.color self.composeToolbarView.characterCountLabel.textColor = Asset.Colors.danger.color
self.composeToolbarView.characterCountLabel.accessibilityLabel = L10n.Scene.Compose.Accessibility.inputLimitExceedsCount(abs(count))
default: default:
self.composeToolbarView.characterCountLabel.font = .systemFont(ofSize: 15, weight: .regular) self.composeToolbarView.characterCountLabel.font = .systemFont(ofSize: 15, weight: .regular)
self.composeToolbarView.characterCountLabel.textColor = Asset.Colors.Label.secondary.color self.composeToolbarView.characterCountLabel.textColor = Asset.Colors.Label.secondary.color
self.composeToolbarView.characterCountLabel.accessibilityLabel = L10n.Scene.Compose.Accessibility.inputLimitRemainsCount(count)
} }
} }
.store(in: &disposeBag) .store(in: &disposeBag)
@ -552,6 +578,7 @@ extension ComposeViewController {
collectionView.updateInteractiveMovementTargetPosition(position) collectionView.updateInteractiveMovementTargetPosition(position)
case .ended: case .ended:
collectionView.endInteractiveMovement() collectionView.endInteractiveMovement()
collectionView.reloadData()
default: default:
collectionView.cancelInteractiveMovement() collectionView.cancelInteractiveMovement()
} }

View File

@ -47,7 +47,7 @@ final class AttachmentContainerView: UIView {
textView.showsVerticalScrollIndicator = false textView.showsVerticalScrollIndicator = false
textView.backgroundColor = .clear textView.backgroundColor = .clear
textView.textColor = .white 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.placeholder = L10n.Scene.Compose.Attachment.descriptionPhoto
textView.placeholderColor = UIColor.white.withAlphaComponent(0.6) // force white with alpha for Light/Dark mode textView.placeholderColor = UIColor.white.withAlphaComponent(0.6) // force white with alpha for Light/Dark mode
textView.returnKeyType = .done textView.returnKeyType = .done

View File

@ -28,6 +28,7 @@ final class ComposeToolbarView: UIView {
let button = HighlightDimmableButton() let button = HighlightDimmableButton()
ComposeToolbarView.configureToolbarButtonAppearance(button: button) ComposeToolbarView.configureToolbarButtonAppearance(button: button)
button.setImage(UIImage(systemName: "photo", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal) button.setImage(UIImage(systemName: "photo", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal)
button.accessibilityLabel = L10n.Scene.Compose.Accessibility.appendAttachment
return button return button
}() }()
@ -35,6 +36,7 @@ final class ComposeToolbarView: UIView {
let button = HighlightDimmableButton(type: .custom) let button = HighlightDimmableButton(type: .custom)
ComposeToolbarView.configureToolbarButtonAppearance(button: button) ComposeToolbarView.configureToolbarButtonAppearance(button: button)
button.setImage(UIImage(systemName: "list.bullet", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .medium)), for: .normal) button.setImage(UIImage(systemName: "list.bullet", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .medium)), for: .normal)
button.accessibilityLabel = L10n.Scene.Compose.Accessibility.appendPoll
return button return button
}() }()
@ -45,6 +47,7 @@ final class ComposeToolbarView: UIView {
.af.imageScaled(to: CGSize(width: 20, height: 20)) .af.imageScaled(to: CGSize(width: 20, height: 20))
.withRenderingMode(.alwaysTemplate) .withRenderingMode(.alwaysTemplate)
button.setImage(image, for: .normal) button.setImage(image, for: .normal)
button.accessibilityLabel = L10n.Scene.Compose.Accessibility.customEmojiPicker
return button return button
}() }()
@ -52,6 +55,7 @@ final class ComposeToolbarView: UIView {
let button = HighlightDimmableButton() let button = HighlightDimmableButton()
ComposeToolbarView.configureToolbarButtonAppearance(button: button) ComposeToolbarView.configureToolbarButtonAppearance(button: button)
button.setImage(UIImage(systemName: "exclamationmark.shield", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal) button.setImage(UIImage(systemName: "exclamationmark.shield", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal)
button.accessibilityLabel = L10n.Scene.Compose.Accessibility.enableContentWarning
return button return button
}() }()
@ -59,6 +63,7 @@ final class ComposeToolbarView: UIView {
let button = HighlightDimmableButton() let button = HighlightDimmableButton()
ComposeToolbarView.configureToolbarButtonAppearance(button: button) ComposeToolbarView.configureToolbarButtonAppearance(button: button)
button.setImage(UIImage(systemName: "person.3", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium)), for: .normal) button.setImage(UIImage(systemName: "person.3", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium)), for: .normal)
button.accessibilityLabel = L10n.Scene.Compose.Accessibility.postVisibilityMenu
return button return button
}() }()
@ -67,6 +72,7 @@ final class ComposeToolbarView: UIView {
label.font = .systemFont(ofSize: 15, weight: .regular) label.font = .systemFont(ofSize: 15, weight: .regular)
label.text = "500" label.text = "500"
label.textColor = Asset.Colors.Label.secondary.color label.textColor = Asset.Colors.Label.secondary.color
label.accessibilityLabel = L10n.Scene.Compose.Accessibility.inputLimitRemainsCount(500)
return label return label
}() }()

View File

@ -25,10 +25,10 @@ class MainTabBarController: UITabBarController {
var title: String { var title: String {
switch self { switch self {
case .home: return "Home" case .home: return L10n.Common.Controls.Tabs.home
case .search: return "Search" case .search: return L10n.Common.Controls.Tabs.search
case .notification: return "Notification" case .notification: return L10n.Common.Controls.Tabs.notification
case .me: return "Me" case .me: return L10n.Common.Controls.Tabs.profile
} }
} }
@ -99,6 +99,7 @@ extension MainTabBarController {
let viewController = tab.viewController(context: context, coordinator: coordinator) 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.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.image = tab.image
viewController.tabBarItem.accessibilityLabel = tab.title
return viewController return viewController
} }
setViewControllers(viewControllers, animated: false) setViewControllers(viewControllers, animated: false)

View File

@ -36,7 +36,7 @@ final class MediaPreviewViewModel: NSObject {
switch entity.type { switch entity.type {
case .image: case .image:
guard let url = URL(string: entity.url) else { continue } 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 mediaPreviewImageModel = MediaPreviewImageViewModel(meta: meta)
let mediaPreviewImageViewController = MediaPreviewImageViewController() let mediaPreviewImageViewController = MediaPreviewImageViewController()
mediaPreviewImageViewController.viewModel = mediaPreviewImageModel mediaPreviewImageViewController.viewModel = mediaPreviewImageModel
@ -60,7 +60,7 @@ final class MediaPreviewViewModel: NSObject {
managedObjectContext.performAndWait { managedObjectContext.performAndWait {
let account = managedObjectContext.object(with: meta.accountObjectID) as! MastodonUser let account = managedObjectContext.object(with: meta.accountObjectID) as! MastodonUser
let avatarURL = account.headerImageURLWithFallback(domain: account.domain) 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 mediaPreviewImageModel = MediaPreviewImageViewModel(meta: meta)
let mediaPreviewImageViewController = MediaPreviewImageViewController() let mediaPreviewImageViewController = MediaPreviewImageViewController()
mediaPreviewImageViewController.viewModel = mediaPreviewImageModel mediaPreviewImageViewController.viewModel = mediaPreviewImageModel
@ -80,7 +80,7 @@ final class MediaPreviewViewModel: NSObject {
managedObjectContext.performAndWait { managedObjectContext.performAndWait {
let account = managedObjectContext.object(with: meta.accountObjectID) as! MastodonUser let account = managedObjectContext.object(with: meta.accountObjectID) as! MastodonUser
let avatarURL = account.avatarImageURLWithFallback(domain: account.domain) 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 mediaPreviewImageModel = MediaPreviewImageViewModel(meta: meta)
let mediaPreviewImageViewController = MediaPreviewImageViewController() let mediaPreviewImageViewController = MediaPreviewImageViewController()
mediaPreviewImageViewController.viewModel = mediaPreviewImageModel mediaPreviewImageViewController.viewModel = mediaPreviewImageModel

View File

@ -16,6 +16,9 @@ final class MediaPreviewImageView: UIScrollView {
imageView.contentMode = .scaleAspectFit imageView.contentMode = .scaleAspectFit
imageView.clipsToBounds = true imageView.clipsToBounds = true
imageView.isUserInteractionEnabled = true imageView.isUserInteractionEnabled = true
// accessibility
imageView.accessibilityIgnoresInvertColors = true
imageView.isAccessibilityElement = true
return imageView return imageView
}() }()

View File

@ -76,6 +76,7 @@ extension MediaPreviewImageViewController {
guard let image = image else { return } guard let image = image else { return }
self.previewImageView.imageView.image = image self.previewImageView.imageView.image = image
self.previewImageView.setup(image: image, container: self.previewImageView, forceUpdate: true) self.previewImageView.setup(image: image, container: self.previewImageView, forceUpdate: true)
self.previewImageView.imageView.accessibilityLabel = self.viewModel.altText
} }
.store(in: &disposeBag) .store(in: &disposeBag)
} }

View File

@ -17,10 +17,12 @@ class MediaPreviewImageViewModel {
// output // output
let image: CurrentValueSubject<UIImage?, Never> let image: CurrentValueSubject<UIImage?, Never>
let altText: String?
init(meta: RemoteImagePreviewMeta) { init(meta: RemoteImagePreviewMeta) {
self.item = .status(meta) self.item = .status(meta)
self.image = CurrentValueSubject(meta.thumbnail) self.image = CurrentValueSubject(meta.thumbnail)
self.altText = meta.altText
let url = meta.url let url = meta.url
ImageDownloader.default.download(URLRequest(url: url), completion: { [weak self] response in ImageDownloader.default.download(URLRequest(url: url), completion: { [weak self] response in
@ -38,6 +40,7 @@ class MediaPreviewImageViewModel {
init(meta: LocalImagePreviewMeta) { init(meta: LocalImagePreviewMeta) {
self.item = .local(meta) self.item = .local(meta)
self.image = CurrentValueSubject(meta.image) self.image = CurrentValueSubject(meta.image)
self.altText = nil
} }
} }
@ -64,6 +67,7 @@ extension MediaPreviewImageViewModel {
struct RemoteImagePreviewMeta { struct RemoteImagePreviewMeta {
let url: URL let url: URL
let thumbnail: UIImage? let thumbnail: UIImage?
let altText: String?
} }
struct LocalImagePreviewMeta { struct LocalImagePreviewMeta {

View File

@ -28,14 +28,14 @@ final class NotificationViewController: UIViewController, NeedsDependency {
let tableView: UITableView = { let tableView: UITableView = {
let tableView = ControlContainableTableView() let tableView = ControlContainableTableView()
tableView.rowHeight = UITableView.automaticDimension
tableView.separatorStyle = .singleLine
tableView.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) tableView.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
tableView.register(NotificationTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationTableViewCell.self)) tableView.register(NotificationTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationTableViewCell.self))
tableView.register(NotificationStatusTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationStatusTableViewCell.self)) tableView.register(NotificationStatusTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationStatusTableViewCell.self))
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
tableView.tableFooterView = UIView() tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = UITableView.automaticDimension tableView.estimatedRowHeight = UITableView.automaticDimension
tableView.separatorStyle = .none
tableView.tableFooterView = UIView()
tableView.backgroundColor = .clear tableView.backgroundColor = .clear
return tableView return tableView
}() }()

View File

@ -50,7 +50,7 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell {
let actionLabel: UILabel = { let actionLabel: UILabel = {
let label = UILabel() let label = UILabel()
label.textColor = Asset.Colors.Label.secondary.color 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 label.lineBreakMode = .byTruncatingTail
return label return label
}() }()
@ -58,7 +58,7 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell {
let nameLabel: UILabel = { let nameLabel: UILabel = {
let label = UILabel() let label = UILabel()
label.textColor = Asset.Colors.brandBlue.color 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 label.lineBreakMode = .byTruncatingTail
return label return label
}() }()
@ -76,6 +76,14 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell {
let statusView = StatusView() let statusView = StatusView()
let separatorLine = UIView.separatorLine
var separatorLineToEdgeLeadingLayoutConstraint: NSLayoutConstraint!
var separatorLineToEdgeTrailingLayoutConstraint: NSLayoutConstraint!
var separatorLineToMarginLeadingLayoutConstraint: NSLayoutConstraint!
var separatorLineToMarginTrailingLayoutConstraint: NSLayoutConstraint!
override func prepareForReuse() { override func prepareForReuse() {
super.prepareForReuse() super.prepareForReuse()
avatatImageView.af.cancelImageRequest() avatatImageView.af.cancelImageRequest()
@ -197,6 +205,18 @@ extension NotificationStatusTableViewCell {
containerStackView.addArrangedSubview(statusStackView) 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 // remove item don't display
statusView.actionToolbarContainer.removeFromStackView() statusView.actionToolbarContainer.removeFromStackView()
// it affect stackView's height,need remove // it affect stackView's height,need remove
@ -206,6 +226,8 @@ extension NotificationStatusTableViewCell {
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection) super.traitCollectionDidChange(previousTraitCollection)
resetSeparatorLineLayout()
statusBorder.layer.borderColor = Asset.Colors.Border.notification.color.cgColor statusBorder.layer.borderColor = Asset.Colors.Border.notification.color.cgColor
actionImageBackground.layer.borderColor = Asset.Colors.Background.systemBackground.color.cgColor actionImageBackground.layer.borderColor = Asset.Colors.Background.systemBackground.color.cgColor
} }
@ -258,5 +280,37 @@ extension NotificationStatusTableViewCell: StatusViewDelegate {
// do nothing // 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,
])
}
}
}
} }

View File

@ -67,7 +67,7 @@ final class NotificationTableViewCell: UITableViewCell {
let actionLabel: UILabel = { let actionLabel: UILabel = {
let label = UILabel() let label = UILabel()
label.textColor = Asset.Colors.Label.secondary.color 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 label.lineBreakMode = .byTruncatingTail
return label return label
}() }()
@ -75,7 +75,7 @@ final class NotificationTableViewCell: UITableViewCell {
let nameLabel: UILabel = { let nameLabel: UILabel = {
let label = UILabel() let label = UILabel()
label.textColor = Asset.Colors.brandBlue.color 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 label.lineBreakMode = .byTruncatingTail
return label return label
}() }()
@ -98,6 +98,14 @@ final class NotificationTableViewCell: UITableViewCell {
let buttonStackView = UIStackView() let buttonStackView = UIStackView()
let separatorLine = UIView.separatorLine
var separatorLineToEdgeLeadingLayoutConstraint: NSLayoutConstraint!
var separatorLineToEdgeTrailingLayoutConstraint: NSLayoutConstraint!
var separatorLineToMarginLeadingLayoutConstraint: NSLayoutConstraint!
var separatorLineToMarginTrailingLayoutConstraint: NSLayoutConstraint!
override func prepareForReuse() { override func prepareForReuse() {
super.prepareForReuse() super.prepareForReuse()
avatatImageView.af.cancelImageRequest() avatatImageView.af.cancelImageRequest()
@ -187,10 +195,57 @@ extension NotificationTableViewCell {
buttonStackView.addArrangedSubview(acceptButton) buttonStackView.addArrangedSubview(acceptButton)
buttonStackView.addArrangedSubview(rejectButton) buttonStackView.addArrangedSubview(rejectButton)
containerStackView.addArrangedSubview(buttonStackView) 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?) { override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection) super.traitCollectionDidChange(previousTraitCollection)
actionImageBackground.layer.borderColor = Asset.Colors.Background.systemBackground.color.cgColor 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,
])
}
}
}
}

View File

@ -31,6 +31,7 @@ final class MastodonPickServerViewController: UIViewController, NeedsDependency
tableView.register(PickServerCategoriesCell.self, forCellReuseIdentifier: String(describing: PickServerCategoriesCell.self)) tableView.register(PickServerCategoriesCell.self, forCellReuseIdentifier: String(describing: PickServerCategoriesCell.self))
tableView.register(PickServerSearchCell.self, forCellReuseIdentifier: String(describing: PickServerSearchCell.self)) tableView.register(PickServerSearchCell.self, forCellReuseIdentifier: String(describing: PickServerSearchCell.self))
tableView.register(PickServerCell.self, forCellReuseIdentifier: String(describing: PickServerCell.self)) tableView.register(PickServerCell.self, forCellReuseIdentifier: String(describing: PickServerCell.self))
tableView.register(PickServerLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: PickServerLoaderTableViewCell.self))
tableView.rowHeight = UITableView.automaticDimension tableView.rowHeight = UITableView.automaticDimension
tableView.separatorStyle = .none tableView.separatorStyle = .none
tableView.backgroundColor = .clear tableView.backgroundColor = .clear

View File

@ -39,7 +39,7 @@ class MastodonPickServerViewModel: NSObject {
let selectCategoryItem = CurrentValueSubject<CategoryPickerItem, Never>(.all) let selectCategoryItem = CurrentValueSubject<CategoryPickerItem, Never>(.all)
let searchText = CurrentValueSubject<String, Never>("") let searchText = CurrentValueSubject<String, Never>("")
let indexedServers = CurrentValueSubject<[Mastodon.Entity.Server], Never>([]) 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<Void, Never>() let viewWillAppear = PassthroughSubject<Void, Never>()
// output // output
@ -85,8 +85,8 @@ extension MastodonPickServerViewModel {
private func configure() { private func configure() {
Publishers.CombineLatest( Publishers.CombineLatest(
filteredIndexedServers.eraseToAnyPublisher(), filteredIndexedServers,
unindexedServers.eraseToAnyPublisher() unindexedServers
) )
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] indexedServers, unindexedServers in .sink(receiveValue: { [weak self] indexedServers, unindexedServers in
@ -114,6 +114,9 @@ extension MastodonPickServerViewModel {
guard !serverItems.contains(item) else { continue } guard !serverItems.contains(item) else { continue }
serverItems.append(item) serverItems.append(item)
} }
if let unindexedServers = unindexedServers {
if !unindexedServers.isEmpty {
for server in unindexedServers { for server in unindexedServers {
let attribute = oldSnapshotServerItemAttributeDict[server.domain] ?? PickServerItem.ServerItemAttribute(isLast: false, isExpand: false) let attribute = oldSnapshotServerItemAttributeDict[server.domain] ?? PickServerItem.ServerItemAttribute(isLast: false, isExpand: false)
attribute.isLast = false attribute.isLast = false
@ -121,9 +124,21 @@ extension MastodonPickServerViewModel {
guard !serverItems.contains(item) else { continue } guard !serverItems.contains(item) else { continue }
serverItems.append(item) 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 { if case let .server(_, attribute) = serverItems.last {
attribute.isLast = true attribute.isLast = true
} }
if case let .loader(attribute) = serverItems.last {
attribute.isLast = true
}
snapshot.appendItems(serverItems, toSection: .servers) snapshot.appendItems(serverItems, toSection: .servers)
diffableDataSource.defaultRowAnimation = .fade diffableDataSource.defaultRowAnimation = .fade
@ -168,6 +183,7 @@ extension MastodonPickServerViewModel {
guard let domain = AuthenticationViewModel.parseDomain(from: searchText) else { guard let domain = AuthenticationViewModel.parseDomain(from: searchText) else {
return Just(Result.failure(APIService.APIError.implicit(.badRequest))).eraseToAnyPublisher() return Just(Result.failure(APIService.APIError.implicit(.badRequest))).eraseToAnyPublisher()
} }
self.unindexedServers.value = nil
return self.context.apiService.instance(domain: domain) return self.context.apiService.instance(domain: domain)
.map { response -> Result<Mastodon.Response.Content<[Mastodon.Entity.Server]>, Error>in .map { response -> Result<Mastodon.Response.Content<[Mastodon.Entity.Server]>, Error>in
let newResponse = response.map { [Mastodon.Entity.Server(instance: $0)] } let newResponse = response.map { [Mastodon.Entity.Server(instance: $0)] }
@ -184,9 +200,14 @@ extension MastodonPickServerViewModel {
switch result { switch result {
case .success(let response): case .success(let response):
self.unindexedServers.send(response.value) self.unindexedServers.send(response.value)
case .failure: case .failure(let error):
// TODO: What should be presented when user inputs invalid search text? if let error = error as? APIService.APIError,
case let .implicit(reason) = error,
case .badRequest = reason {
self.unindexedServers.send([]) self.unindexedServers.send([])
} else {
self.unindexedServers.send(nil)
}
} }
}) })
.store(in: &disposeBag) .store(in: &disposeBag)

View File

@ -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
}
}

View File

@ -34,7 +34,7 @@ class PickServerCell: UITableViewCell {
let domainLabel: UILabel = { let domainLabel: UILabel = {
let label = 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.textColor = Asset.Colors.Label.primary.color
label.adjustsFontForContentSizeCategory = true label.adjustsFontForContentSizeCategory = true
label.translatesAutoresizingMaskIntoConstraints = false label.translatesAutoresizingMaskIntoConstraints = false
@ -52,7 +52,7 @@ class PickServerCell: UITableViewCell {
let descriptionLabel: UILabel = { let descriptionLabel: UILabel = {
let label = 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.numberOfLines = 0
label.textColor = Asset.Colors.Label.primary.color label.textColor = Asset.Colors.Label.primary.color
label.adjustsFontForContentSizeCategory = true label.adjustsFontForContentSizeCategory = true
@ -88,11 +88,14 @@ class PickServerCell: UITableViewCell {
let expandButton: UIButton = { let expandButton: UIButton = {
let button = UIButton(type: .custom) 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.seeMore, for: .normal)
button.setTitle(L10n.Scene.ServerPicker.Button.seeLess, for: .selected)
button.setTitleColor(Asset.Colors.brandBlue.color, for: .normal) 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.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 return button
}() }()
@ -106,7 +109,7 @@ class PickServerCell: UITableViewCell {
let langValueLabel: UILabel = { let langValueLabel: UILabel = {
let label = UILabel() let label = UILabel()
label.textColor = Asset.Colors.Label.primary.color 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.textAlignment = .center
label.adjustsFontForContentSizeCategory = true label.adjustsFontForContentSizeCategory = true
label.translatesAutoresizingMaskIntoConstraints = false label.translatesAutoresizingMaskIntoConstraints = false
@ -116,7 +119,7 @@ class PickServerCell: UITableViewCell {
let usersValueLabel: UILabel = { let usersValueLabel: UILabel = {
let label = UILabel() let label = UILabel()
label.textColor = Asset.Colors.Label.primary.color 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.textAlignment = .center
label.adjustsFontForContentSizeCategory = true label.adjustsFontForContentSizeCategory = true
label.translatesAutoresizingMaskIntoConstraints = false label.translatesAutoresizingMaskIntoConstraints = false
@ -126,7 +129,7 @@ class PickServerCell: UITableViewCell {
let categoryValueLabel: UILabel = { let categoryValueLabel: UILabel = {
let label = UILabel() let label = UILabel()
label.textColor = Asset.Colors.Label.primary.color 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.textAlignment = .center
label.adjustsFontForContentSizeCategory = true label.adjustsFontForContentSizeCategory = true
label.translatesAutoresizingMaskIntoConstraints = false label.translatesAutoresizingMaskIntoConstraints = false
@ -136,7 +139,7 @@ class PickServerCell: UITableViewCell {
let langTitleLabel: UILabel = { let langTitleLabel: UILabel = {
let label = UILabel() let label = UILabel()
label.textColor = Asset.Colors.Label.primary.color 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.text = L10n.Scene.ServerPicker.Label.language
label.textAlignment = .center label.textAlignment = .center
label.adjustsFontForContentSizeCategory = true label.adjustsFontForContentSizeCategory = true
@ -147,7 +150,7 @@ class PickServerCell: UITableViewCell {
let usersTitleLabel: UILabel = { let usersTitleLabel: UILabel = {
let label = UILabel() let label = UILabel()
label.textColor = Asset.Colors.Label.primary.color 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.text = L10n.Scene.ServerPicker.Label.users
label.textAlignment = .center label.textAlignment = .center
label.adjustsFontForContentSizeCategory = true label.adjustsFontForContentSizeCategory = true
@ -158,7 +161,7 @@ class PickServerCell: UITableViewCell {
let categoryTitleLabel: UILabel = { let categoryTitleLabel: UILabel = {
let label = UILabel() let label = UILabel()
label.textColor = Asset.Colors.Label.primary.color 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.text = L10n.Scene.ServerPicker.Label.category
label.textAlignment = .center label.textAlignment = .center
label.adjustsFontForContentSizeCategory = true label.adjustsFontForContentSizeCategory = true
@ -325,11 +328,15 @@ extension PickServerCell {
func updateExpandMode(mode: ExpandMode) { func updateExpandMode(mode: ExpandMode) {
switch mode { switch mode {
case .collapse: 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 expandBox.isHidden = true
expandButton.isSelected = false expandButton.isSelected = false
NSLayoutConstraint.deactivate(expandConstraints) NSLayoutConstraint.deactivate(expandConstraints)
NSLayoutConstraint.activate(collapseConstraints) NSLayoutConstraint.activate(collapseConstraints)
case .expand: 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 expandBox.isHidden = false
expandButton.isSelected = true expandButton.isSelected = true
NSLayoutConstraint.activate(expandConstraints) NSLayoutConstraint.activate(expandConstraints)

View File

@ -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

View File

@ -16,6 +16,9 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
static let avatarImageMaxSizeInPixel = CGSize(width: 400, height: 400) 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<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } 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.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: UIFont.boldSystemFont(ofSize: 34))
label.textColor = Asset.Colors.Label.primary.color label.textColor = Asset.Colors.Label.primary.color
label.text = L10n.Scene.Register.title label.text = L10n.Scene.Register.title
label.numberOfLines = 0
return label return label
}() }()
@ -99,7 +103,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
let domainLabel: UILabel = { let domainLabel: UILabel = {
let label = UILabel() let label = UILabel()
label.font = .preferredFont(forTextStyle: .headline) label.font = MastodonRegisterViewController.textFieldLabelFont
label.textColor = Asset.Colors.Label.primary.color label.textColor = Asset.Colors.Label.primary.color
return label return label
}() }()
@ -113,7 +117,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
textField.textColor = Asset.Colors.Label.primary.color textField.textColor = Asset.Colors.Label.primary.color
textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Username.placeholder, textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Username.placeholder,
attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color, 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 textField.borderStyle = UITextField.BorderStyle.roundedRect
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height))
textField.leftView = paddingView textField.leftView = paddingView
@ -124,7 +128,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
let usernameErrorPromptLabel: UILabel = { let usernameErrorPromptLabel: UILabel = {
let label = UILabel() let label = UILabel()
let color = Asset.Colors.danger.color let color = Asset.Colors.danger.color
let font = UIFont.preferredFont(forTextStyle: .caption1) let font = MastodonRegisterViewController.errorPromptLabelFont
return label return label
}() }()
@ -136,7 +140,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
textField.textColor = Asset.Colors.Label.primary.color textField.textColor = Asset.Colors.Label.primary.color
textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.DisplayName.placeholder, textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.DisplayName.placeholder,
attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color, 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 textField.borderStyle = UITextField.BorderStyle.roundedRect
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height))
textField.leftView = paddingView textField.leftView = paddingView
@ -153,7 +157,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
textField.textColor = Asset.Colors.Label.primary.color textField.textColor = Asset.Colors.Label.primary.color
textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Email.placeholder, textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Email.placeholder,
attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color, 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 textField.borderStyle = UITextField.BorderStyle.roundedRect
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height))
textField.leftView = paddingView textField.leftView = paddingView
@ -164,7 +168,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
let emailErrorPromptLabel: UILabel = { let emailErrorPromptLabel: UILabel = {
let label = UILabel() let label = UILabel()
let color = Asset.Colors.danger.color let color = Asset.Colors.danger.color
let font = UIFont.preferredFont(forTextStyle: .caption1) let font = MastodonRegisterViewController.errorPromptLabelFont
return label return label
}() }()
@ -178,7 +182,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
textField.textColor = Asset.Colors.Label.primary.color textField.textColor = Asset.Colors.Label.primary.color
textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Password.placeholder, textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Password.placeholder,
attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color, 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 textField.borderStyle = UITextField.BorderStyle.roundedRect
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height))
textField.leftView = paddingView textField.leftView = paddingView
@ -195,7 +199,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
let passwordErrorPromptLabel: UILabel = { let passwordErrorPromptLabel: UILabel = {
let label = UILabel() let label = UILabel()
let color = Asset.Colors.danger.color let color = Asset.Colors.danger.color
let font = UIFont.preferredFont(forTextStyle: .caption1) let font = MastodonRegisterViewController.errorPromptLabelFont
return label return label
}() }()
@ -208,7 +212,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
textField.textColor = Asset.Colors.Label.primary.color textField.textColor = Asset.Colors.Label.primary.color
textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Invite.registrationUserInviteRequest, textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Invite.registrationUserInviteRequest,
attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color, 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 textField.borderStyle = UITextField.BorderStyle.roundedRect
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height))
textField.leftView = paddingView textField.leftView = paddingView
@ -219,7 +223,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
let reasonErrorPromptLabel: UILabel = { let reasonErrorPromptLabel: UILabel = {
let label = UILabel() let label = UILabel()
let color = Asset.Colors.danger.color let color = Asset.Colors.danger.color
let font = UIFont.preferredFont(forTextStyle: .caption1) let font = MastodonRegisterViewController.errorPromptLabelFont
return label return label
}() }()

View File

@ -223,7 +223,7 @@ extension MastodonRegisterViewModel {
} }
static func attributeStringForPassword(validateState: ValidateState) -> NSAttributedString { 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 attributeString = NSMutableAttributedString()
let image = MastodonRegisterViewModel.checkmarkImage(font: font) let image = MastodonRegisterViewModel.checkmarkImage(font: font)
@ -236,7 +236,7 @@ extension MastodonRegisterViewModel {
} }
static func errorPromptAttributedString(for prompt: String) -> NSAttributedString { 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 attributeString = NSMutableAttributedString()
let image = MastodonRegisterViewModel.xmarkImage(font: font) let image = MastodonRegisterViewModel.xmarkImage(font: font)

View File

@ -25,6 +25,7 @@ final class MastodonServerRulesViewController: UIViewController, NeedsDependency
label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: .systemFont(ofSize: 34, weight: .bold)) label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: .systemFont(ofSize: 34, weight: .bold))
label.textColor = .label label.textColor = .label
label.text = L10n.Scene.ServerRules.title label.text = L10n.Scene.ServerRules.title
label.numberOfLines = 0
return label return label
}() }()
@ -54,7 +55,7 @@ final class MastodonServerRulesViewController: UIViewController, NeedsDependency
private(set) lazy var bottomPromptTextView: UITextView = { private(set) lazy var bottomPromptTextView: UITextView = {
let textView = 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.textColor = .label
textView.isSelectable = true textView.isSelectable = true
textView.isEditable = false textView.isEditable = false
@ -181,7 +182,7 @@ extension MastodonServerRulesViewController {
let str = NSString(string: L10n.Scene.ServerRules.prompt(viewModel.domain)) let str = NSString(string: L10n.Scene.ServerRules.prompt(viewModel.domain))
let termsOfServiceRange = str.range(of: L10n.Scene.ServerRules.termsOfService) let termsOfServiceRange = str.range(of: L10n.Scene.ServerRules.termsOfService)
let privacyRange = str.range(of: L10n.Scene.ServerRules.privacyPolicy) 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.serverRulesURL(domain: viewModel.domain), range: termsOfServiceRange)
attributeString.addAttribute(.link, value: Mastodon.API.privacyURL(domain: viewModel.domain), range: privacyRange) attributeString.addAttribute(.link, value: Mastodon.API.privacyURL(domain: viewModel.domain), range: privacyRange)
let linkAttributes = [NSAttributedString.Key.foregroundColor:linkColor] let linkAttributes = [NSAttributedString.Key.foregroundColor:linkColor]

View File

@ -45,9 +45,8 @@ final class ProfileHeaderView: UIView {
imageView.backgroundColor = ProfileHeaderView.bannerImageViewPlaceholderColor imageView.backgroundColor = ProfileHeaderView.bannerImageViewPlaceholderColor
imageView.layer.masksToBounds = true imageView.layer.masksToBounds = true
imageView.isUserInteractionEnabled = true imageView.isUserInteractionEnabled = true
// #if DEBUG // accessibility
// imageView.image = .placeholder(color: .red) imageView.accessibilityIgnoresInvertColors = true
// #endif
return imageView return imageView
}() }()
let bannerImageViewOverlayView: UIView = { let bannerImageViewOverlayView: UIView = {
@ -101,7 +100,7 @@ final class ProfileHeaderView: UIView {
let nameTextField: UITextField = { let nameTextField: UITextField = {
let textField = 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.textColor = .white
textField.text = "Alice" textField.text = "Alice"
textField.autocorrectionType = .no textField.autocorrectionType = .no
@ -112,7 +111,7 @@ final class ProfileHeaderView: UIView {
let usernameLabel: UILabel = { let usernameLabel: UILabel = {
let label = 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.adjustsFontSizeToFitWidth = true
label.minimumScaleFactor = 0.5 label.minimumScaleFactor = 0.5
label.textColor = Asset.Scene.Profile.Banner.usernameGray.color label.textColor = Asset.Scene.Profile.Banner.usernameGray.color

View File

@ -479,6 +479,8 @@ extension ProfileViewController {
guard let self = self else { return } guard let self = self else { return }
let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-" let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-"
self.profileHeaderViewController.profileHeaderView.statusDashboardView.postDashboardMeterView.numberLabel.text = text 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) .store(in: &disposeBag)
viewModel.followingCount viewModel.followingCount
@ -486,6 +488,8 @@ extension ProfileViewController {
guard let self = self else { return } guard let self = self else { return }
let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-" let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-"
self.profileHeaderViewController.profileHeaderView.statusDashboardView.followingDashboardMeterView.numberLabel.text = text 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) .store(in: &disposeBag)
viewModel.followersCount viewModel.followersCount
@ -493,6 +497,8 @@ extension ProfileViewController {
guard let self = self else { return } guard let self = self else { return }
let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-" let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-"
self.profileHeaderViewController.profileHeaderView.statusDashboardView.followersDashboardMeterView.numberLabel.text = text 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) .store(in: &disposeBag)

View File

@ -32,6 +32,7 @@ extension UserTimelineViewModel {
// set empty section to make update animation top-to-bottom style // set empty section to make update animation top-to-bottom style
var snapshot = NSDiffableDataSourceSnapshot<StatusSection, Item>() var snapshot = NSDiffableDataSourceSnapshot<StatusSection, Item>()
snapshot.appendSections([.main]) snapshot.appendSections([.main])
snapshot.appendItems([.bottomLoader], toSection: .main)
diffableDataSource?.apply(snapshot) diffableDataSource?.apply(snapshot)
} }

View File

@ -51,23 +51,27 @@ class SettingsViewController: UIViewController, NeedsDependency {
return menu return menu
} }
private(set) lazy var notifySectionHeader: UIView = { private let notifySectionHeaderStackView: UIStackView = {
let view = UIStackView() let view = UIStackView()
view.translatesAutoresizingMaskIntoConstraints = false view.translatesAutoresizingMaskIntoConstraints = false
view.isLayoutMarginsRelativeArrangement = true view.isLayoutMarginsRelativeArrangement = true
//view.layoutMargins = UIEdgeInsets(top: 15, left: 4, bottom: 5, right: 4)
view.axis = .horizontal view.axis = .horizontal
view.alignment = .fill
view.distribution = .equalSpacing
view.spacing = 4 view.spacing = 4
return view
}()
let notifyLabel = UILabel() let notifyLabel = UILabel()
private(set) lazy var notifySectionHeader: UIView = {
let view = notifySectionHeaderStackView
notifyLabel.translatesAutoresizingMaskIntoConstraints = false 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.textColor = Asset.Colors.Label.primary.color
notifyLabel.text = L10n.Scene.Settings.Section.Notifications.Trigger.title notifyLabel.text = L10n.Scene.Settings.Section.Notifications.Trigger.title
view.addArrangedSubview(notifyLabel) view.addArrangedSubview(notifyLabel)
view.addArrangedSubview(whoButton) view.addArrangedSubview(whoButton)
whoButton.setContentHuggingPriority(.defaultHigh + 1, for: .horizontal)
whoButton.setContentHuggingPriority(.defaultHigh + 1, for: .vertical)
return view return view
}() }()
@ -77,6 +81,7 @@ class SettingsViewController: UIViewController, NeedsDependency {
whoButton.showsMenuAsPrimaryAction = true whoButton.showsMenuAsPrimaryAction = true
whoButton.setBackgroundColor(Asset.Colors.battleshipGrey.color, for: .normal) whoButton.setBackgroundColor(Asset.Colors.battleshipGrey.color, for: .normal)
whoButton.setTitleColor(Asset.Colors.Label.primary.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.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.contentEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
whoButton.layer.cornerRadius = 10 whoButton.layer.cornerRadius = 10
@ -107,6 +112,7 @@ class SettingsViewController: UIViewController, NeedsDependency {
view.alignment = .center view.alignment = .center
let label = ActiveLabel(style: .default) let label = ActiveLabel(style: .default)
label.adjustsFontForContentSizeCategory = true
label.textAlignment = .center label.textAlignment = .center
label.configure(content: "Mastodon is open source software. You can contribute or report issues on GitHub at <a href=\"https://github.com/tootsuite/mastodon\">tootsuite/mastodon</a> (v3.3.0).", emojiDict: [:]) label.configure(content: "Mastodon is open source software. You can contribute or report issues on GitHub at <a href=\"https://github.com/tootsuite/mastodon\">tootsuite/mastodon</a> (v3.3.0).", emojiDict: [:])
label.delegate = self label.delegate = self
@ -138,7 +144,25 @@ class SettingsViewController: UIViewController, NeedsDependency {
} }
} }
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
updateSectionHeaderStackViewLayout()
}
// MAKR: - Private methods // 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() { private func bindViewModel() {
self.whoButton.setTitle(viewModel.setting.value.activeSubscription?.policy.title, for: .normal) self.whoButton.setTitle(viewModel.setting.value.activeSubscription?.policy.title, for: .normal)
viewModel.setting viewModel.setting
@ -173,6 +197,8 @@ class SettingsViewController: UIViewController, NeedsDependency {
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
]) ])
setupTableView() setupTableView()
updateSectionHeaderStackViewLayout()
} }
private func setupNavigation() { private func setupNavigation() {

View File

@ -15,6 +15,8 @@ protocol SettingsAppearanceTableViewCellDelegate: AnyObject {
class AppearanceView: UIView { class AppearanceView: UIView {
lazy var imageView: UIImageView = { lazy var imageView: UIImageView = {
let view = UIImageView() let view = UIImageView()
// accessibility
view.accessibilityIgnoresInvertColors = true
return view return view
}() }()
lazy var titleLabel: UILabel = { lazy var titleLabel: UILabel = {

View File

@ -31,6 +31,7 @@ final class MosaicImageViewContainer: UIView {
let tapGesture = UITapGestureRecognizer.singleTapGestureRecognizer let tapGesture = UITapGestureRecognizer.singleTapGestureRecognizer
tapGesture.addTarget(self, action: #selector(MosaicImageViewContainer.photoTapGestureRecognizerHandler(_:))) tapGesture.addTarget(self, action: #selector(MosaicImageViewContainer.photoTapGestureRecognizerHandler(_:)))
imageView.addGestureRecognizer(tapGesture) imageView.addGestureRecognizer(tapGesture)
imageView.isAccessibilityElement = true
} }
} }
} }
@ -65,6 +66,9 @@ extension MosaicImageViewContainer: ContentWarningOverlayViewDelegate {
extension MosaicImageViewContainer { extension MosaicImageViewContainer {
private func _init() { private func _init() {
// accessibility
accessibilityIgnoresInvertColors = true
container.translatesAutoresizingMaskIntoConstraints = false container.translatesAutoresizingMaskIntoConstraints = false
container.axis = .horizontal container.axis = .horizontal
container.distribution = .fillEqually container.distribution = .fillEqually

View File

@ -46,6 +46,9 @@ final class PlayerContainerView: UIView {
extension PlayerContainerView { extension PlayerContainerView {
private func _init() { private func _init() {
// accessibility
accessibilityIgnoresInvertColors = true
container.translatesAutoresizingMaskIntoConstraints = false container.translatesAutoresizingMaskIntoConstraints = false
addSubview(container) addSubview(container)
containerHeightLayoutConstraint = container.heightAnchor.constraint(equalToConstant: 162).priority(.required - 1) 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), 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 mediaTypeIndicotorViewInContentWarningOverlay.translatesAutoresizingMaskIntoConstraints = false
contentWarningOverlayView.addSubview(mediaTypeIndicotorViewInContentWarningOverlay) contentWarningOverlayView.addSubview(mediaTypeIndicotorViewInContentWarningOverlay)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
@ -90,6 +83,8 @@ extension PlayerContainerView {
mediaTypeIndicotorViewInContentWarningOverlay.heightAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.height).priority(.required - 1), mediaTypeIndicotorViewInContentWarningOverlay.heightAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.height).priority(.required - 1),
mediaTypeIndicotorViewInContentWarningOverlay.widthAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.width).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.constant = floor(rect.height)
containerHeightLayoutConstraint.isActive = true 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) bringSubviewToFront(mediaTypeIndicotorView)
return playerViewController return playerViewController

View File

@ -26,6 +26,7 @@ class ContentWarningOverlayView: UIView {
label.text = L10n.Common.Controls.Status.mediaContentWarning label.text = L10n.Common.Controls.Status.mediaContentWarning
label.textAlignment = .center label.textAlignment = .center
label.numberOfLines = 0 label.numberOfLines = 0
label.isAccessibilityElement = false
return label return label
}() }()
@ -36,19 +37,21 @@ class ContentWarningOverlayView: UIView {
}() }()
let blurContentWarningTitleLabel: UILabel = { let blurContentWarningTitleLabel: UILabel = {
let label = 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.text = L10n.Common.Controls.Status.mediaContentWarning
label.textColor = Asset.Colors.Label.primary.color label.textColor = Asset.Colors.Label.primary.color
label.textAlignment = .center label.textAlignment = .center
label.isAccessibilityElement = false
return label return label
}() }()
let blurContentWarningLabel: UILabel = { let blurContentWarningLabel: UILabel = {
let label = 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.text = L10n.Common.Controls.Status.mediaContentWarning
label.textColor = Asset.Colors.Label.secondary.color label.textColor = Asset.Colors.Label.secondary.color
label.textAlignment = .center label.textAlignment = .center
label.layer.setupShadow() label.layer.setupShadow()
label.isAccessibilityElement = false
return label return label
}() }()

View File

@ -75,7 +75,13 @@ final class StatusView: UIView {
return label 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 avatarButton: UIButton = {
let button = HighlightDimmableButton(type: .custom) let button = HighlightDimmableButton(type: .custom)
let placeholderImage = UIImage.placeholder(size: avatarImageSize, color: .systemFill) let placeholderImage = UIImage.placeholder(size: avatarImageSize, color: .systemFill)
@ -96,6 +102,7 @@ final class StatusView: UIView {
label.textColor = Asset.Colors.Label.secondary.color label.textColor = Asset.Colors.Label.secondary.color
label.font = .systemFont(ofSize: 17) label.font = .systemFont(ofSize: 17)
label.text = "·" label.text = "·"
label.isAccessibilityElement = false
return label return label
}() }()
@ -104,6 +111,7 @@ final class StatusView: UIView {
label.font = .systemFont(ofSize: 15, weight: .regular) label.font = .systemFont(ofSize: 15, weight: .regular)
label.textColor = Asset.Colors.Label.secondary.color label.textColor = Asset.Colors.Label.secondary.color
label.text = "@alice" label.text = "@alice"
label.isAccessibilityElement = false
return label return label
}() }()
@ -295,7 +303,7 @@ extension StatusView {
authorMetaContainerStackView.axis = .vertical authorMetaContainerStackView.axis = .vertical
authorMetaContainerStackView.spacing = 4 authorMetaContainerStackView.spacing = 4
// title container: [display name | "·" | date] // title container: [display name | "·" | date | padding | visibility]
let titleContainerStackView = UIStackView() let titleContainerStackView = UIStackView()
authorMetaContainerStackView.addArrangedSubview(titleContainerStackView) authorMetaContainerStackView.addArrangedSubview(titleContainerStackView)
titleContainerStackView.axis = .horizontal titleContainerStackView.axis = .horizontal
@ -308,11 +316,14 @@ extension StatusView {
titleContainerStackView.alignment = .firstBaseline titleContainerStackView.alignment = .firstBaseline
titleContainerStackView.addArrangedSubview(nameTrialingDotLabel) titleContainerStackView.addArrangedSubview(nameTrialingDotLabel)
titleContainerStackView.addArrangedSubview(dateLabel) titleContainerStackView.addArrangedSubview(dateLabel)
titleContainerStackView.addArrangedSubview(UIView()) // padding
titleContainerStackView.addArrangedSubview(visibilityImageView)
nameLabel.setContentHuggingPriority(.defaultHigh + 1, for: .horizontal) nameLabel.setContentHuggingPriority(.defaultHigh + 1, for: .horizontal)
nameTrialingDotLabel.setContentHuggingPriority(.defaultHigh + 2, for: .horizontal) nameTrialingDotLabel.setContentHuggingPriority(.defaultHigh + 2, for: .horizontal)
nameTrialingDotLabel.setContentCompressionResistancePriority(.required - 2, for: .horizontal) nameTrialingDotLabel.setContentCompressionResistancePriority(.required - 2, for: .horizontal)
dateLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) dateLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
dateLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal) dateLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
visibilityImageView.setContentHuggingPriority(.defaultHigh + 3, for: .horizontal)
// subtitle container: [username] // subtitle container: [username]
let subtitleContainerStackView = UIStackView() let subtitleContainerStackView = UIStackView()
@ -324,10 +335,6 @@ extension StatusView {
authorContainerStackView.addArrangedSubview(revealContentWarningButton) authorContainerStackView.addArrangedSubview(revealContentWarningButton)
revealContentWarningButton.setContentHuggingPriority(.required - 2, for: .horizontal) revealContentWarningButton.setContentHuggingPriority(.required - 2, for: .horizontal)
// visibility ImageView
authorContainerStackView.addArrangedSubview(visibilityImageView)
visibilityImageView.setContentHuggingPriority(.required - 2, for: .horizontal)
authorContainerStackView.translatesAutoresizingMaskIntoConstraints = false authorContainerStackView.translatesAutoresizingMaskIntoConstraints = false
authorContainerView.addSubview(authorContainerStackView) authorContainerView.addSubview(authorContainerStackView)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
@ -353,7 +360,7 @@ extension StatusView {
// only layout to top and left & right then draw image to fit size // only layout to top and left & right then draw image to fit size
]) ])
// avoid overlay clip author view // avoid overlay clip author view
containerStackView.bringSubviewToFront(authorContainerStackView) containerStackView.bringSubviewToFront(authorContainerView)
// status // status
statusContainerStackView.addArrangedSubview(activeTextLabel) statusContainerStackView.addArrangedSubview(activeTextLabel)

View File

@ -34,6 +34,14 @@ final class ThreadMetaView: UIView {
return button return button
}() }()
let containerStackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.spacing = 20
return stackView
}()
let actionButtonStackView = UIStackView()
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
_init() _init()
@ -48,27 +56,53 @@ final class ThreadMetaView: UIView {
extension ThreadMetaView { extension ThreadMetaView {
private func _init() { private func _init() {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.spacing = 20
stackView.translatesAutoresizingMaskIntoConstraints = false containerStackView.translatesAutoresizingMaskIntoConstraints = false
addSubview(stackView) addSubview(containerStackView)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: topAnchor, constant: 8), containerStackView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
stackView.leadingAnchor.constraint(equalTo: leadingAnchor), containerStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: trailingAnchor), containerStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
bottomAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 12), bottomAnchor.constraint(equalTo: containerStackView.bottomAnchor, constant: 12),
]) ])
stackView.addArrangedSubview(dateLabel) containerStackView.addArrangedSubview(dateLabel)
stackView.addArrangedSubview(reblogButton) containerStackView.addArrangedSubview(actionButtonStackView)
stackView.addArrangedSubview(favoriteButton)
actionButtonStackView.axis = .horizontal
actionButtonStackView.spacing = 20
actionButtonStackView.addArrangedSubview(reblogButton)
actionButtonStackView.addArrangedSubview(favoriteButton)
dateLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) dateLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
reblogButton.setContentHuggingPriority(.required - 2, for: .horizontal) reblogButton.setContentHuggingPriority(.required - 2, for: .horizontal)
favoriteButton.setContentHuggingPriority(.required - 1, 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 #if canImport(SwiftUI) && DEBUG

View File

@ -81,6 +81,7 @@ final class StatusTableViewCell: UITableViewCell, StatusCell {
threadMetaView.isHidden = true threadMetaView.isHidden = true
disposeBag.removeAll() disposeBag.removeAll()
observations.removeAll() observations.removeAll()
isAccessibilityElement = false // reset behavior
} }
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
@ -357,3 +358,10 @@ extension StatusTableViewCell: ActionToolbarContainerDelegate {
} }
} }
extension StatusTableViewCell {
override var accessibilityActivationPoint: CGPoint {
get { return .zero }
set { }
}
}

View File

@ -17,7 +17,7 @@ class TimelineLoaderTableViewCell: UITableViewCell {
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
var stateBindDispose: AnyCancellable? let stackView = UIStackView()
let loadMoreButton: UIButton = { let loadMoreButton: UIButton = {
let button = HighlightDimmableButton() let button = HighlightDimmableButton()
@ -86,7 +86,6 @@ class TimelineLoaderTableViewCell: UITableViewCell {
]) ])
// use stack view to alignlment content center // use stack view to alignlment content center
let stackView = UIStackView()
stackView.spacing = 4 stackView.spacing = 4
stackView.axis = .horizontal stackView.axis = .horizontal
stackView.alignment = .center stackView.alignment = .center

View File

@ -105,6 +105,11 @@ extension ActionToolbarContainer {
let starImage = UIImage(systemName: "star.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 17, weight: .bold))!.withRenderingMode(.alwaysTemplate) 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) 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 { switch style {
case .inline: case .inline:
buttons.forEach { button in buttons.forEach { button in
@ -194,6 +199,14 @@ extension ActionToolbarContainer {
} }
extension ActionToolbarContainer {
override var accessibilityElements: [Any]? {
get { [replyButton, reblogButton, favoriteButton, moreButton] }
set { }
}
}
#if DEBUG #if DEBUG
import SwiftUI import SwiftUI

View File

@ -28,7 +28,8 @@ struct MosaicImageViewModel {
let mosaicMeta = MosaicMeta( let mosaicMeta = MosaicMeta(
url: url, url: url,
size: CGSize(width: width, height: height), size: CGSize(width: width, height: height),
blurhash: element.blurhash blurhash: element.blurhash,
altText: element.descriptionString
) )
metas.append(mosaicMeta) metas.append(mosaicMeta)
} }
@ -43,6 +44,7 @@ struct MosaicMeta {
let url: URL let url: URL
let size: CGSize let size: CGSize
let blurhash: String? let blurhash: String?
let altText: String?
let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.MosaicMeta.working-queue", qos: .userInitiated, attributes: .concurrent) let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.MosaicMeta.working-queue", qos: .userInitiated, attributes: .concurrent)

View File

@ -73,22 +73,36 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
imageView.contentMode = .scaleAspectFill imageView.contentMode = .scaleAspectFill
imageView.isUserInteractionEnabled = false imageView.isUserInteractionEnabled = false
imageView.image = transitionItem.image imageView.image = transitionItem.image
// accessibility
imageView.accessibilityIgnoresInvertColors = true
return imageView return imageView
}() }()
transitionItem.targetFrame = transitionTargetFrame transitionItem.targetFrame = transitionTargetFrame
transitionItem.imageView = transitionImageView transitionItem.imageView = transitionImageView
transitionContext.containerView.addSubview(transitionImageView) transitionContext.containerView.addSubview(transitionImageView)
toVC.closeButtonBackground.alpha = 0
if UIAccessibility.isReduceTransparencyEnabled {
toVC.visualEffectView.alpha = 0
}
let animator = MediaHostToMediaPreviewViewControllerAnimatedTransitioning.animator(initialVelocity: .zero) let animator = MediaHostToMediaPreviewViewControllerAnimatedTransitioning.animator(initialVelocity: .zero)
animator.addAnimations { animator.addAnimations {
transitionImageView.frame = transitionTargetFrame transitionImageView.frame = transitionTargetFrame
toView.alpha = 1 toView.alpha = 1
if UIAccessibility.isReduceTransparencyEnabled {
toVC.visualEffectView.alpha = 1
}
} }
animator.addCompletion { position in animator.addCompletion { position in
toVC.pagingViewConttroller.view.alpha = 1 toVC.pagingViewConttroller.view.alpha = 1
transitionImageView.removeFromSuperview() transitionImageView.removeFromSuperview()
UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseInOut]) {
toVC.closeButtonBackground.alpha = 1
}
transitionContext.completeTransition(position == .end) transitionContext.completeTransition(position == .end)
} }
@ -119,9 +133,20 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
transitionContext.completeTransition(false) transitionContext.completeTransition(false)
fatalError() 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.imageView = imageView
transitionItem.snapshotTransitioning = snapshot transitionItem.snapshotTransitioning = snapshot
@ -134,6 +159,38 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
let animator = popInteractiveTransitionAnimator let animator = popInteractiveTransitionAnimator
self.transitionItem.snapshotRaw?.alpha = 0.0 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 { animator.addAnimations {
if let targetFrame = targetFrame { if let targetFrame = targetFrame {
self.transitionItem.snapshotTransitioning?.frame = targetFrame self.transitionItem.snapshotTransitioning?.frame = targetFrame
@ -143,6 +200,12 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
self.transitionItem.sourceImageViewCornerRadius.flatMap { self.transitionItem.snapshotTransitioning?.layer.cornerRadius = $0 } self.transitionItem.sourceImageViewCornerRadius.flatMap { self.transitionItem.snapshotTransitioning?.layer.cornerRadius = $0 }
fromVC.closeButtonBackground.alpha = 0 fromVC.closeButtonBackground.alpha = 0
fromVC.visualEffectView.effect = nil fromVC.visualEffectView.effect = nil
if let maskLayerToFinalPath = maskLayerToFinalPath {
maskLayer.path = maskLayerToFinalPath
}
if UIAccessibility.isReduceTransparencyEnabled {
fromVC.visualEffectView.alpha = 0
}
} }
animator.addCompletion { position in animator.addCompletion { position in
@ -197,9 +260,21 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
transitionContext.completeTransition(false) transitionContext.completeTransition(false)
return 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.imageView = imageView
transitionItem.snapshotTransitioning = snapshot transitionItem.snapshotTransitioning = snapshot
@ -214,6 +289,10 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
let blurEffect = fromVC.visualEffectView.effect let blurEffect = fromVC.visualEffectView.effect
self.transitionItem.snapshotRaw?.alpha = 0.0 self.transitionItem.snapshotRaw?.alpha = 0.0
UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseInOut]) {
fromVC.closeButtonBackground.alpha = 0
}
animator.addAnimations { animator.addAnimations {
switch self.transitionItem.source { switch self.transitionItem.source {
case .profileBanner: case .profileBanner:
@ -221,9 +300,11 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
default: default:
break break
} }
fromVC.closeButtonBackground.alpha = 0
fromVC.visualEffectView.effect = nil fromVC.visualEffectView.effect = nil
self.transitionItem.sourceImageViewCornerRadius.flatMap { self.transitionItem.snapshotTransitioning?.layer.cornerRadius = $0 } self.transitionItem.sourceImageViewCornerRadius.flatMap { self.transitionItem.snapshotTransitioning?.layer.cornerRadius = $0 }
if UIAccessibility.isReduceTransparencyEnabled {
fromVC.visualEffectView.alpha = 0
}
} }
animator.addCompletion { position in animator.addCompletion { position in
@ -237,6 +318,13 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
self.transitionItem.source.updateAppearance(position: position, index: nil) self.transitionItem.source.updateAppearance(position: position, index: nil)
} }
fromVC.visualEffectView.effect = position == .end ? nil : blurEffect 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) transitionContext.completeTransition(position == .end)
} }
} }
@ -314,7 +402,50 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
let velocity = convert(gestureVelocity, for: transitionItem) let velocity = convert(gestureVelocity, for: transitionItem)
let itemAnimator = MediaHostToMediaPreviewViewControllerAnimatedTransitioning.animator(initialVelocity: velocity) 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 { itemAnimator.addAnimations {
if let maskLayer = self.transitionItem.interactiveTransitionMaskLayer,
let maskLayerToFinalPath = maskLayerToFinalPath {
maskLayer.path = maskLayerToFinalPath
}
if toPosition == .end { if toPosition == .end {
switch self.transitionItem.source { switch self.transitionItem.source {
case .profileBanner where toPosition == .end: case .profileBanner where toPosition == .end:

View File

@ -30,6 +30,8 @@ class MediaPreviewTransitionItem: Identifiable {
var snapshotRaw: UIView? var snapshotRaw: UIView?
var snapshotTransitioning: UIView? var snapshotTransitioning: UIView?
var touchOffset: CGVector = CGVector.zero var touchOffset: CGVector = CGVector.zero
var interactiveTransitionMaskView: UIView?
var interactiveTransitionMaskLayer: CAShapeLayer?
init(id: UUID = UUID(), source: Source, previewableViewController: MediaPreviewableViewController) { init(id: UUID = UUID(), source: Source, previewableViewController: MediaPreviewableViewController) {
self.id = id self.id = id

View File

@ -7,7 +7,7 @@
import UIKit import UIKit
protocol MediaPreviewableViewController: AnyObject { protocol MediaPreviewableViewController: UIViewController {
var mediaPreviewTransitionController: MediaPreviewTransitionController { get } var mediaPreviewTransitionController: MediaPreviewTransitionController { get }
func sourceFrame(transitionItem: MediaPreviewTransitionItem, index: Int) -> CGRect? func sourceFrame(transitionItem: MediaPreviewTransitionItem, index: Int) -> CGRect?
} }