forked from zelo72/mastodon-ios
commit
761d094832
|
@ -47,6 +47,7 @@ private func map(language: String) -> String? {
|
|||
case "ja_JP": return "ja"
|
||||
case "de_DE": return "de"
|
||||
case "pt_BR": return "pt-BR"
|
||||
case "ar_SA": return "ar"
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,10 @@ mkdir -p input/en_US
|
|||
cp ../app.json ./input/en_US
|
||||
cp ../ios-infoPlist.json ./input/en_US
|
||||
|
||||
mkdir -p input/ar_SA
|
||||
cp ../app.json ./input/ar_SA
|
||||
cp ../ios-infoPlist.json ./input/ar_SA
|
||||
|
||||
# curl -o <TBD>.zip -L ${Crowin_Latest_Build}
|
||||
# unzip -o -q <TBD>.zip -d input
|
||||
# rm -rf <TBD>.zip
|
||||
|
|
|
@ -74,10 +74,17 @@
|
|||
"settings": "Settings",
|
||||
"delete": "Delete"
|
||||
},
|
||||
"tabs": {
|
||||
"home": "Home",
|
||||
"search": "Search",
|
||||
"notification": "Notification",
|
||||
"profile": "Profile"
|
||||
},
|
||||
"status": {
|
||||
"user_reblogged": "%s reblogged",
|
||||
"user_replied_to": "Replied to %s",
|
||||
"show_post": "Show Post",
|
||||
"show_user_profile": "Show user profile",
|
||||
"content_warning": "content warning",
|
||||
"content_warning_text": "cw: %s",
|
||||
"media_content_warning": "Tap to reveal that may be sensitive",
|
||||
|
@ -93,6 +100,22 @@
|
|||
},
|
||||
"time_left": "%s left",
|
||||
"closed": "Closed"
|
||||
},
|
||||
"actions": {
|
||||
"reply": "Reply",
|
||||
"reblog": "Reblog",
|
||||
"unreblog": "Unreblog",
|
||||
"favorite": "Favorite",
|
||||
"unfavorite": "Unfavorite",
|
||||
"menu": "Menu"
|
||||
},
|
||||
"tag": {
|
||||
"url": "URL",
|
||||
"mention": "Mention",
|
||||
"link": "Link",
|
||||
"hashtag": "Hashtag",
|
||||
"email": "Email",
|
||||
"emoji": "Emoji"
|
||||
}
|
||||
},
|
||||
"firendship": {
|
||||
|
@ -125,6 +148,11 @@
|
|||
"blocked_warning": "You can’t view Artbot’s profile\n until they unblock you.",
|
||||
"suspended_warning": "This account has been suspended.",
|
||||
"user_suspended_warning": "%s's account has been suspended."
|
||||
},
|
||||
"accessibility": {
|
||||
"count_replies": "%s replies",
|
||||
"count_reblogs": "%s reblogs",
|
||||
"count_favorites": "%s favorites"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -143,7 +171,20 @@
|
|||
"title": "Pick a Server,\nany server.",
|
||||
"button": {
|
||||
"category": {
|
||||
"All": "All"
|
||||
"all": "All",
|
||||
"all_accessiblity_description": "Category: All",
|
||||
"academia": "academia",
|
||||
"activism": "activism",
|
||||
"food": "food",
|
||||
"furry": "furry",
|
||||
"games": "games",
|
||||
"general": "general",
|
||||
"journalism": "journalism",
|
||||
"lgbt": "lgbt",
|
||||
"regional": "regional",
|
||||
"art": "art",
|
||||
"music": "music",
|
||||
"tech": "tech"
|
||||
},
|
||||
"see_less": "See Less",
|
||||
"see_more": "See More"
|
||||
|
@ -158,7 +199,8 @@
|
|||
},
|
||||
"empty_state": {
|
||||
"finding_servers": "Finding available servers...",
|
||||
"bad_network": "Something went wrong while loading data. Check your internet connection."
|
||||
"bad_network": "Something went wrong while loading data. Check your internet connection.",
|
||||
"no_results": "No results"
|
||||
}
|
||||
},
|
||||
"register": {
|
||||
|
@ -297,6 +339,17 @@
|
|||
"unlisted": "Unlisted",
|
||||
"private": "Followers only",
|
||||
"direct": "Only people I mention"
|
||||
},
|
||||
"accessibility": {
|
||||
"append_attachment": "Append attachment",
|
||||
"append_poll": "Append poll",
|
||||
"remove_poll": "Remove poll",
|
||||
"custom_emoji_picker": "Custom emoji picker",
|
||||
"enable_content_warning": "Enable content warning",
|
||||
"disable_content_warning": "Disable content warning",
|
||||
"post_visibility_menu": "Post visibility menu",
|
||||
"input_limit_remains_count": "Input limit remains %ld",
|
||||
"input_limit_exceeds_count": "Input limit exceeds %ld"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
|
@ -304,7 +357,12 @@
|
|||
"dashboard": {
|
||||
"posts": "posts",
|
||||
"following": "following",
|
||||
"followers": "followers"
|
||||
"followers": "followers",
|
||||
"accessibility": {
|
||||
"count_posts": "%ld posts",
|
||||
"count_following": "%ld following",
|
||||
"count_followers": "%ld followers"
|
||||
}
|
||||
},
|
||||
"segmented_control": {
|
||||
"posts": "Posts",
|
||||
|
@ -336,7 +394,7 @@
|
|||
},
|
||||
"accounts": {
|
||||
"title": "Accounts you might like",
|
||||
"description": "Except for Sam, you will not like his account.",
|
||||
"description": "You may like to follow these accounts",
|
||||
"follow": "Follow"
|
||||
}
|
||||
},
|
||||
|
@ -428,4 +486,4 @@
|
|||
"text_placeholder": "Type or paste additional comments"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -191,6 +191,7 @@
|
|||
DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */; };
|
||||
DB084B5725CBC56C00F898ED /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB084B5625CBC56C00F898ED /* Status.swift */; };
|
||||
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */; };
|
||||
DB0F8150264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0F814F264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift */; };
|
||||
DB118A8225E4B6E600FAB162 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */; };
|
||||
DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */; };
|
||||
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D186B25EF5BA7003F1F23 /* PollTableView.swift */; };
|
||||
|
@ -284,13 +285,11 @@
|
|||
DB6804872637CD4C00430867 /* AppShared.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
DB6804922637CD8700430867 /* AppName.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6804912637CD8700430867 /* AppName.swift */; };
|
||||
DB6804A52637CDCC00430867 /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; };
|
||||
DB6804A62637CDCC00430867 /* AppShared.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
DB6804C82637CE2F00430867 /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; };
|
||||
DB6804D12637CE4700430867 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6804D02637CE4700430867 /* UserDefaults.swift */; };
|
||||
DB6804FD2637CFEC00430867 /* AppSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6804FC2637CFEC00430867 /* AppSecret.swift */; };
|
||||
DB6805102637D0F800430867 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = DB68050F2637D0F800430867 /* KeychainAccess */; };
|
||||
DB6805262637D7DD00430867 /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; };
|
||||
DB6805272637D7DD00430867 /* AppShared.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */; };
|
||||
DB68A04A25E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A04925E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift */; };
|
||||
DB68A05D25E9055900CFDF14 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DB68A05C25E9055900CFDF14 /* Settings.bundle */; };
|
||||
|
@ -520,28 +519,6 @@
|
|||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
DB6804A92637CDCC00430867 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
DB6804A62637CDCC00430867 /* AppShared.framework in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
DB68052A2637D7DD00430867 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
DB6805272637D7DD00430867 /* AppShared.framework in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
DB89BA0825C10FD0008580ED /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
@ -756,6 +733,9 @@
|
|||
DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveUserInterfaceStyleBarButtonItem.swift; sourceTree = "<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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -1134,6 +1114,7 @@
|
|||
0FB3D30725E524C600AAD544 /* PickServerCategoriesCell.swift */,
|
||||
0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */,
|
||||
0FB3D33725E6401400AAD544 /* PickServerCell.swift */,
|
||||
DB0F814F264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift */,
|
||||
);
|
||||
path = TableViewCell;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1388,6 +1369,7 @@
|
|||
2D7631A425C1532200929FB9 /* Share */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5D03938E2612D200007FE196 /* Webview */,
|
||||
DB68A04F25E9028800CFDF14 /* NavigationController */,
|
||||
DB9D6C2025E502C60051B173 /* ViewModel */,
|
||||
2D7631A525C1532D00929FB9 /* View */,
|
||||
|
@ -2045,7 +2027,6 @@
|
|||
DB8AF55525C1379F002E6C99 /* Scene */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5D03938E2612D200007FE196 /* Webview */,
|
||||
2D7631A425C1532200929FB9 /* Share */,
|
||||
DB6180E426391A500018D199 /* Transition */,
|
||||
DB8AF54E25C13703002E6C99 /* MainTab */,
|
||||
|
@ -2494,7 +2475,6 @@
|
|||
DB89B9EA25C10FD0008580ED /* Sources */,
|
||||
DB89B9EB25C10FD0008580ED /* Frameworks */,
|
||||
DB89B9EC25C10FD0008580ED /* Resources */,
|
||||
DB68052A2637D7DD00430867 /* Embed Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
|
@ -2533,7 +2513,6 @@
|
|||
DBF8AE0F263293E400C9C23C /* Sources */,
|
||||
DBF8AE10263293E400C9C23C /* Frameworks */,
|
||||
DBF8AE11263293E400C9C23C /* Resources */,
|
||||
DB6804A92637CDCC00430867 /* Embed Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
|
@ -2595,6 +2574,7 @@
|
|||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
ar,
|
||||
);
|
||||
mainGroup = DB427DC925BAA00100D1B89D;
|
||||
packageReferences = (
|
||||
|
@ -2980,6 +2960,7 @@
|
|||
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */,
|
||||
DBE3CE0D261D767100430CC6 /* FavoriteViewController+Provider.swift in Sources */,
|
||||
2D084B9326259545003AA3AF /* NotificationViewModel+LoadLatestState.swift in Sources */,
|
||||
DB0F8150264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift in Sources */,
|
||||
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
|
||||
DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */,
|
||||
DBB9759C262462E1004620BD /* ThreadMetaView.swift in Sources */,
|
||||
|
@ -3353,6 +3334,7 @@
|
|||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
DB2B3ABD25E37E15007045F9 /* en */,
|
||||
DB0F814E264CFFD300F2A12B /* ar */,
|
||||
);
|
||||
name = InfoPlist.strings;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3361,6 +3343,7 @@
|
|||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
DB3D100E25BAA75E00EAA174 /* en */,
|
||||
DB0F814D264CFFD300F2A12B /* ar */,
|
||||
);
|
||||
name = Localizable.strings;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3388,6 +3371,7 @@
|
|||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
|
@ -3449,6 +3433,7 @@
|
|||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
|
@ -3509,7 +3494,7 @@
|
|||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
|
||||
DEVELOPMENT_TEAM = 7LFDZ96332;
|
||||
INFOPLIST_FILE = Mastodon/Info.plist;
|
||||
|
@ -3517,7 +3502,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.3.0;
|
||||
MARKETING_VERSION = 0.4.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
@ -3536,7 +3521,7 @@
|
|||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
|
||||
DEVELOPMENT_TEAM = 7LFDZ96332;
|
||||
INFOPLIST_FILE = Mastodon/Info.plist;
|
||||
|
@ -3544,7 +3529,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.3.0;
|
||||
MARKETING_VERSION = 0.4.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
@ -3639,6 +3624,7 @@
|
|||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = D7D7CF93E262178800077512 /* Pods-Mastodon-AppShared.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
|
@ -3669,6 +3655,7 @@
|
|||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = B31D44635FCF6452F7E1B865 /* Pods-Mastodon-AppShared.release.xcconfig */;
|
||||
buildSettings = {
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
|
@ -3916,7 +3903,7 @@
|
|||
repositoryURL = "https://github.com/TwidereProject/ActiveLabel.swift";
|
||||
requirement = {
|
||||
kind = exactVersion;
|
||||
version = 5.0.1;
|
||||
version = 5.0.2;
|
||||
};
|
||||
};
|
||||
2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */ = {
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>15</integer>
|
||||
<integer>14</integer>
|
||||
</dict>
|
||||
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
@ -32,7 +32,7 @@
|
|||
<key>NotificationService.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>14</integer>
|
||||
<integer>15</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
"repositoryURL": "https://github.com/TwidereProject/ActiveLabel.swift",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "40e104063d825d1125ef4b8eeb6460eba8a57483",
|
||||
"version": "5.0.1"
|
||||
"revision": "2132a7bf8da2bea74bb49b0b815950ea4d8f6a7e",
|
||||
"version": "5.0.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -69,7 +69,7 @@
|
|||
"repositoryURL": "https://github.com/onevcat/Kingfisher.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "15d199e84677303a7004ed2c5ecaa1a90f3863f8",
|
||||
"revision": "bbc4bc4def7eb05a7ba8e1219f80ee9be327334e",
|
||||
"version": "6.2.1"
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -14,6 +14,7 @@ enum PickServerItem {
|
|||
case categoryPicker(items: [CategoryPickerItem])
|
||||
case search
|
||||
case server(server: Mastodon.Entity.Server, attribute: ServerItemAttribute)
|
||||
case loader(attribute: LoaderItemAttribute)
|
||||
}
|
||||
|
||||
extension PickServerItem {
|
||||
|
@ -34,6 +35,26 @@ extension PickServerItem {
|
|||
hasher.combine(isExpand)
|
||||
}
|
||||
}
|
||||
|
||||
final class LoaderItemAttribute: Equatable, Hashable {
|
||||
let id = UUID()
|
||||
|
||||
var isLast: Bool
|
||||
var isNoResult: Bool
|
||||
|
||||
init(isLast: Bool, isEmptyResult: Bool) {
|
||||
self.isLast = isLast
|
||||
self.isNoResult = isEmptyResult
|
||||
}
|
||||
|
||||
static func == (lhs: PickServerItem.LoaderItemAttribute, rhs: PickServerItem.LoaderItemAttribute) -> Bool {
|
||||
return lhs.id == rhs.id
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PickServerItem: Equatable {
|
||||
|
@ -47,6 +68,8 @@ extension PickServerItem: Equatable {
|
|||
return true
|
||||
case (.server(let serverLeft, _), .server(let serverRight, _)):
|
||||
return serverLeft.domain == serverRight.domain
|
||||
case (.loader(let attributeLeft), loader(let attributeRight)):
|
||||
return attributeLeft == attributeRight
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
@ -64,6 +87,8 @@ extension PickServerItem: Hashable {
|
|||
hasher.combine(String(describing: PickServerItem.search.self))
|
||||
case .server(let server, _):
|
||||
hasher.combine(server.domain)
|
||||
case .loader(let attribute):
|
||||
hasher.combine(attribute)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,6 +42,10 @@ extension CategoryPickerSection {
|
|||
}
|
||||
}
|
||||
.store(in: &cell.observations)
|
||||
|
||||
cell.isAccessibilityElement = true
|
||||
cell.accessibilityLabel = item.accessibilityDescription
|
||||
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ extension CustomEmojiPickerSection {
|
|||
],
|
||||
completionHandler: nil
|
||||
)
|
||||
cell.accessibilityLabel = attribute.emoji.shortcode
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,6 +57,10 @@ extension PickServerSection {
|
|||
PickServerSection.configure(cell: cell, server: server, attribute: attribute)
|
||||
cell.delegate = pickServerCellDelegate
|
||||
return cell
|
||||
case .loader(let attribute):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerLoaderTableViewCell.self), for: indexPath) as! PickServerLoaderTableViewCell
|
||||
PickServerSection.configure(cell: cell, attribute: attribute)
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -137,3 +141,23 @@ extension PickServerSection {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
extension PickServerSection {
|
||||
|
||||
static func configure(cell: PickServerLoaderTableViewCell, attribute: PickServerItem.LoaderItemAttribute) {
|
||||
if attribute.isLast {
|
||||
cell.containerView.layer.maskedCorners = [
|
||||
.layerMinXMaxYCorner,
|
||||
.layerMaxXMaxYCorner
|
||||
]
|
||||
cell.containerView.layer.cornerCurve = .continuous
|
||||
cell.containerView.layer.cornerRadius = MastodonPickServerAppearance.tableViewCornerRadius
|
||||
} else {
|
||||
cell.containerView.layer.cornerRadius = 0
|
||||
}
|
||||
|
||||
attribute.isNoResult ? cell.stopAnimating() : cell.startAnimating()
|
||||
cell.emptyStatusLabel.isHidden = !attribute.isNoResult
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -58,6 +58,7 @@ extension StatusSection {
|
|||
)
|
||||
}
|
||||
cell.delegate = statusTableViewCellDelegate
|
||||
cell.isAccessibilityElement = true
|
||||
return cell
|
||||
case .status(let objectID, let attribute),
|
||||
.root(let objectID, let attribute),
|
||||
|
@ -97,7 +98,23 @@ extension StatusSection {
|
|||
}
|
||||
}
|
||||
cell.delegate = statusTableViewCellDelegate
|
||||
|
||||
switch item {
|
||||
case .root:
|
||||
cell.statusView.activeTextLabel.isAccessibilityElement = false
|
||||
var accessibilityElements: [Any] = []
|
||||
accessibilityElements.append(cell.statusView.avatarView)
|
||||
accessibilityElements.append(cell.statusView.nameLabel)
|
||||
accessibilityElements.append(cell.statusView.dateLabel)
|
||||
accessibilityElements.append(contentsOf: cell.statusView.activeTextLabel.createAccessibilityElements())
|
||||
accessibilityElements.append(contentsOf: cell.statusView.statusMosaicImageViewContainer.imageViews)
|
||||
accessibilityElements.append(cell.statusView.playerContainerView)
|
||||
accessibilityElements.append(cell.statusView.actionToolbarContainer)
|
||||
accessibilityElements.append(cell.threadMetaView)
|
||||
cell.accessibilityElements = accessibilityElements
|
||||
default:
|
||||
cell.isAccessibilityElement = true
|
||||
cell.accessibilityElements = nil
|
||||
}
|
||||
return cell
|
||||
case .leafBottomLoader:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ThreadReplyLoaderTableViewCell.self), for: indexPath) as! ThreadReplyLoaderTableViewCell
|
||||
|
@ -179,6 +196,7 @@ extension StatusSection {
|
|||
}()
|
||||
cell.statusView.nameLabel.configure(content: nameText, emojiDict: (status.reblog ?? status).author.emojiDict)
|
||||
cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct
|
||||
|
||||
// set avatar
|
||||
if let reblog = status.reblog {
|
||||
cell.statusView.avatarButton.isHidden = true
|
||||
|
@ -196,6 +214,7 @@ extension StatusSection {
|
|||
content: (status.reblog ?? status).content,
|
||||
emojiDict: (status.reblog ?? status).emojiDict
|
||||
)
|
||||
cell.statusView.activeTextLabel.accessibilityLanguage = (status.reblog ?? status).language
|
||||
|
||||
// set visibility
|
||||
if let visibility = (status.reblog ?? status).visibility {
|
||||
|
@ -275,6 +294,7 @@ extension StatusSection {
|
|||
break
|
||||
}
|
||||
}
|
||||
imageView.accessibilityLabel = meta.altText
|
||||
Publishers.CombineLatest(
|
||||
statusItemAttribute.isImageLoaded,
|
||||
statusItemAttribute.isRevealing
|
||||
|
@ -452,6 +472,7 @@ extension StatusSection {
|
|||
.sink { [weak cell] _ in
|
||||
guard let cell = cell else { return }
|
||||
cell.statusView.dateLabel.text = createdAt.shortTimeAgoSinceNow
|
||||
cell.statusView.dateLabel.accessibilityLabel = createdAt.timeAgoSinceNow
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
|
||||
|
@ -463,7 +484,8 @@ extension StatusSection {
|
|||
} receiveValue: { [weak dependency, weak cell] change in
|
||||
guard let dependency = dependency else { return }
|
||||
guard case .update(let object) = change.changeType,
|
||||
let status = object as? Status else { return }
|
||||
let status = object as? Status,
|
||||
!status.isDeleted else { return }
|
||||
guard let statusTableViewCell = cell as? StatusTableViewCell else { return }
|
||||
StatusSection.configureActionToolBar(
|
||||
cell: statusTableViewCell,
|
||||
|
@ -571,6 +593,7 @@ extension StatusSection {
|
|||
formatter.timeStyle = .short
|
||||
return formatter.string(from: status.createdAt)
|
||||
}()
|
||||
cell.threadMetaView.dateLabel.accessibilityLabel = DateFormatter.localizedString(from: status.createdAt, dateStyle: .medium, timeStyle: .short)
|
||||
let reblogCountTitle: String = {
|
||||
let count = status.reblogsCount.intValue
|
||||
if count > 1 {
|
||||
|
@ -608,6 +631,7 @@ extension StatusSection {
|
|||
return L10n.Common.Controls.Status.userReblogged(name)
|
||||
}()
|
||||
cell.statusView.headerInfoLabel.configure(content: headerText, emojiDict: status.author.emojiDict)
|
||||
cell.statusView.headerInfoLabel.isAccessibilityElement = true
|
||||
} else if status.inReplyToID != nil {
|
||||
cell.statusView.headerContainerView.isHidden = false
|
||||
cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage)
|
||||
|
@ -620,8 +644,10 @@ extension StatusSection {
|
|||
return L10n.Common.Controls.Status.userRepliedTo(name)
|
||||
}()
|
||||
cell.statusView.headerInfoLabel.configure(content: headerText, emojiDict: status.replyTo?.author.emojiDict ?? [:])
|
||||
cell.statusView.headerInfoLabel.isAccessibilityElement = true
|
||||
} else {
|
||||
cell.statusView.headerContainerView.isHidden = true
|
||||
cell.statusView.headerInfoLabel.isAccessibilityElement = false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -639,6 +665,9 @@ extension StatusSection {
|
|||
return StatusSection.formattedNumberTitleForActionButton(count)
|
||||
}()
|
||||
cell.statusView.actionToolbarContainer.replyButton.setTitle(replyCountTitle, for: .normal)
|
||||
cell.statusView.actionToolbarContainer.replyButton.accessibilityValue = status.repliesCount.flatMap {
|
||||
L10n.Common.Controls.Timeline.Accessibility.countReplies($0.intValue)
|
||||
} ?? nil
|
||||
// set reblog
|
||||
let isReblogged = status.rebloggedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false
|
||||
let reblogCountTitle: String = {
|
||||
|
@ -647,6 +676,11 @@ extension StatusSection {
|
|||
}()
|
||||
cell.statusView.actionToolbarContainer.reblogButton.setTitle(reblogCountTitle, for: .normal)
|
||||
cell.statusView.actionToolbarContainer.isReblogButtonHighlight = isReblogged
|
||||
cell.statusView.actionToolbarContainer.reblogButton.accessibilityLabel = isReblogged ? L10n.Common.Controls.Status.Actions.unreblog : L10n.Common.Controls.Status.Actions.reblog
|
||||
cell.statusView.actionToolbarContainer.reblogButton.accessibilityValue = {
|
||||
guard status.reblogsCount.intValue > 0 else { return nil }
|
||||
return L10n.Common.Controls.Timeline.Accessibility.countReblogs(status.reblogsCount.intValue)
|
||||
}()
|
||||
// set like
|
||||
let isLike = status.favouritedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false
|
||||
let favoriteCountTitle: String = {
|
||||
|
@ -655,14 +689,18 @@ extension StatusSection {
|
|||
}()
|
||||
cell.statusView.actionToolbarContainer.favoriteButton.setTitle(favoriteCountTitle, for: .normal)
|
||||
cell.statusView.actionToolbarContainer.isFavoriteButtonHighlight = isLike
|
||||
|
||||
cell.statusView.actionToolbarContainer.favoriteButton.accessibilityLabel = isLike ? L10n.Common.Controls.Status.Actions.unfavorite : L10n.Common.Controls.Status.Actions.favorite
|
||||
cell.statusView.actionToolbarContainer.favoriteButton.accessibilityValue = {
|
||||
guard status.favouritesCount.intValue > 0 else { return nil }
|
||||
return L10n.Common.Controls.Timeline.Accessibility.countReblogs(status.favouritesCount.intValue)
|
||||
}()
|
||||
Publishers.CombineLatest(
|
||||
dependency.context.blockDomainService.blockedDomains,
|
||||
ManagedObjectObserver.observe(object: status.authorForUserProvider)
|
||||
.assertNoFailure()
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak dependency, weak cell] _,change in
|
||||
.sink { [weak dependency, weak cell] _, change in
|
||||
guard let cell = cell else { return }
|
||||
guard let dependency = dependency else { return }
|
||||
switch change.changeType {
|
||||
|
|
|
@ -32,12 +32,14 @@ extension ActiveLabel {
|
|||
text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
|
||||
#endif
|
||||
|
||||
accessibilityContainerType = .semanticGroup
|
||||
|
||||
switch style {
|
||||
case .default:
|
||||
font = .preferredFont(forTextStyle: .body)
|
||||
textColor = Asset.Colors.Label.primary.color
|
||||
case .statusHeader:
|
||||
font = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: .systemFont(ofSize: 13, weight: .medium))
|
||||
font = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: .systemFont(ofSize: 13, weight: .medium), maximumPointSize: 17)
|
||||
textColor = Asset.Colors.Label.secondary.color
|
||||
numberOfLines = 1
|
||||
case .statusName:
|
||||
|
@ -61,8 +63,10 @@ extension ActiveLabel {
|
|||
if let parseResult = try? MastodonStatusContent.parse(content: content, emojiDict: emojiDict) {
|
||||
text = parseResult.trimmed
|
||||
activeEntities = parseResult.activeEntities
|
||||
accessibilityLabel = parseResult.original
|
||||
} else {
|
||||
text = ""
|
||||
accessibilityLabel = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,5 +83,110 @@ extension ActiveLabel {
|
|||
let parseResult = MastodonField.parse(field: field)
|
||||
text = parseResult.value
|
||||
activeEntities = parseResult.activeEntities
|
||||
accessibilityLabel = parseResult.value
|
||||
}
|
||||
}
|
||||
|
||||
extension ActiveEntity {
|
||||
|
||||
var accessibilityLabelDescription: String {
|
||||
switch self.type {
|
||||
case .email: return L10n.Common.Controls.Status.Tag.email
|
||||
case .hashtag: return L10n.Common.Controls.Status.Tag.hashtag
|
||||
case .mention: return L10n.Common.Controls.Status.Tag.mention
|
||||
case .url: return L10n.Common.Controls.Status.Tag.url
|
||||
case .emoji: return L10n.Common.Controls.Status.Tag.emoji
|
||||
}
|
||||
}
|
||||
|
||||
var accessibilityValueDescription: String {
|
||||
switch self.type {
|
||||
case .email(let text, _): return text
|
||||
case .hashtag(let text, _): return text
|
||||
case .mention(let text, _): return text
|
||||
case .url(_, let trimmed, _, _): return trimmed
|
||||
case .emoji(let text, _, _): return text
|
||||
}
|
||||
}
|
||||
|
||||
func accessibilityElement(in accessibilityContainer: Any) -> ActiveLabelAccessibilityElement? {
|
||||
if case .emoji = self.type {
|
||||
return nil
|
||||
}
|
||||
|
||||
let element = ActiveLabelAccessibilityElement(accessibilityContainer: accessibilityContainer)
|
||||
element.accessibilityTraits = .button
|
||||
element.accessibilityLabel = accessibilityLabelDescription
|
||||
element.accessibilityValue = accessibilityValueDescription
|
||||
return element
|
||||
}
|
||||
}
|
||||
|
||||
final class ActiveLabelAccessibilityElement: UIAccessibilityElement {
|
||||
var index: Int!
|
||||
}
|
||||
|
||||
// MARK: - UIAccessibilityContainer
|
||||
extension ActiveLabel {
|
||||
|
||||
func createAccessibilityElements() -> [UIAccessibilityElement] {
|
||||
var elements: [UIAccessibilityElement] = []
|
||||
|
||||
let element = ActiveLabelAccessibilityElement(accessibilityContainer: self)
|
||||
element.accessibilityTraits = .staticText
|
||||
element.accessibilityLabel = accessibilityLabel
|
||||
element.accessibilityFrame = superview!.convert(frame, to: nil)
|
||||
element.accessibilityLanguage = accessibilityLanguage
|
||||
elements.append(element)
|
||||
|
||||
for eneity in activeEntities {
|
||||
guard let element = eneity.accessibilityElement(in: self) else { continue }
|
||||
var glyphRange = NSRange()
|
||||
layoutManager.characterRange(forGlyphRange: eneity.range, actualGlyphRange: &glyphRange)
|
||||
let rect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
|
||||
element.accessibilityFrame = self.convert(rect, to: nil)
|
||||
element.accessibilityContainer = self
|
||||
elements.append(element)
|
||||
}
|
||||
|
||||
return elements
|
||||
}
|
||||
|
||||
// public override func accessibilityElementCount() -> Int {
|
||||
// return 1 + activeEntities.count
|
||||
// }
|
||||
//
|
||||
// public override func accessibilityElement(at index: Int) -> Any? {
|
||||
// if index == 0 {
|
||||
// let element = ActiveLabelAccessibilityElement(accessibilityContainer: self)
|
||||
// element.accessibilityTraits = .staticText
|
||||
// element.accessibilityLabel = accessibilityLabel
|
||||
// element.accessibilityFrame = superview!.convert(frame, to: nil)
|
||||
// element.index = index
|
||||
// return element
|
||||
// }
|
||||
//
|
||||
// let index = index - 1
|
||||
// guard index < activeEntities.count else { return nil }
|
||||
// let eneity = activeEntities[index]
|
||||
// guard let element = eneity.accessibilityElement(in: self) else { return nil }
|
||||
//
|
||||
// var glyphRange = NSRange()
|
||||
// layoutManager.characterRange(forGlyphRange: eneity.range, actualGlyphRange: &glyphRange)
|
||||
// let rect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
|
||||
// element.accessibilityFrame = self.convert(rect, to: nil)
|
||||
// element.accessibilityContainer = self
|
||||
//
|
||||
// return element
|
||||
// }
|
||||
//
|
||||
// public override func index(ofAccessibilityElement element: Any) -> Int {
|
||||
// guard let element = element as? ActiveLabelAccessibilityElement,
|
||||
// let index = element.index else {
|
||||
// return NSNotFound
|
||||
// }
|
||||
//
|
||||
// return index
|
||||
// }
|
||||
|
||||
}
|
||||
|
|
|
@ -200,6 +200,8 @@ internal enum L10n {
|
|||
internal static let mediaContentWarning = L10n.tr("Localizable", "Common.Controls.Status.MediaContentWarning")
|
||||
/// Show Post
|
||||
internal static let showPost = L10n.tr("Localizable", "Common.Controls.Status.ShowPost")
|
||||
/// Show user profile
|
||||
internal static let showUserProfile = L10n.tr("Localizable", "Common.Controls.Status.ShowUserProfile")
|
||||
/// %@ reblogged
|
||||
internal static func userReblogged(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Common.Controls.Status.UserReblogged", String(describing: p1))
|
||||
|
@ -208,6 +210,20 @@ internal enum L10n {
|
|||
internal static func userRepliedTo(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Common.Controls.Status.UserRepliedTo", String(describing: p1))
|
||||
}
|
||||
internal enum Actions {
|
||||
/// Favorite
|
||||
internal static let favorite = L10n.tr("Localizable", "Common.Controls.Status.Actions.Favorite")
|
||||
/// Menu
|
||||
internal static let menu = L10n.tr("Localizable", "Common.Controls.Status.Actions.Menu")
|
||||
/// Reblog
|
||||
internal static let reblog = L10n.tr("Localizable", "Common.Controls.Status.Actions.Reblog")
|
||||
/// Reply
|
||||
internal static let reply = L10n.tr("Localizable", "Common.Controls.Status.Actions.Reply")
|
||||
/// Unfavorite
|
||||
internal static let unfavorite = L10n.tr("Localizable", "Common.Controls.Status.Actions.Unfavorite")
|
||||
/// Unreblog
|
||||
internal static let unreblog = L10n.tr("Localizable", "Common.Controls.Status.Actions.Unreblog")
|
||||
}
|
||||
internal enum Poll {
|
||||
/// Closed
|
||||
internal static let closed = L10n.tr("Localizable", "Common.Controls.Status.Poll.Closed")
|
||||
|
@ -238,8 +254,46 @@ internal enum L10n {
|
|||
}
|
||||
}
|
||||
}
|
||||
internal enum Tag {
|
||||
/// Email
|
||||
internal static let email = L10n.tr("Localizable", "Common.Controls.Status.Tag.Email")
|
||||
/// Emoji
|
||||
internal static let emoji = L10n.tr("Localizable", "Common.Controls.Status.Tag.Emoji")
|
||||
/// Hashtag
|
||||
internal static let hashtag = L10n.tr("Localizable", "Common.Controls.Status.Tag.Hashtag")
|
||||
/// Link
|
||||
internal static let link = L10n.tr("Localizable", "Common.Controls.Status.Tag.Link")
|
||||
/// Mention
|
||||
internal static let mention = L10n.tr("Localizable", "Common.Controls.Status.Tag.Mention")
|
||||
/// URL
|
||||
internal static let url = L10n.tr("Localizable", "Common.Controls.Status.Tag.Url")
|
||||
}
|
||||
}
|
||||
internal enum Tabs {
|
||||
/// Home
|
||||
internal static let home = L10n.tr("Localizable", "Common.Controls.Tabs.Home")
|
||||
/// Notification
|
||||
internal static let notification = L10n.tr("Localizable", "Common.Controls.Tabs.Notification")
|
||||
/// Profile
|
||||
internal static let profile = L10n.tr("Localizable", "Common.Controls.Tabs.Profile")
|
||||
/// Search
|
||||
internal static let search = L10n.tr("Localizable", "Common.Controls.Tabs.Search")
|
||||
}
|
||||
internal enum Timeline {
|
||||
internal enum Accessibility {
|
||||
/// %@ favorites
|
||||
internal static func countFavorites(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Common.Controls.Timeline.Accessibility.CountFavorites", String(describing: p1))
|
||||
}
|
||||
/// %@ reblogs
|
||||
internal static func countReblogs(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Common.Controls.Timeline.Accessibility.CountReblogs", String(describing: p1))
|
||||
}
|
||||
/// %@ replies
|
||||
internal static func countReplies(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Common.Controls.Timeline.Accessibility.CountReplies", String(describing: p1))
|
||||
}
|
||||
}
|
||||
internal enum Header {
|
||||
/// You can’t view Artbot’s profile\n until they unblock you.
|
||||
internal static let blockedWarning = L10n.tr("Localizable", "Common.Controls.Timeline.Header.BlockedWarning")
|
||||
|
@ -284,6 +338,30 @@ internal enum L10n {
|
|||
internal static func replyingToUser(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Scene.Compose.ReplyingToUser", String(describing: p1))
|
||||
}
|
||||
internal enum Accessibility {
|
||||
/// Append attachment
|
||||
internal static let appendAttachment = L10n.tr("Localizable", "Scene.Compose.Accessibility.AppendAttachment")
|
||||
/// Append poll
|
||||
internal static let appendPoll = L10n.tr("Localizable", "Scene.Compose.Accessibility.AppendPoll")
|
||||
/// Custom emoji picker
|
||||
internal static let customEmojiPicker = L10n.tr("Localizable", "Scene.Compose.Accessibility.CustomEmojiPicker")
|
||||
/// Disable content warning
|
||||
internal static let disableContentWarning = L10n.tr("Localizable", "Scene.Compose.Accessibility.DisableContentWarning")
|
||||
/// Enable content warning
|
||||
internal static let enableContentWarning = L10n.tr("Localizable", "Scene.Compose.Accessibility.EnableContentWarning")
|
||||
/// Input limit exceeds %ld
|
||||
internal static func inputLimitExceedsCount(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "Scene.Compose.Accessibility.InputLimitExceedsCount", p1)
|
||||
}
|
||||
/// Input limit remains %ld
|
||||
internal static func inputLimitRemainsCount(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "Scene.Compose.Accessibility.InputLimitRemainsCount", p1)
|
||||
}
|
||||
/// Post visibility menu
|
||||
internal static let postVisibilityMenu = L10n.tr("Localizable", "Scene.Compose.Accessibility.PostVisibilityMenu")
|
||||
/// Remove poll
|
||||
internal static let removePoll = L10n.tr("Localizable", "Scene.Compose.Accessibility.RemovePoll")
|
||||
}
|
||||
internal enum Attachment {
|
||||
/// This %@ is broken and can't be\nuploaded to Mastodon.
|
||||
internal static func attachmentBroken(_ p1: Any) -> String {
|
||||
|
@ -439,6 +517,20 @@ internal enum L10n {
|
|||
internal static let following = L10n.tr("Localizable", "Scene.Profile.Dashboard.Following")
|
||||
/// posts
|
||||
internal static let posts = L10n.tr("Localizable", "Scene.Profile.Dashboard.Posts")
|
||||
internal enum Accessibility {
|
||||
/// %ld followers
|
||||
internal static func countFollowers(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "Scene.Profile.Dashboard.Accessibility.CountFollowers", p1)
|
||||
}
|
||||
/// %ld following
|
||||
internal static func countFollowing(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "Scene.Profile.Dashboard.Accessibility.CountFollowing", p1)
|
||||
}
|
||||
/// %ld posts
|
||||
internal static func countPosts(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "Scene.Profile.Dashboard.Accessibility.CountPosts", p1)
|
||||
}
|
||||
}
|
||||
}
|
||||
internal enum RelationshipActionAlert {
|
||||
internal enum ConfirmUnblockUsre {
|
||||
|
@ -598,7 +690,7 @@ internal enum L10n {
|
|||
/// See All
|
||||
internal static let buttonText = L10n.tr("Localizable", "Scene.Search.Recommend.ButtonText")
|
||||
internal enum Accounts {
|
||||
/// Except for Sam, you will not like his account.
|
||||
/// You may like to follow these accounts
|
||||
internal static let description = L10n.tr("Localizable", "Scene.Search.Recommend.Accounts.Description")
|
||||
/// Follow
|
||||
internal static let follow = L10n.tr("Localizable", "Scene.Search.Recommend.Accounts.Follow")
|
||||
|
@ -646,8 +738,34 @@ internal enum L10n {
|
|||
/// See More
|
||||
internal static let seeMore = L10n.tr("Localizable", "Scene.ServerPicker.Button.SeeMore")
|
||||
internal enum Category {
|
||||
/// academia
|
||||
internal static let academia = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Academia")
|
||||
/// activism
|
||||
internal static let activism = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Activism")
|
||||
/// All
|
||||
internal static let all = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.All")
|
||||
/// Category: All
|
||||
internal static let allAccessiblityDescription = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.AllAccessiblityDescription")
|
||||
/// art
|
||||
internal static let art = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Art")
|
||||
/// food
|
||||
internal static let food = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Food")
|
||||
/// furry
|
||||
internal static let furry = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Furry")
|
||||
/// games
|
||||
internal static let games = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Games")
|
||||
/// general
|
||||
internal static let general = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.General")
|
||||
/// journalism
|
||||
internal static let journalism = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Journalism")
|
||||
/// lgbt
|
||||
internal static let lgbt = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Lgbt")
|
||||
/// music
|
||||
internal static let music = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Music")
|
||||
/// regional
|
||||
internal static let regional = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Regional")
|
||||
/// tech
|
||||
internal static let tech = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.Tech")
|
||||
}
|
||||
}
|
||||
internal enum EmptyState {
|
||||
|
@ -655,6 +773,8 @@ internal enum L10n {
|
|||
internal static let badNetwork = L10n.tr("Localizable", "Scene.ServerPicker.EmptyState.BadNetwork")
|
||||
/// Finding available servers...
|
||||
internal static let findingServers = L10n.tr("Localizable", "Scene.ServerPicker.EmptyState.FindingServers")
|
||||
/// No results
|
||||
internal static let noResults = L10n.tr("Localizable", "Scene.ServerPicker.EmptyState.NoResults")
|
||||
}
|
||||
internal enum Input {
|
||||
/// Find a server or join your own...
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
|
|
|
@ -26,7 +26,7 @@ extension AvatarConfigurableView {
|
|||
if Self.configurableAvatarImageCornerRadius < Self.configurableAvatarImageSize.width * 0.5 {
|
||||
return placeholderImage
|
||||
.af.imageAspectScaled(toFill: Self.configurableAvatarImageSize)
|
||||
.af.imageRounded(withCornerRadius: Self.configurableAvatarImageCornerRadius, divideRadiusByImageScale: true)
|
||||
.af.imageRounded(withCornerRadius: Self.configurableAvatarImageCornerRadius, divideRadiusByImageScale: false)
|
||||
} else {
|
||||
return placeholderImage.af.imageRoundedIntoCircle()
|
||||
}
|
||||
|
@ -47,6 +47,10 @@ extension AvatarConfigurableView {
|
|||
configurableAvatarButton?.layer.cornerRadius = 0
|
||||
configurableAvatarButton?.layer.cornerCurve = .circular
|
||||
|
||||
// accessibility
|
||||
configurableAvatarImageView?.accessibilityIgnoresInvertColors = true
|
||||
configurableAvatarButton?.accessibilityIgnoresInvertColors = true
|
||||
|
||||
defer {
|
||||
avatarConfigurableView(self, didFinishConfiguration: configuration)
|
||||
}
|
||||
|
|
|
@ -59,7 +59,6 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
|||
extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||
|
||||
|
||||
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) {
|
||||
StatusProviderFacade.responseToStatusContentWarningRevealAction(provider: self, cell: cell)
|
||||
}
|
||||
|
@ -76,7 +75,11 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
|||
|
||||
extension StatusTableViewCellDelegate where Self: StatusProvider & MediaPreviewableViewController {
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) {
|
||||
StatusProviderFacade.coordinateToStatusMediaPreviewScene(provider: self, cell: cell, mosaicImageView: mosaicImageViewContainer, didTapImageView: imageView, atIndex: index)
|
||||
if UIAccessibility.isVoiceOverRunning, !(self is ThreadViewController) {
|
||||
StatusProviderFacade.coordinateToStatusThreadScene(for: .primary, provider: self, cell: cell)
|
||||
} else {
|
||||
StatusProviderFacade.coordinateToStatusMediaPreviewScene(provider: self, cell: cell, mosaicImageView: mosaicImageViewContainer, didTapImageView: imageView, atIndex: index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
"NSCameraUsageDescription" = "Used to take photo for post status";
|
||||
"NSPhotoLibraryAddUsageDescription" = "Used to save photo into the Photo Library";
|
|
@ -0,0 +1,303 @@
|
|||
"Common.Alerts.BlockDomain.BlockEntireDomain" = "Block entire domain";
|
||||
"Common.Alerts.BlockDomain.Title" = "Are you really, really sure you want to block the entire %@? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.";
|
||||
"Common.Alerts.Common.PleaseTryAgain" = "Please try again.";
|
||||
"Common.Alerts.Common.PleaseTryAgainLater" = "Please try again later.";
|
||||
"Common.Alerts.DeletePost.Delete" = "Delete";
|
||||
"Common.Alerts.DeletePost.Title" = "Are you sure you want to delete this post?";
|
||||
"Common.Alerts.DiscardPostContent.Message" = "Confirm discard composed post content.";
|
||||
"Common.Alerts.DiscardPostContent.Title" = "Discard Publish";
|
||||
"Common.Alerts.PublishPostFailure.Message" = "Failed to publish the post.
|
||||
Please check your internet connection.";
|
||||
"Common.Alerts.PublishPostFailure.Title" = "Publish Failure";
|
||||
"Common.Alerts.SavePhotoFailure.Message" = "Please enable photo libaray access permission to save photo.";
|
||||
"Common.Alerts.SavePhotoFailure.Title" = "Save Photo Failure";
|
||||
"Common.Alerts.ServerError.Title" = "Server Error";
|
||||
"Common.Alerts.SignOut.Confirm" = "Sign Out";
|
||||
"Common.Alerts.SignOut.Message" = "Are you sure you want to sign out?";
|
||||
"Common.Alerts.SignOut.Title" = "Sign out";
|
||||
"Common.Alerts.SignUpFailure.Title" = "Sign Up Failure";
|
||||
"Common.Alerts.VoteFailure.PollExpired" = "The poll has expired";
|
||||
"Common.Alerts.VoteFailure.Title" = "Vote Failure";
|
||||
"Common.Controls.Actions.Add" = "Add";
|
||||
"Common.Controls.Actions.Back" = "Back";
|
||||
"Common.Controls.Actions.BlockDomain" = "Block %@";
|
||||
"Common.Controls.Actions.Cancel" = "Cancel";
|
||||
"Common.Controls.Actions.Confirm" = "Confirm";
|
||||
"Common.Controls.Actions.Continue" = "Continue";
|
||||
"Common.Controls.Actions.Delete" = "Delete";
|
||||
"Common.Controls.Actions.Discard" = "Discard";
|
||||
"Common.Controls.Actions.Done" = "Done";
|
||||
"Common.Controls.Actions.Edit" = "Edit";
|
||||
"Common.Controls.Actions.FindPeople" = "Find people to follow";
|
||||
"Common.Controls.Actions.ManuallySearch" = "Manually search instead";
|
||||
"Common.Controls.Actions.Ok" = "OK";
|
||||
"Common.Controls.Actions.OpenInSafari" = "Open in Safari";
|
||||
"Common.Controls.Actions.Preview" = "Preview";
|
||||
"Common.Controls.Actions.Remove" = "Remove";
|
||||
"Common.Controls.Actions.ReportUser" = "Report %@";
|
||||
"Common.Controls.Actions.Save" = "Save";
|
||||
"Common.Controls.Actions.SavePhoto" = "Save photo";
|
||||
"Common.Controls.Actions.SeeMore" = "See More";
|
||||
"Common.Controls.Actions.Settings" = "Settings";
|
||||
"Common.Controls.Actions.Share" = "Share";
|
||||
"Common.Controls.Actions.SharePost" = "Share post";
|
||||
"Common.Controls.Actions.ShareUser" = "Share %@";
|
||||
"Common.Controls.Actions.SignIn" = "Sign In";
|
||||
"Common.Controls.Actions.SignUp" = "Sign Up";
|
||||
"Common.Controls.Actions.Skip" = "Skip";
|
||||
"Common.Controls.Actions.TakePhoto" = "Take photo";
|
||||
"Common.Controls.Actions.TryAgain" = "Try Again";
|
||||
"Common.Controls.Actions.UnblockDomain" = "Unblock %@";
|
||||
"Common.Controls.Firendship.Block" = "Block";
|
||||
"Common.Controls.Firendship.BlockDomain" = "Block %@";
|
||||
"Common.Controls.Firendship.BlockUser" = "Block %@";
|
||||
"Common.Controls.Firendship.Blocked" = "Blocked";
|
||||
"Common.Controls.Firendship.EditInfo" = "Edit info";
|
||||
"Common.Controls.Firendship.Follow" = "Follow";
|
||||
"Common.Controls.Firendship.Following" = "Following";
|
||||
"Common.Controls.Firendship.Mute" = "Mute";
|
||||
"Common.Controls.Firendship.MuteUser" = "Mute %@";
|
||||
"Common.Controls.Firendship.Muted" = "Muted";
|
||||
"Common.Controls.Firendship.Pending" = "Pending";
|
||||
"Common.Controls.Firendship.Request" = "Request";
|
||||
"Common.Controls.Firendship.Unblock" = "Unblock";
|
||||
"Common.Controls.Firendship.UnblockUser" = "Unblock %@";
|
||||
"Common.Controls.Firendship.Unmute" = "Unmute";
|
||||
"Common.Controls.Firendship.UnmuteUser" = "Unmute %@";
|
||||
"Common.Controls.Status.Actions.Favorite" = "Favorite";
|
||||
"Common.Controls.Status.Actions.Menu" = "Menu";
|
||||
"Common.Controls.Status.Actions.Reblog" = "Reblog";
|
||||
"Common.Controls.Status.Actions.Reply" = "Reply";
|
||||
"Common.Controls.Status.Actions.Unfavorite" = "Unfavorite";
|
||||
"Common.Controls.Status.Actions.Unreblog" = "Unreblog";
|
||||
"Common.Controls.Status.ContentWarning" = "content warning";
|
||||
"Common.Controls.Status.ContentWarningText" = "cw: %@";
|
||||
"Common.Controls.Status.MediaContentWarning" = "Tap to reveal that may be sensitive";
|
||||
"Common.Controls.Status.Poll.Closed" = "Closed";
|
||||
"Common.Controls.Status.Poll.TimeLeft" = "%@ left";
|
||||
"Common.Controls.Status.Poll.Vote" = "Vote";
|
||||
"Common.Controls.Status.Poll.VoteCount.Multiple" = "%d votes";
|
||||
"Common.Controls.Status.Poll.VoteCount.Single" = "%d vote";
|
||||
"Common.Controls.Status.Poll.VoterCount.Multiple" = "%d voters";
|
||||
"Common.Controls.Status.Poll.VoterCount.Single" = "%d voter";
|
||||
"Common.Controls.Status.ShowPost" = "Show Post";
|
||||
"Common.Controls.Status.ShowUserProfile" = "Show user profile";
|
||||
"Common.Controls.Status.Tag.Email" = "Email";
|
||||
"Common.Controls.Status.Tag.Emoji" = "Emoji";
|
||||
"Common.Controls.Status.Tag.Hashtag" = "Hashtag";
|
||||
"Common.Controls.Status.Tag.Link" = "Link";
|
||||
"Common.Controls.Status.Tag.Mention" = "Mention";
|
||||
"Common.Controls.Status.Tag.Url" = "URL";
|
||||
"Common.Controls.Status.UserReblogged" = "%@ reblogged";
|
||||
"Common.Controls.Status.UserRepliedTo" = "Replied to %@";
|
||||
"Common.Controls.Tabs.Home" = "Home";
|
||||
"Common.Controls.Tabs.Notification" = "Notification";
|
||||
"Common.Controls.Tabs.Profile" = "Profile";
|
||||
"Common.Controls.Tabs.Search" = "Search";
|
||||
"Common.Controls.Timeline.Accessibility.CountFavorites" = "%@ favorites";
|
||||
"Common.Controls.Timeline.Accessibility.CountReblogs" = "%@ reblogs";
|
||||
"Common.Controls.Timeline.Accessibility.CountReplies" = "%@ replies";
|
||||
"Common.Controls.Timeline.Header.BlockedWarning" = "You can’t view Artbot’s profile
|
||||
until they unblock you.";
|
||||
"Common.Controls.Timeline.Header.BlockingWarning" = "You can’t view Artbot’s profile
|
||||
until you unblock them.
|
||||
Your account looks like this to them.";
|
||||
"Common.Controls.Timeline.Header.NoStatusFound" = "No Status Found";
|
||||
"Common.Controls.Timeline.Header.SuspendedWarning" = "This account has been suspended.";
|
||||
"Common.Controls.Timeline.Header.UserSuspendedWarning" = "%@'s account has been suspended.";
|
||||
"Common.Controls.Timeline.Loader.LoadMissingPosts" = "Load missing posts";
|
||||
"Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Loading missing posts...";
|
||||
"Common.Controls.Timeline.Loader.ShowMoreReplies" = "Show more replies";
|
||||
"Common.Countable.Photo.Multiple" = "photos";
|
||||
"Common.Countable.Photo.Single" = "photo";
|
||||
"Scene.Compose.Accessibility.AppendAttachment" = "Append attachment";
|
||||
"Scene.Compose.Accessibility.AppendPoll" = "Append poll";
|
||||
"Scene.Compose.Accessibility.CustomEmojiPicker" = "Custom emoji picker";
|
||||
"Scene.Compose.Accessibility.DisableContentWarning" = "Disable content warning";
|
||||
"Scene.Compose.Accessibility.EnableContentWarning" = "Enable content warning";
|
||||
"Scene.Compose.Accessibility.InputLimitExceedsCount" = "Input limit exceeds %ld";
|
||||
"Scene.Compose.Accessibility.InputLimitRemainsCount" = "Input limit remains %ld";
|
||||
"Scene.Compose.Accessibility.PostVisibilityMenu" = "Post visibility menu";
|
||||
"Scene.Compose.Accessibility.RemovePoll" = "Remove poll";
|
||||
"Scene.Compose.Attachment.AttachmentBroken" = "This %@ is broken and can't be
|
||||
uploaded to Mastodon.";
|
||||
"Scene.Compose.Attachment.DescriptionPhoto" = "Describe photo for low vision people...";
|
||||
"Scene.Compose.Attachment.DescriptionVideo" = "Describe what’s happening for low vision people...";
|
||||
"Scene.Compose.Attachment.Photo" = "photo";
|
||||
"Scene.Compose.Attachment.Video" = "video";
|
||||
"Scene.Compose.ComposeAction" = "Publish";
|
||||
"Scene.Compose.ContentInputPlaceholder" = "Type or paste what's on your mind";
|
||||
"Scene.Compose.ContentWarning.Placeholder" = "Write an accurate warning here...";
|
||||
"Scene.Compose.MediaSelection.Browse" = "Browse";
|
||||
"Scene.Compose.MediaSelection.Camera" = "Take Photo";
|
||||
"Scene.Compose.MediaSelection.PhotoLibrary" = "Photo Library";
|
||||
"Scene.Compose.Poll.DurationTime" = "Duration: %@";
|
||||
"Scene.Compose.Poll.OneDay" = "1 Day";
|
||||
"Scene.Compose.Poll.OneHour" = "1 Hour";
|
||||
"Scene.Compose.Poll.OptionNumber" = "Option %ld";
|
||||
"Scene.Compose.Poll.SevenDays" = "7 Days";
|
||||
"Scene.Compose.Poll.SixHours" = "6 Hours";
|
||||
"Scene.Compose.Poll.ThirtyMinutes" = "30 minutes";
|
||||
"Scene.Compose.Poll.ThreeDays" = "3 Days";
|
||||
"Scene.Compose.ReplyingToUser" = "replying to %@";
|
||||
"Scene.Compose.Title.NewPost" = "New Post";
|
||||
"Scene.Compose.Title.NewReply" = "New Reply";
|
||||
"Scene.Compose.Visibility.Direct" = "Only people I mention";
|
||||
"Scene.Compose.Visibility.Private" = "Followers only";
|
||||
"Scene.Compose.Visibility.Public" = "Public";
|
||||
"Scene.Compose.Visibility.Unlisted" = "Unlisted";
|
||||
"Scene.ConfirmEmail.Button.DontReceiveEmail" = "I never got an email";
|
||||
"Scene.ConfirmEmail.Button.OpenEmailApp" = "Open Email App";
|
||||
"Scene.ConfirmEmail.DontReceiveEmail.Description" = "Check if your email address is correct as well as your junk folder if you haven’t.";
|
||||
"Scene.ConfirmEmail.DontReceiveEmail.ResendEmail" = "Resend Email";
|
||||
"Scene.ConfirmEmail.DontReceiveEmail.Title" = "Check your email";
|
||||
"Scene.ConfirmEmail.OpenEmailApp.Description" = "We just sent you an email. Check your junk folder if you haven’t.";
|
||||
"Scene.ConfirmEmail.OpenEmailApp.Mail" = "Mail";
|
||||
"Scene.ConfirmEmail.OpenEmailApp.OpenEmailClient" = "Open Email Client";
|
||||
"Scene.ConfirmEmail.OpenEmailApp.Title" = "Check your inbox.";
|
||||
"Scene.ConfirmEmail.Subtitle" = "We just sent an email to %@,
|
||||
tap the link to confirm your account.";
|
||||
"Scene.ConfirmEmail.Title" = "One last thing.";
|
||||
"Scene.Favorite.Title" = "Your Favorites";
|
||||
"Scene.Hashtag.Prompt" = "%@ people talking";
|
||||
"Scene.HomeTimeline.NavigationBarState.NewPosts" = "See new posts";
|
||||
"Scene.HomeTimeline.NavigationBarState.Offline" = "Offline";
|
||||
"Scene.HomeTimeline.NavigationBarState.Published" = "Published!";
|
||||
"Scene.HomeTimeline.NavigationBarState.Publishing" = "Publishing post...";
|
||||
"Scene.HomeTimeline.Title" = "Home";
|
||||
"Scene.Notification.Action.Favourite" = "favorited your post";
|
||||
"Scene.Notification.Action.Follow" = "followed you";
|
||||
"Scene.Notification.Action.FollowRequest" = "request to follow you";
|
||||
"Scene.Notification.Action.Mention" = "mentioned you";
|
||||
"Scene.Notification.Action.Poll" = "Your poll has ended";
|
||||
"Scene.Notification.Action.Reblog" = "rebloged your post";
|
||||
"Scene.Notification.Title.Everything" = "Everything";
|
||||
"Scene.Notification.Title.Mentions" = "Mentions";
|
||||
"Scene.Profile.Dashboard.Accessibility.CountFollowers" = "%ld followers";
|
||||
"Scene.Profile.Dashboard.Accessibility.CountFollowing" = "%ld following";
|
||||
"Scene.Profile.Dashboard.Accessibility.CountPosts" = "%ld posts";
|
||||
"Scene.Profile.Dashboard.Followers" = "followers";
|
||||
"Scene.Profile.Dashboard.Following" = "following";
|
||||
"Scene.Profile.Dashboard.Posts" = "posts";
|
||||
"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.Message" = "Confirm unblock %@";
|
||||
"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.Title" = "Unblock Account";
|
||||
"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Confirm unmute %@";
|
||||
"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Title" = "Unmute Account";
|
||||
"Scene.Profile.SegmentedControl.Media" = "Media";
|
||||
"Scene.Profile.SegmentedControl.Posts" = "Posts";
|
||||
"Scene.Profile.SegmentedControl.Replies" = "Replies";
|
||||
"Scene.Profile.Subtitle" = "%@ posts";
|
||||
"Scene.PublicTimeline.Title" = "Public";
|
||||
"Scene.Register.Error.Item.Agreement" = "Agreement";
|
||||
"Scene.Register.Error.Item.Email" = "Email";
|
||||
"Scene.Register.Error.Item.Locale" = "Locale";
|
||||
"Scene.Register.Error.Item.Password" = "Password";
|
||||
"Scene.Register.Error.Item.Reason" = "Reason";
|
||||
"Scene.Register.Error.Item.Username" = "Username";
|
||||
"Scene.Register.Error.Reason.Accepted" = "%@ must be accepted";
|
||||
"Scene.Register.Error.Reason.Blank" = "%@ is required";
|
||||
"Scene.Register.Error.Reason.Blocked" = "%@ contains a disallowed e-mail provider";
|
||||
"Scene.Register.Error.Reason.Inclusion" = "%@ is not a supported value";
|
||||
"Scene.Register.Error.Reason.Invalid" = "%@ is invalid";
|
||||
"Scene.Register.Error.Reason.Reserved" = "%@ is a reserved keyword";
|
||||
"Scene.Register.Error.Reason.Taken" = "%@ is already in use";
|
||||
"Scene.Register.Error.Reason.TooLong" = "%@ is too long";
|
||||
"Scene.Register.Error.Reason.TooShort" = "%@ is too short";
|
||||
"Scene.Register.Error.Reason.Unreachable" = "%@ does not seem to exist";
|
||||
"Scene.Register.Error.Special.EmailInvalid" = "This is not a valid e-mail address";
|
||||
"Scene.Register.Error.Special.PasswordTooShort" = "Password is too short (must be at least 8 characters)";
|
||||
"Scene.Register.Error.Special.UsernameInvalid" = "Username must only contain alphanumeric characters and underscores";
|
||||
"Scene.Register.Error.Special.UsernameTooLong" = "Username is too long (can't be longer than 30 characters)";
|
||||
"Scene.Register.Input.Avatar.Delete" = "Delete";
|
||||
"Scene.Register.Input.DisplayName.Placeholder" = "display name";
|
||||
"Scene.Register.Input.Email.Placeholder" = "email";
|
||||
"Scene.Register.Input.Invite.RegistrationUserInviteRequest" = "Why do you want to join?";
|
||||
"Scene.Register.Input.Password.Hint" = "Your password needs at least eight characters";
|
||||
"Scene.Register.Input.Password.Placeholder" = "password";
|
||||
"Scene.Register.Input.Username.DuplicatePrompt" = "This username is taken.";
|
||||
"Scene.Register.Input.Username.Placeholder" = "username";
|
||||
"Scene.Register.Title" = "Tell us about you.";
|
||||
"Scene.Report.Content1" = "Are there any other posts you’d like to add to the report?";
|
||||
"Scene.Report.Content2" = "Is there anything the moderators should know about this report?";
|
||||
"Scene.Report.Send" = "Send Report";
|
||||
"Scene.Report.SkipToSend" = "Send without comment";
|
||||
"Scene.Report.Step1" = "Step 1 of 2";
|
||||
"Scene.Report.Step2" = "Step 2 of 2";
|
||||
"Scene.Report.TextPlaceholder" = "Type or paste additional comments";
|
||||
"Scene.Report.Title" = "Report %@";
|
||||
"Scene.Search.Recommend.Accounts.Description" = "You may like to follow these accounts";
|
||||
"Scene.Search.Recommend.Accounts.Follow" = "Follow";
|
||||
"Scene.Search.Recommend.Accounts.Title" = "Accounts you might like";
|
||||
"Scene.Search.Recommend.ButtonText" = "See All";
|
||||
"Scene.Search.Recommend.HashTag.Description" = "Hashtags that are getting quite a bit of attention among people you follow";
|
||||
"Scene.Search.Recommend.HashTag.PeopleTalking" = "%@ people are talking";
|
||||
"Scene.Search.Recommend.HashTag.Title" = "Trending in your timeline";
|
||||
"Scene.Search.Searchbar.Cancel" = "Cancel";
|
||||
"Scene.Search.Searchbar.Placeholder" = "Search hashtags and users";
|
||||
"Scene.Search.Searching.Clear" = "clear";
|
||||
"Scene.Search.Searching.RecentSearch" = "Recent searches";
|
||||
"Scene.Search.Searching.Segment.All" = "All";
|
||||
"Scene.Search.Searching.Segment.Hashtags" = "Hashtags";
|
||||
"Scene.Search.Searching.Segment.People" = "People";
|
||||
"Scene.ServerPicker.Button.Category.Academia" = "academia";
|
||||
"Scene.ServerPicker.Button.Category.Activism" = "activism";
|
||||
"Scene.ServerPicker.Button.Category.All" = "All";
|
||||
"Scene.ServerPicker.Button.Category.AllAccessiblityDescription" = "Category: All";
|
||||
"Scene.ServerPicker.Button.Category.Art" = "art";
|
||||
"Scene.ServerPicker.Button.Category.Food" = "food";
|
||||
"Scene.ServerPicker.Button.Category.Furry" = "furry";
|
||||
"Scene.ServerPicker.Button.Category.Games" = "games";
|
||||
"Scene.ServerPicker.Button.Category.General" = "general";
|
||||
"Scene.ServerPicker.Button.Category.Journalism" = "journalism";
|
||||
"Scene.ServerPicker.Button.Category.Lgbt" = "lgbt";
|
||||
"Scene.ServerPicker.Button.Category.Music" = "music";
|
||||
"Scene.ServerPicker.Button.Category.Regional" = "regional";
|
||||
"Scene.ServerPicker.Button.Category.Tech" = "tech";
|
||||
"Scene.ServerPicker.Button.SeeLess" = "See Less";
|
||||
"Scene.ServerPicker.Button.SeeMore" = "See More";
|
||||
"Scene.ServerPicker.EmptyState.BadNetwork" = "Something went wrong while loading data. Check your internet connection.";
|
||||
"Scene.ServerPicker.EmptyState.FindingServers" = "Finding available servers...";
|
||||
"Scene.ServerPicker.EmptyState.NoResults" = "No results";
|
||||
"Scene.ServerPicker.Input.Placeholder" = "Find a server or join your own...";
|
||||
"Scene.ServerPicker.Label.Category" = "CATEGORY";
|
||||
"Scene.ServerPicker.Label.Language" = "LANGUAGE";
|
||||
"Scene.ServerPicker.Label.Users" = "USERS";
|
||||
"Scene.ServerPicker.Title" = "Pick a Server,
|
||||
any server.";
|
||||
"Scene.ServerRules.Button.Confirm" = "I Agree";
|
||||
"Scene.ServerRules.PrivacyPolicy" = "privacy policy";
|
||||
"Scene.ServerRules.Prompt" = "By continuing, you're subject to the terms of service and privacy policy for %@.";
|
||||
"Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@.";
|
||||
"Scene.ServerRules.TermsOfService" = "terms of service";
|
||||
"Scene.ServerRules.Title" = "Some ground rules.";
|
||||
"Scene.Settings.Section.Appearance.Automatic" = "Automatic";
|
||||
"Scene.Settings.Section.Appearance.Dark" = "Always Dark";
|
||||
"Scene.Settings.Section.Appearance.Light" = "Always Light";
|
||||
"Scene.Settings.Section.Appearance.Title" = "Appearance";
|
||||
"Scene.Settings.Section.Boringzone.Privacy" = "Privacy Policy";
|
||||
"Scene.Settings.Section.Boringzone.Terms" = "Terms of Service";
|
||||
"Scene.Settings.Section.Boringzone.Title" = "The Boring zone";
|
||||
"Scene.Settings.Section.Notifications.Boosts" = "Reblogs my post";
|
||||
"Scene.Settings.Section.Notifications.Favorites" = "Favorites my post";
|
||||
"Scene.Settings.Section.Notifications.Follows" = "Follows me";
|
||||
"Scene.Settings.Section.Notifications.Mentions" = "Mentions me";
|
||||
"Scene.Settings.Section.Notifications.Title" = "Notifications";
|
||||
"Scene.Settings.Section.Notifications.Trigger.Anyone" = "anyone";
|
||||
"Scene.Settings.Section.Notifications.Trigger.Follow" = "anyone I follow";
|
||||
"Scene.Settings.Section.Notifications.Trigger.Follower" = "a follower";
|
||||
"Scene.Settings.Section.Notifications.Trigger.Noone" = "no one";
|
||||
"Scene.Settings.Section.Notifications.Trigger.Title" = "Notify me when";
|
||||
"Scene.Settings.Section.Spicyzone.Clear" = "Clear Media Cache";
|
||||
"Scene.Settings.Section.Spicyzone.Signout" = "Sign Out";
|
||||
"Scene.Settings.Section.Spicyzone.Title" = "The spicy zone";
|
||||
"Scene.Settings.Title" = "Settings";
|
||||
"Scene.SuggestionAccount.FollowExplain" = "When you follow someone, you’ll see their posts in your home feed.";
|
||||
"Scene.SuggestionAccount.Title" = "Find People to Follow";
|
||||
"Scene.Thread.BackTitle" = "Post";
|
||||
"Scene.Thread.Favorite.Multiple" = "%@ favorites";
|
||||
"Scene.Thread.Favorite.Single" = "%@ favorite";
|
||||
"Scene.Thread.Reblog.Multiple" = "%@ reblogs";
|
||||
"Scene.Thread.Reblog.Single" = "%@ reblog";
|
||||
"Scene.Thread.Title" = "Post from %@";
|
||||
"Scene.Welcome.Slogan" = "Social networking
|
||||
back in your hands.";
|
|
@ -64,6 +64,12 @@ Please check your internet connection.";
|
|||
"Common.Controls.Firendship.UnblockUser" = "Unblock %@";
|
||||
"Common.Controls.Firendship.Unmute" = "Unmute";
|
||||
"Common.Controls.Firendship.UnmuteUser" = "Unmute %@";
|
||||
"Common.Controls.Status.Actions.Favorite" = "Favorite";
|
||||
"Common.Controls.Status.Actions.Menu" = "Menu";
|
||||
"Common.Controls.Status.Actions.Reblog" = "Reblog";
|
||||
"Common.Controls.Status.Actions.Reply" = "Reply";
|
||||
"Common.Controls.Status.Actions.Unfavorite" = "Unfavorite";
|
||||
"Common.Controls.Status.Actions.Unreblog" = "Unreblog";
|
||||
"Common.Controls.Status.ContentWarning" = "content warning";
|
||||
"Common.Controls.Status.ContentWarningText" = "cw: %@";
|
||||
"Common.Controls.Status.MediaContentWarning" = "Tap to reveal that may be sensitive";
|
||||
|
@ -75,8 +81,22 @@ Please check your internet connection.";
|
|||
"Common.Controls.Status.Poll.VoterCount.Multiple" = "%d voters";
|
||||
"Common.Controls.Status.Poll.VoterCount.Single" = "%d voter";
|
||||
"Common.Controls.Status.ShowPost" = "Show Post";
|
||||
"Common.Controls.Status.ShowUserProfile" = "Show user profile";
|
||||
"Common.Controls.Status.Tag.Email" = "Email";
|
||||
"Common.Controls.Status.Tag.Emoji" = "Emoji";
|
||||
"Common.Controls.Status.Tag.Hashtag" = "Hashtag";
|
||||
"Common.Controls.Status.Tag.Link" = "Link";
|
||||
"Common.Controls.Status.Tag.Mention" = "Mention";
|
||||
"Common.Controls.Status.Tag.Url" = "URL";
|
||||
"Common.Controls.Status.UserReblogged" = "%@ reblogged";
|
||||
"Common.Controls.Status.UserRepliedTo" = "Replied to %@";
|
||||
"Common.Controls.Tabs.Home" = "Home";
|
||||
"Common.Controls.Tabs.Notification" = "Notification";
|
||||
"Common.Controls.Tabs.Profile" = "Profile";
|
||||
"Common.Controls.Tabs.Search" = "Search";
|
||||
"Common.Controls.Timeline.Accessibility.CountFavorites" = "%@ favorites";
|
||||
"Common.Controls.Timeline.Accessibility.CountReblogs" = "%@ reblogs";
|
||||
"Common.Controls.Timeline.Accessibility.CountReplies" = "%@ replies";
|
||||
"Common.Controls.Timeline.Header.BlockedWarning" = "You can’t view Artbot’s profile
|
||||
until they unblock you.";
|
||||
"Common.Controls.Timeline.Header.BlockingWarning" = "You can’t view Artbot’s profile
|
||||
|
@ -90,6 +110,15 @@ Your account looks like this to them.";
|
|||
"Common.Controls.Timeline.Loader.ShowMoreReplies" = "Show more replies";
|
||||
"Common.Countable.Photo.Multiple" = "photos";
|
||||
"Common.Countable.Photo.Single" = "photo";
|
||||
"Scene.Compose.Accessibility.AppendAttachment" = "Append attachment";
|
||||
"Scene.Compose.Accessibility.AppendPoll" = "Append poll";
|
||||
"Scene.Compose.Accessibility.CustomEmojiPicker" = "Custom emoji picker";
|
||||
"Scene.Compose.Accessibility.DisableContentWarning" = "Disable content warning";
|
||||
"Scene.Compose.Accessibility.EnableContentWarning" = "Enable content warning";
|
||||
"Scene.Compose.Accessibility.InputLimitExceedsCount" = "Input limit exceeds %ld";
|
||||
"Scene.Compose.Accessibility.InputLimitRemainsCount" = "Input limit remains %ld";
|
||||
"Scene.Compose.Accessibility.PostVisibilityMenu" = "Post visibility menu";
|
||||
"Scene.Compose.Accessibility.RemovePoll" = "Remove poll";
|
||||
"Scene.Compose.Attachment.AttachmentBroken" = "This %@ is broken and can't be
|
||||
uploaded to Mastodon.";
|
||||
"Scene.Compose.Attachment.DescriptionPhoto" = "Describe photo for low vision people...";
|
||||
|
@ -144,6 +173,9 @@ tap the link to confirm your account.";
|
|||
"Scene.Notification.Action.Reblog" = "rebloged your post";
|
||||
"Scene.Notification.Title.Everything" = "Everything";
|
||||
"Scene.Notification.Title.Mentions" = "Mentions";
|
||||
"Scene.Profile.Dashboard.Accessibility.CountFollowers" = "%ld followers";
|
||||
"Scene.Profile.Dashboard.Accessibility.CountFollowing" = "%ld following";
|
||||
"Scene.Profile.Dashboard.Accessibility.CountPosts" = "%ld posts";
|
||||
"Scene.Profile.Dashboard.Followers" = "followers";
|
||||
"Scene.Profile.Dashboard.Following" = "following";
|
||||
"Scene.Profile.Dashboard.Posts" = "posts";
|
||||
|
@ -193,7 +225,7 @@ tap the link to confirm your account.";
|
|||
"Scene.Report.Step2" = "Step 2 of 2";
|
||||
"Scene.Report.TextPlaceholder" = "Type or paste additional comments";
|
||||
"Scene.Report.Title" = "Report %@";
|
||||
"Scene.Search.Recommend.Accounts.Description" = "Except for Sam, you will not like his account.";
|
||||
"Scene.Search.Recommend.Accounts.Description" = "You may like to follow these accounts";
|
||||
"Scene.Search.Recommend.Accounts.Follow" = "Follow";
|
||||
"Scene.Search.Recommend.Accounts.Title" = "Accounts you might like";
|
||||
"Scene.Search.Recommend.ButtonText" = "See All";
|
||||
|
@ -207,11 +239,25 @@ tap the link to confirm your account.";
|
|||
"Scene.Search.Searching.Segment.All" = "All";
|
||||
"Scene.Search.Searching.Segment.Hashtags" = "Hashtags";
|
||||
"Scene.Search.Searching.Segment.People" = "People";
|
||||
"Scene.ServerPicker.Button.Category.Academia" = "academia";
|
||||
"Scene.ServerPicker.Button.Category.Activism" = "activism";
|
||||
"Scene.ServerPicker.Button.Category.All" = "All";
|
||||
"Scene.ServerPicker.Button.Category.AllAccessiblityDescription" = "Category: All";
|
||||
"Scene.ServerPicker.Button.Category.Art" = "art";
|
||||
"Scene.ServerPicker.Button.Category.Food" = "food";
|
||||
"Scene.ServerPicker.Button.Category.Furry" = "furry";
|
||||
"Scene.ServerPicker.Button.Category.Games" = "games";
|
||||
"Scene.ServerPicker.Button.Category.General" = "general";
|
||||
"Scene.ServerPicker.Button.Category.Journalism" = "journalism";
|
||||
"Scene.ServerPicker.Button.Category.Lgbt" = "lgbt";
|
||||
"Scene.ServerPicker.Button.Category.Music" = "music";
|
||||
"Scene.ServerPicker.Button.Category.Regional" = "regional";
|
||||
"Scene.ServerPicker.Button.Category.Tech" = "tech";
|
||||
"Scene.ServerPicker.Button.SeeLess" = "See Less";
|
||||
"Scene.ServerPicker.Button.SeeMore" = "See More";
|
||||
"Scene.ServerPicker.EmptyState.BadNetwork" = "Something went wrong while loading data. Check your internet connection.";
|
||||
"Scene.ServerPicker.EmptyState.FindingServers" = "Finding available servers...";
|
||||
"Scene.ServerPicker.EmptyState.NoResults" = "No results";
|
||||
"Scene.ServerPicker.Input.Placeholder" = "Find a server or join your own...";
|
||||
"Scene.ServerPicker.Label.Category" = "CATEGORY";
|
||||
"Scene.ServerPicker.Label.Language" = "LANGUAGE";
|
||||
|
|
|
@ -47,6 +47,10 @@ extension CustomEmojiPickerItemCollectionViewCell {
|
|||
emojiImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||
emojiImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
])
|
||||
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits = .button
|
||||
accessibilityHint = "emoji"
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -260,6 +260,21 @@ extension ComposeViewController {
|
|||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: \.isEnabled, on: composeToolbarView.pollButton)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
Publishers.CombineLatest(
|
||||
viewModel.isPollComposing,
|
||||
viewModel.isPollToolbarButtonEnabled
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] isPollComposing, isPollToolbarButtonEnabled in
|
||||
guard let self = self else { return }
|
||||
guard isPollToolbarButtonEnabled else {
|
||||
self.composeToolbarView.pollButton.accessibilityLabel = L10n.Scene.Compose.Accessibility.appendPoll
|
||||
return
|
||||
}
|
||||
self.composeToolbarView.pollButton.accessibilityLabel = isPollComposing ? L10n.Scene.Compose.Accessibility.removePoll : L10n.Scene.Compose.Accessibility.appendPoll
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind image picker toolbar state
|
||||
viewModel.attachmentServices
|
||||
|
@ -271,6 +286,15 @@ extension ComposeViewController {
|
|||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind content warning button state
|
||||
viewModel.isContentWarningComposing
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] isContentWarningComposing in
|
||||
guard let self = self else { return }
|
||||
self.composeToolbarView.contentWarningButton.accessibilityLabel = isContentWarningComposing ? L10n.Scene.Compose.Accessibility.disableContentWarning : L10n.Scene.Compose.Accessibility.enableContentWarning
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind visibility toolbar UI
|
||||
Publishers.CombineLatest(
|
||||
viewModel.selectedStatusVisibility,
|
||||
|
@ -294,9 +318,11 @@ extension ComposeViewController {
|
|||
case _ where count < 0:
|
||||
self.composeToolbarView.characterCountLabel.font = .systemFont(ofSize: 24, weight: .bold)
|
||||
self.composeToolbarView.characterCountLabel.textColor = Asset.Colors.danger.color
|
||||
self.composeToolbarView.characterCountLabel.accessibilityLabel = L10n.Scene.Compose.Accessibility.inputLimitExceedsCount(abs(count))
|
||||
default:
|
||||
self.composeToolbarView.characterCountLabel.font = .systemFont(ofSize: 15, weight: .regular)
|
||||
self.composeToolbarView.characterCountLabel.textColor = Asset.Colors.Label.secondary.color
|
||||
self.composeToolbarView.characterCountLabel.accessibilityLabel = L10n.Scene.Compose.Accessibility.inputLimitRemainsCount(count)
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
@ -552,6 +578,7 @@ extension ComposeViewController {
|
|||
collectionView.updateInteractiveMovementTargetPosition(position)
|
||||
case .ended:
|
||||
collectionView.endInteractiveMovement()
|
||||
collectionView.reloadData()
|
||||
default:
|
||||
collectionView.cancelInteractiveMovement()
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ final class AttachmentContainerView: UIView {
|
|||
textView.showsVerticalScrollIndicator = false
|
||||
textView.backgroundColor = .clear
|
||||
textView.textColor = .white
|
||||
textView.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15))
|
||||
textView.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15), maximumPointSize: 20)
|
||||
textView.placeholder = L10n.Scene.Compose.Attachment.descriptionPhoto
|
||||
textView.placeholderColor = UIColor.white.withAlphaComponent(0.6) // force white with alpha for Light/Dark mode
|
||||
textView.returnKeyType = .done
|
||||
|
|
|
@ -28,6 +28,7 @@ final class ComposeToolbarView: UIView {
|
|||
let button = HighlightDimmableButton()
|
||||
ComposeToolbarView.configureToolbarButtonAppearance(button: button)
|
||||
button.setImage(UIImage(systemName: "photo", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal)
|
||||
button.accessibilityLabel = L10n.Scene.Compose.Accessibility.appendAttachment
|
||||
return button
|
||||
}()
|
||||
|
||||
|
@ -35,6 +36,7 @@ final class ComposeToolbarView: UIView {
|
|||
let button = HighlightDimmableButton(type: .custom)
|
||||
ComposeToolbarView.configureToolbarButtonAppearance(button: button)
|
||||
button.setImage(UIImage(systemName: "list.bullet", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .medium)), for: .normal)
|
||||
button.accessibilityLabel = L10n.Scene.Compose.Accessibility.appendPoll
|
||||
return button
|
||||
}()
|
||||
|
||||
|
@ -45,6 +47,7 @@ final class ComposeToolbarView: UIView {
|
|||
.af.imageScaled(to: CGSize(width: 20, height: 20))
|
||||
.withRenderingMode(.alwaysTemplate)
|
||||
button.setImage(image, for: .normal)
|
||||
button.accessibilityLabel = L10n.Scene.Compose.Accessibility.customEmojiPicker
|
||||
return button
|
||||
}()
|
||||
|
||||
|
@ -52,6 +55,7 @@ final class ComposeToolbarView: UIView {
|
|||
let button = HighlightDimmableButton()
|
||||
ComposeToolbarView.configureToolbarButtonAppearance(button: button)
|
||||
button.setImage(UIImage(systemName: "exclamationmark.shield", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal)
|
||||
button.accessibilityLabel = L10n.Scene.Compose.Accessibility.enableContentWarning
|
||||
return button
|
||||
}()
|
||||
|
||||
|
@ -59,6 +63,7 @@ final class ComposeToolbarView: UIView {
|
|||
let button = HighlightDimmableButton()
|
||||
ComposeToolbarView.configureToolbarButtonAppearance(button: button)
|
||||
button.setImage(UIImage(systemName: "person.3", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium)), for: .normal)
|
||||
button.accessibilityLabel = L10n.Scene.Compose.Accessibility.postVisibilityMenu
|
||||
return button
|
||||
}()
|
||||
|
||||
|
@ -67,6 +72,7 @@ final class ComposeToolbarView: UIView {
|
|||
label.font = .systemFont(ofSize: 15, weight: .regular)
|
||||
label.text = "500"
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
label.accessibilityLabel = L10n.Scene.Compose.Accessibility.inputLimitRemainsCount(500)
|
||||
return label
|
||||
}()
|
||||
|
||||
|
|
|
@ -25,10 +25,10 @@ class MainTabBarController: UITabBarController {
|
|||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .home: return "Home"
|
||||
case .search: return "Search"
|
||||
case .notification: return "Notification"
|
||||
case .me: return "Me"
|
||||
case .home: return L10n.Common.Controls.Tabs.home
|
||||
case .search: return L10n.Common.Controls.Tabs.search
|
||||
case .notification: return L10n.Common.Controls.Tabs.notification
|
||||
case .me: return L10n.Common.Controls.Tabs.profile
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,6 +99,7 @@ extension MainTabBarController {
|
|||
let viewController = tab.viewController(context: context, coordinator: coordinator)
|
||||
viewController.tabBarItem.title = "" // set text to empty string for image only style (SDK failed to layout when set to nil)
|
||||
viewController.tabBarItem.image = tab.image
|
||||
viewController.tabBarItem.accessibilityLabel = tab.title
|
||||
return viewController
|
||||
}
|
||||
setViewControllers(viewControllers, animated: false)
|
||||
|
|
|
@ -36,7 +36,7 @@ final class MediaPreviewViewModel: NSObject {
|
|||
switch entity.type {
|
||||
case .image:
|
||||
guard let url = URL(string: entity.url) else { continue }
|
||||
let meta = MediaPreviewImageViewModel.RemoteImagePreviewMeta(url: url, thumbnail: thumbnail)
|
||||
let meta = MediaPreviewImageViewModel.RemoteImagePreviewMeta(url: url, thumbnail: thumbnail, altText: entity.descriptionString)
|
||||
let mediaPreviewImageModel = MediaPreviewImageViewModel(meta: meta)
|
||||
let mediaPreviewImageViewController = MediaPreviewImageViewController()
|
||||
mediaPreviewImageViewController.viewModel = mediaPreviewImageModel
|
||||
|
@ -60,7 +60,7 @@ final class MediaPreviewViewModel: NSObject {
|
|||
managedObjectContext.performAndWait {
|
||||
let account = managedObjectContext.object(with: meta.accountObjectID) as! MastodonUser
|
||||
let avatarURL = account.headerImageURLWithFallback(domain: account.domain)
|
||||
let meta = MediaPreviewImageViewModel.RemoteImagePreviewMeta(url: avatarURL, thumbnail: meta.preloadThumbnailImage)
|
||||
let meta = MediaPreviewImageViewModel.RemoteImagePreviewMeta(url: avatarURL, thumbnail: meta.preloadThumbnailImage, altText: nil)
|
||||
let mediaPreviewImageModel = MediaPreviewImageViewModel(meta: meta)
|
||||
let mediaPreviewImageViewController = MediaPreviewImageViewController()
|
||||
mediaPreviewImageViewController.viewModel = mediaPreviewImageModel
|
||||
|
@ -80,7 +80,7 @@ final class MediaPreviewViewModel: NSObject {
|
|||
managedObjectContext.performAndWait {
|
||||
let account = managedObjectContext.object(with: meta.accountObjectID) as! MastodonUser
|
||||
let avatarURL = account.avatarImageURLWithFallback(domain: account.domain)
|
||||
let meta = MediaPreviewImageViewModel.RemoteImagePreviewMeta(url: avatarURL, thumbnail: meta.preloadThumbnailImage)
|
||||
let meta = MediaPreviewImageViewModel.RemoteImagePreviewMeta(url: avatarURL, thumbnail: meta.preloadThumbnailImage, altText: nil)
|
||||
let mediaPreviewImageModel = MediaPreviewImageViewModel(meta: meta)
|
||||
let mediaPreviewImageViewController = MediaPreviewImageViewController()
|
||||
mediaPreviewImageViewController.viewModel = mediaPreviewImageModel
|
||||
|
|
|
@ -16,6 +16,9 @@ final class MediaPreviewImageView: UIScrollView {
|
|||
imageView.contentMode = .scaleAspectFit
|
||||
imageView.clipsToBounds = true
|
||||
imageView.isUserInteractionEnabled = true
|
||||
// accessibility
|
||||
imageView.accessibilityIgnoresInvertColors = true
|
||||
imageView.isAccessibilityElement = true
|
||||
return imageView
|
||||
}()
|
||||
|
||||
|
|
|
@ -76,6 +76,7 @@ extension MediaPreviewImageViewController {
|
|||
guard let image = image else { return }
|
||||
self.previewImageView.imageView.image = image
|
||||
self.previewImageView.setup(image: image, container: self.previewImageView, forceUpdate: true)
|
||||
self.previewImageView.imageView.accessibilityLabel = self.viewModel.altText
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
|
|
@ -17,10 +17,12 @@ class MediaPreviewImageViewModel {
|
|||
|
||||
// output
|
||||
let image: CurrentValueSubject<UIImage?, Never>
|
||||
let altText: String?
|
||||
|
||||
init(meta: RemoteImagePreviewMeta) {
|
||||
self.item = .status(meta)
|
||||
self.image = CurrentValueSubject(meta.thumbnail)
|
||||
self.altText = meta.altText
|
||||
|
||||
let url = meta.url
|
||||
ImageDownloader.default.download(URLRequest(url: url), completion: { [weak self] response in
|
||||
|
@ -38,6 +40,7 @@ class MediaPreviewImageViewModel {
|
|||
init(meta: LocalImagePreviewMeta) {
|
||||
self.item = .local(meta)
|
||||
self.image = CurrentValueSubject(meta.image)
|
||||
self.altText = nil
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -64,6 +67,7 @@ extension MediaPreviewImageViewModel {
|
|||
struct RemoteImagePreviewMeta {
|
||||
let url: URL
|
||||
let thumbnail: UIImage?
|
||||
let altText: String?
|
||||
}
|
||||
|
||||
struct LocalImagePreviewMeta {
|
||||
|
|
|
@ -28,14 +28,14 @@ final class NotificationViewController: UIViewController, NeedsDependency {
|
|||
|
||||
let tableView: UITableView = {
|
||||
let tableView = ControlContainableTableView()
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.separatorStyle = .singleLine
|
||||
tableView.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
|
||||
tableView.register(NotificationTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationTableViewCell.self))
|
||||
tableView.register(NotificationStatusTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationStatusTableViewCell.self))
|
||||
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
|
||||
tableView.tableFooterView = UIView()
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.estimatedRowHeight = UITableView.automaticDimension
|
||||
tableView.separatorStyle = .none
|
||||
tableView.tableFooterView = UIView()
|
||||
tableView.backgroundColor = .clear
|
||||
return tableView
|
||||
}()
|
||||
|
|
|
@ -50,7 +50,7 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell {
|
|||
let actionLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
label.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular), maximumPointSize: 20)
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
return label
|
||||
}()
|
||||
|
@ -58,7 +58,7 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell {
|
|||
let nameLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.brandBlue.color
|
||||
label.font = .systemFont(ofSize: 15, weight: .semibold)
|
||||
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold), maximumPointSize: 20)
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
return label
|
||||
}()
|
||||
|
@ -76,6 +76,14 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell {
|
|||
|
||||
let statusView = StatusView()
|
||||
|
||||
let separatorLine = UIView.separatorLine
|
||||
|
||||
var separatorLineToEdgeLeadingLayoutConstraint: NSLayoutConstraint!
|
||||
var separatorLineToEdgeTrailingLayoutConstraint: NSLayoutConstraint!
|
||||
|
||||
var separatorLineToMarginLeadingLayoutConstraint: NSLayoutConstraint!
|
||||
var separatorLineToMarginTrailingLayoutConstraint: NSLayoutConstraint!
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
avatatImageView.af.cancelImageRequest()
|
||||
|
@ -197,6 +205,18 @@ extension NotificationStatusTableViewCell {
|
|||
|
||||
containerStackView.addArrangedSubview(statusStackView)
|
||||
|
||||
separatorLine.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(separatorLine)
|
||||
separatorLineToEdgeLeadingLayoutConstraint = separatorLine.leadingAnchor.constraint(equalTo: contentView.leadingAnchor)
|
||||
separatorLineToEdgeTrailingLayoutConstraint = separatorLine.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
|
||||
separatorLineToMarginLeadingLayoutConstraint = separatorLine.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor)
|
||||
separatorLineToMarginTrailingLayoutConstraint = separatorLine.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor)
|
||||
NSLayoutConstraint.activate([
|
||||
separatorLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)),
|
||||
])
|
||||
resetSeparatorLineLayout()
|
||||
|
||||
// remove item don't display
|
||||
statusView.actionToolbarContainer.removeFromStackView()
|
||||
// it affect stackView's height,need remove
|
||||
|
@ -206,6 +226,8 @@ extension NotificationStatusTableViewCell {
|
|||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
resetSeparatorLineLayout()
|
||||
statusBorder.layer.borderColor = Asset.Colors.Border.notification.color.cgColor
|
||||
actionImageBackground.layer.borderColor = Asset.Colors.Background.systemBackground.color.cgColor
|
||||
}
|
||||
|
@ -258,5 +280,37 @@ extension NotificationStatusTableViewCell: StatusViewDelegate {
|
|||
// do nothing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension NotificationStatusTableViewCell {
|
||||
|
||||
private func resetSeparatorLineLayout() {
|
||||
separatorLineToEdgeLeadingLayoutConstraint.isActive = false
|
||||
separatorLineToEdgeTrailingLayoutConstraint.isActive = false
|
||||
separatorLineToMarginLeadingLayoutConstraint.isActive = false
|
||||
separatorLineToMarginTrailingLayoutConstraint.isActive = false
|
||||
|
||||
if traitCollection.userInterfaceIdiom == .phone {
|
||||
// to edge
|
||||
NSLayoutConstraint.activate([
|
||||
separatorLineToEdgeLeadingLayoutConstraint,
|
||||
separatorLineToEdgeTrailingLayoutConstraint,
|
||||
])
|
||||
} else {
|
||||
if traitCollection.horizontalSizeClass == .compact {
|
||||
// to edge
|
||||
NSLayoutConstraint.activate([
|
||||
separatorLineToEdgeLeadingLayoutConstraint,
|
||||
separatorLineToEdgeTrailingLayoutConstraint,
|
||||
])
|
||||
} else {
|
||||
// to margin
|
||||
NSLayoutConstraint.activate([
|
||||
separatorLineToMarginLeadingLayoutConstraint,
|
||||
separatorLineToMarginTrailingLayoutConstraint,
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@ final class NotificationTableViewCell: UITableViewCell {
|
|||
let actionLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
label.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular), maximumPointSize: 20)
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
return label
|
||||
}()
|
||||
|
@ -75,7 +75,7 @@ final class NotificationTableViewCell: UITableViewCell {
|
|||
let nameLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.brandBlue.color
|
||||
label.font = .systemFont(ofSize: 15, weight: .semibold)
|
||||
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold), maximumPointSize: 20)
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
return label
|
||||
}()
|
||||
|
@ -98,6 +98,14 @@ final class NotificationTableViewCell: UITableViewCell {
|
|||
|
||||
let buttonStackView = UIStackView()
|
||||
|
||||
let separatorLine = UIView.separatorLine
|
||||
|
||||
var separatorLineToEdgeLeadingLayoutConstraint: NSLayoutConstraint!
|
||||
var separatorLineToEdgeTrailingLayoutConstraint: NSLayoutConstraint!
|
||||
|
||||
var separatorLineToMarginLeadingLayoutConstraint: NSLayoutConstraint!
|
||||
var separatorLineToMarginTrailingLayoutConstraint: NSLayoutConstraint!
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
avatatImageView.af.cancelImageRequest()
|
||||
|
@ -187,10 +195,57 @@ extension NotificationTableViewCell {
|
|||
buttonStackView.addArrangedSubview(acceptButton)
|
||||
buttonStackView.addArrangedSubview(rejectButton)
|
||||
containerStackView.addArrangedSubview(buttonStackView)
|
||||
|
||||
separatorLine.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(separatorLine)
|
||||
separatorLineToEdgeLeadingLayoutConstraint = separatorLine.leadingAnchor.constraint(equalTo: contentView.leadingAnchor)
|
||||
separatorLineToEdgeTrailingLayoutConstraint = separatorLine.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
|
||||
separatorLineToMarginLeadingLayoutConstraint = separatorLine.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor)
|
||||
separatorLineToMarginTrailingLayoutConstraint = separatorLine.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor)
|
||||
NSLayoutConstraint.activate([
|
||||
separatorLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)),
|
||||
])
|
||||
resetSeparatorLineLayout()
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
actionImageBackground.layer.borderColor = Asset.Colors.Background.systemBackground.color.cgColor
|
||||
resetSeparatorLineLayout()
|
||||
}
|
||||
}
|
||||
|
||||
extension NotificationTableViewCell {
|
||||
|
||||
private func resetSeparatorLineLayout() {
|
||||
separatorLineToEdgeLeadingLayoutConstraint.isActive = false
|
||||
separatorLineToEdgeTrailingLayoutConstraint.isActive = false
|
||||
separatorLineToMarginLeadingLayoutConstraint.isActive = false
|
||||
separatorLineToMarginTrailingLayoutConstraint.isActive = false
|
||||
|
||||
if traitCollection.userInterfaceIdiom == .phone {
|
||||
// to edge
|
||||
NSLayoutConstraint.activate([
|
||||
separatorLineToEdgeLeadingLayoutConstraint,
|
||||
separatorLineToEdgeTrailingLayoutConstraint,
|
||||
])
|
||||
} else {
|
||||
if traitCollection.horizontalSizeClass == .compact {
|
||||
// to edge
|
||||
NSLayoutConstraint.activate([
|
||||
separatorLineToEdgeLeadingLayoutConstraint,
|
||||
separatorLineToEdgeTrailingLayoutConstraint,
|
||||
])
|
||||
} else {
|
||||
// to margin
|
||||
NSLayoutConstraint.activate([
|
||||
separatorLineToMarginLeadingLayoutConstraint,
|
||||
separatorLineToMarginTrailingLayoutConstraint,
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ final class MastodonPickServerViewController: UIViewController, NeedsDependency
|
|||
tableView.register(PickServerCategoriesCell.self, forCellReuseIdentifier: String(describing: PickServerCategoriesCell.self))
|
||||
tableView.register(PickServerSearchCell.self, forCellReuseIdentifier: String(describing: PickServerSearchCell.self))
|
||||
tableView.register(PickServerCell.self, forCellReuseIdentifier: String(describing: PickServerCell.self))
|
||||
tableView.register(PickServerLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: PickServerLoaderTableViewCell.self))
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.separatorStyle = .none
|
||||
tableView.backgroundColor = .clear
|
||||
|
|
|
@ -39,7 +39,7 @@ class MastodonPickServerViewModel: NSObject {
|
|||
let selectCategoryItem = CurrentValueSubject<CategoryPickerItem, Never>(.all)
|
||||
let searchText = CurrentValueSubject<String, 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>()
|
||||
|
||||
// output
|
||||
|
@ -85,8 +85,8 @@ extension MastodonPickServerViewModel {
|
|||
|
||||
private func configure() {
|
||||
Publishers.CombineLatest(
|
||||
filteredIndexedServers.eraseToAnyPublisher(),
|
||||
unindexedServers.eraseToAnyPublisher()
|
||||
filteredIndexedServers,
|
||||
unindexedServers
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveValue: { [weak self] indexedServers, unindexedServers in
|
||||
|
@ -114,16 +114,31 @@ extension MastodonPickServerViewModel {
|
|||
guard !serverItems.contains(item) else { continue }
|
||||
serverItems.append(item)
|
||||
}
|
||||
for server in unindexedServers {
|
||||
let attribute = oldSnapshotServerItemAttributeDict[server.domain] ?? PickServerItem.ServerItemAttribute(isLast: false, isExpand: false)
|
||||
attribute.isLast = false
|
||||
let item = PickServerItem.server(server: server, attribute: attribute)
|
||||
guard !serverItems.contains(item) else { continue }
|
||||
serverItems.append(item)
|
||||
|
||||
if let unindexedServers = unindexedServers {
|
||||
if !unindexedServers.isEmpty {
|
||||
for server in unindexedServers {
|
||||
let attribute = oldSnapshotServerItemAttributeDict[server.domain] ?? PickServerItem.ServerItemAttribute(isLast: false, isExpand: false)
|
||||
attribute.isLast = false
|
||||
let item = PickServerItem.server(server: server, attribute: attribute)
|
||||
guard !serverItems.contains(item) else { continue }
|
||||
serverItems.append(item)
|
||||
}
|
||||
} else {
|
||||
if indexedServers.isEmpty && !self.isLoadingIndexedServers.value {
|
||||
serverItems.append(.loader(attribute: PickServerItem.LoaderItemAttribute(isLast: false, isEmptyResult: true)))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
serverItems.append(.loader(attribute: PickServerItem.LoaderItemAttribute(isLast: false, isEmptyResult: false)))
|
||||
}
|
||||
|
||||
if case let .server(_, attribute) = serverItems.last {
|
||||
attribute.isLast = true
|
||||
}
|
||||
if case let .loader(attribute) = serverItems.last {
|
||||
attribute.isLast = true
|
||||
}
|
||||
snapshot.appendItems(serverItems, toSection: .servers)
|
||||
|
||||
diffableDataSource.defaultRowAnimation = .fade
|
||||
|
@ -168,6 +183,7 @@ extension MastodonPickServerViewModel {
|
|||
guard let domain = AuthenticationViewModel.parseDomain(from: searchText) else {
|
||||
return Just(Result.failure(APIService.APIError.implicit(.badRequest))).eraseToAnyPublisher()
|
||||
}
|
||||
self.unindexedServers.value = nil
|
||||
return self.context.apiService.instance(domain: domain)
|
||||
.map { response -> Result<Mastodon.Response.Content<[Mastodon.Entity.Server]>, Error>in
|
||||
let newResponse = response.map { [Mastodon.Entity.Server(instance: $0)] }
|
||||
|
@ -184,9 +200,14 @@ extension MastodonPickServerViewModel {
|
|||
switch result {
|
||||
case .success(let response):
|
||||
self.unindexedServers.send(response.value)
|
||||
case .failure:
|
||||
// TODO: What should be presented when user inputs invalid search text?
|
||||
self.unindexedServers.send([])
|
||||
case .failure(let error):
|
||||
if let error = error as? APIService.APIError,
|
||||
case let .implicit(reason) = error,
|
||||
case .badRequest = reason {
|
||||
self.unindexedServers.send([])
|
||||
} else {
|
||||
self.unindexedServers.send(nil)
|
||||
}
|
||||
}
|
||||
})
|
||||
.store(in: &disposeBag)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ class PickServerCell: UITableViewCell {
|
|||
|
||||
let domainLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .preferredFont(forTextStyle: .headline)
|
||||
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 22)
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
@ -52,7 +52,7 @@ class PickServerCell: UITableViewCell {
|
|||
|
||||
let descriptionLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .preferredFont(forTextStyle: .subheadline)
|
||||
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))
|
||||
label.numberOfLines = 0
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
|
@ -88,11 +88,14 @@ class PickServerCell: UITableViewCell {
|
|||
|
||||
let expandButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.setImage(UIImage(systemName: "chevron.down", withConfiguration: UIImage.SymbolConfiguration(pointSize: 13)), for: .normal)
|
||||
button.setTitle(L10n.Scene.ServerPicker.Button.seeMore, for: .normal)
|
||||
button.setTitle(L10n.Scene.ServerPicker.Button.seeLess, for: .selected)
|
||||
button.setTitleColor(Asset.Colors.brandBlue.color, for: .normal)
|
||||
button.titleLabel?.font = .preferredFont(forTextStyle: .footnote)
|
||||
button.titleLabel?.font = .systemFont(ofSize: 13, weight: .regular)
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
button.imageView?.transform = CGAffineTransform(scaleX: -1, y: 1)
|
||||
button.titleLabel?.transform = CGAffineTransform(scaleX: -1, y: 1)
|
||||
button.transform = CGAffineTransform(scaleX: -1, y: 1)
|
||||
return button
|
||||
}()
|
||||
|
||||
|
@ -106,7 +109,7 @@ class PickServerCell: UITableViewCell {
|
|||
let langValueLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: UIFont.systemFont(ofSize: 22, weight: .semibold))
|
||||
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 22, weight: .semibold), maximumPointSize: 27)
|
||||
label.textAlignment = .center
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
@ -116,7 +119,7 @@ class PickServerCell: UITableViewCell {
|
|||
let usersValueLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: UIFont.systemFont(ofSize: 22, weight: .semibold))
|
||||
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 22, weight: .semibold), maximumPointSize: 27)
|
||||
label.textAlignment = .center
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
@ -126,7 +129,7 @@ class PickServerCell: UITableViewCell {
|
|||
let categoryValueLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: UIFont.systemFont(ofSize: 22, weight: .semibold))
|
||||
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 22, weight: .semibold), maximumPointSize: 27)
|
||||
label.textAlignment = .center
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
@ -136,7 +139,7 @@ class PickServerCell: UITableViewCell {
|
|||
let langTitleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.font = .preferredFont(forTextStyle: .caption2)
|
||||
label.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 11, weight: .regular), maximumPointSize: 16)
|
||||
label.text = L10n.Scene.ServerPicker.Label.language
|
||||
label.textAlignment = .center
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
|
@ -147,7 +150,7 @@ class PickServerCell: UITableViewCell {
|
|||
let usersTitleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.font = .preferredFont(forTextStyle: .caption2)
|
||||
label.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 11, weight: .regular), maximumPointSize: 16)
|
||||
label.text = L10n.Scene.ServerPicker.Label.users
|
||||
label.textAlignment = .center
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
|
@ -158,7 +161,7 @@ class PickServerCell: UITableViewCell {
|
|||
let categoryTitleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.font = .preferredFont(forTextStyle: .caption2)
|
||||
label.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 11, weight: .regular), maximumPointSize: 16)
|
||||
label.text = L10n.Scene.ServerPicker.Label.category
|
||||
label.textAlignment = .center
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
|
@ -325,11 +328,15 @@ extension PickServerCell {
|
|||
func updateExpandMode(mode: ExpandMode) {
|
||||
switch mode {
|
||||
case .collapse:
|
||||
expandButton.setImage(UIImage(systemName: "chevron.down", withConfiguration: UIImage.SymbolConfiguration(pointSize: 13)), for: .normal)
|
||||
expandButton.setTitle(L10n.Scene.ServerPicker.Button.seeMore, for: .normal)
|
||||
expandBox.isHidden = true
|
||||
expandButton.isSelected = false
|
||||
NSLayoutConstraint.deactivate(expandConstraints)
|
||||
NSLayoutConstraint.activate(collapseConstraints)
|
||||
case .expand:
|
||||
expandButton.setImage(UIImage(systemName: "chevron.up", withConfiguration: UIImage.SymbolConfiguration(pointSize: 13)), for: .normal)
|
||||
expandButton.setTitle(L10n.Scene.ServerPicker.Button.seeLess, for: .normal)
|
||||
expandBox.isHidden = false
|
||||
expandButton.isSelected = true
|
||||
NSLayoutConstraint.activate(expandConstraints)
|
||||
|
|
|
@ -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
|
|
@ -16,6 +16,9 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
|||
|
||||
static let avatarImageMaxSizeInPixel = CGSize(width: 400, height: 400)
|
||||
|
||||
static let textFieldLabelFont = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 22)
|
||||
static let errorPromptLabelFont = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 13, weight: .semibold), maximumPointSize: 18)
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
|
@ -63,6 +66,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
|||
label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: UIFont.boldSystemFont(ofSize: 34))
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.text = L10n.Scene.Register.title
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
|
@ -99,7 +103,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
|||
|
||||
let domainLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .preferredFont(forTextStyle: .headline)
|
||||
label.font = MastodonRegisterViewController.textFieldLabelFont
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
return label
|
||||
}()
|
||||
|
@ -113,7 +117,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
|||
textField.textColor = Asset.Colors.Label.primary.color
|
||||
textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Username.placeholder,
|
||||
attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color,
|
||||
NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)])
|
||||
NSAttributedString.Key.font: MastodonRegisterViewController.textFieldLabelFont])
|
||||
textField.borderStyle = UITextField.BorderStyle.roundedRect
|
||||
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height))
|
||||
textField.leftView = paddingView
|
||||
|
@ -124,7 +128,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
|||
let usernameErrorPromptLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
let color = Asset.Colors.danger.color
|
||||
let font = UIFont.preferredFont(forTextStyle: .caption1)
|
||||
let font = MastodonRegisterViewController.errorPromptLabelFont
|
||||
return label
|
||||
}()
|
||||
|
||||
|
@ -136,7 +140,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
|||
textField.textColor = Asset.Colors.Label.primary.color
|
||||
textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.DisplayName.placeholder,
|
||||
attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color,
|
||||
NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)])
|
||||
NSAttributedString.Key.font: MastodonRegisterViewController.textFieldLabelFont])
|
||||
textField.borderStyle = UITextField.BorderStyle.roundedRect
|
||||
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height))
|
||||
textField.leftView = paddingView
|
||||
|
@ -153,7 +157,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
|||
textField.textColor = Asset.Colors.Label.primary.color
|
||||
textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Email.placeholder,
|
||||
attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color,
|
||||
NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)])
|
||||
NSAttributedString.Key.font: MastodonRegisterViewController.textFieldLabelFont])
|
||||
textField.borderStyle = UITextField.BorderStyle.roundedRect
|
||||
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height))
|
||||
textField.leftView = paddingView
|
||||
|
@ -164,7 +168,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
|||
let emailErrorPromptLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
let color = Asset.Colors.danger.color
|
||||
let font = UIFont.preferredFont(forTextStyle: .caption1)
|
||||
let font = MastodonRegisterViewController.errorPromptLabelFont
|
||||
return label
|
||||
}()
|
||||
|
||||
|
@ -178,7 +182,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
|||
textField.textColor = Asset.Colors.Label.primary.color
|
||||
textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Password.placeholder,
|
||||
attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color,
|
||||
NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)])
|
||||
NSAttributedString.Key.font: MastodonRegisterViewController.textFieldLabelFont])
|
||||
textField.borderStyle = UITextField.BorderStyle.roundedRect
|
||||
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height))
|
||||
textField.leftView = paddingView
|
||||
|
@ -195,7 +199,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
|||
let passwordErrorPromptLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
let color = Asset.Colors.danger.color
|
||||
let font = UIFont.preferredFont(forTextStyle: .caption1)
|
||||
let font = MastodonRegisterViewController.errorPromptLabelFont
|
||||
return label
|
||||
}()
|
||||
|
||||
|
@ -208,7 +212,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
|||
textField.textColor = Asset.Colors.Label.primary.color
|
||||
textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Invite.registrationUserInviteRequest,
|
||||
attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color,
|
||||
NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)])
|
||||
NSAttributedString.Key.font: MastodonRegisterViewController.textFieldLabelFont])
|
||||
textField.borderStyle = UITextField.BorderStyle.roundedRect
|
||||
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height))
|
||||
textField.leftView = paddingView
|
||||
|
@ -219,7 +223,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
|||
let reasonErrorPromptLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
let color = Asset.Colors.danger.color
|
||||
let font = UIFont.preferredFont(forTextStyle: .caption1)
|
||||
let font = MastodonRegisterViewController.errorPromptLabelFont
|
||||
return label
|
||||
}()
|
||||
|
||||
|
|
|
@ -223,7 +223,7 @@ extension MastodonRegisterViewModel {
|
|||
}
|
||||
|
||||
static func attributeStringForPassword(validateState: ValidateState) -> NSAttributedString {
|
||||
let font = UIFont.preferredFont(forTextStyle: .caption1)
|
||||
let font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 13, weight: .regular), maximumPointSize: 18)
|
||||
let attributeString = NSMutableAttributedString()
|
||||
|
||||
let image = MastodonRegisterViewModel.checkmarkImage(font: font)
|
||||
|
@ -236,7 +236,7 @@ extension MastodonRegisterViewModel {
|
|||
}
|
||||
|
||||
static func errorPromptAttributedString(for prompt: String) -> NSAttributedString {
|
||||
let font = UIFont.preferredFont(forTextStyle: .caption1)
|
||||
let font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 13, weight: .regular), maximumPointSize: 18)
|
||||
let attributeString = NSMutableAttributedString()
|
||||
|
||||
let image = MastodonRegisterViewModel.xmarkImage(font: font)
|
||||
|
|
|
@ -25,6 +25,7 @@ final class MastodonServerRulesViewController: UIViewController, NeedsDependency
|
|||
label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: .systemFont(ofSize: 34, weight: .bold))
|
||||
label.textColor = .label
|
||||
label.text = L10n.Scene.ServerRules.title
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
|
@ -54,7 +55,7 @@ final class MastodonServerRulesViewController: UIViewController, NeedsDependency
|
|||
|
||||
private(set) lazy var bottomPromptTextView: UITextView = {
|
||||
let textView = UITextView()
|
||||
textView.font = .preferredFont(forTextStyle: .body)
|
||||
textView.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular), maximumPointSize: 22)
|
||||
textView.textColor = .label
|
||||
textView.isSelectable = true
|
||||
textView.isEditable = false
|
||||
|
@ -181,7 +182,7 @@ extension MastodonServerRulesViewController {
|
|||
let str = NSString(string: L10n.Scene.ServerRules.prompt(viewModel.domain))
|
||||
let termsOfServiceRange = str.range(of: L10n.Scene.ServerRules.termsOfService)
|
||||
let privacyRange = str.range(of: L10n.Scene.ServerRules.privacyPolicy)
|
||||
let attributeString = NSMutableAttributedString(string: L10n.Scene.ServerRules.prompt(viewModel.domain), attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .body), NSAttributedString.Key.foregroundColor: UIColor.label])
|
||||
let attributeString = NSMutableAttributedString(string: L10n.Scene.ServerRules.prompt(viewModel.domain), attributes: [NSAttributedString.Key.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular), maximumPointSize: 22), NSAttributedString.Key.foregroundColor: UIColor.label])
|
||||
attributeString.addAttribute(.link, value: Mastodon.API.serverRulesURL(domain: viewModel.domain), range: termsOfServiceRange)
|
||||
attributeString.addAttribute(.link, value: Mastodon.API.privacyURL(domain: viewModel.domain), range: privacyRange)
|
||||
let linkAttributes = [NSAttributedString.Key.foregroundColor:linkColor]
|
||||
|
|
|
@ -45,9 +45,8 @@ final class ProfileHeaderView: UIView {
|
|||
imageView.backgroundColor = ProfileHeaderView.bannerImageViewPlaceholderColor
|
||||
imageView.layer.masksToBounds = true
|
||||
imageView.isUserInteractionEnabled = true
|
||||
// #if DEBUG
|
||||
// imageView.image = .placeholder(color: .red)
|
||||
// #endif
|
||||
// accessibility
|
||||
imageView.accessibilityIgnoresInvertColors = true
|
||||
return imageView
|
||||
}()
|
||||
let bannerImageViewOverlayView: UIView = {
|
||||
|
@ -101,7 +100,7 @@ final class ProfileHeaderView: UIView {
|
|||
|
||||
let nameTextField: UITextField = {
|
||||
let textField = UITextField()
|
||||
textField.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold))
|
||||
textField.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold), maximumPointSize: 28)
|
||||
textField.textColor = .white
|
||||
textField.text = "Alice"
|
||||
textField.autocorrectionType = .no
|
||||
|
@ -112,7 +111,7 @@ final class ProfileHeaderView: UIView {
|
|||
|
||||
let usernameLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))
|
||||
label.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular), maximumPointSize: 20)
|
||||
label.adjustsFontSizeToFitWidth = true
|
||||
label.minimumScaleFactor = 0.5
|
||||
label.textColor = Asset.Scene.Profile.Banner.usernameGray.color
|
||||
|
|
|
@ -479,6 +479,8 @@ extension ProfileViewController {
|
|||
guard let self = self else { return }
|
||||
let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-"
|
||||
self.profileHeaderViewController.profileHeaderView.statusDashboardView.postDashboardMeterView.numberLabel.text = text
|
||||
self.profileHeaderViewController.profileHeaderView.statusDashboardView.postDashboardMeterView.isAccessibilityElement = true
|
||||
self.profileHeaderViewController.profileHeaderView.statusDashboardView.postDashboardMeterView.accessibilityLabel = L10n.Scene.Profile.Dashboard.Accessibility.countPosts(count ?? 0)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
viewModel.followingCount
|
||||
|
@ -486,6 +488,8 @@ extension ProfileViewController {
|
|||
guard let self = self else { return }
|
||||
let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-"
|
||||
self.profileHeaderViewController.profileHeaderView.statusDashboardView.followingDashboardMeterView.numberLabel.text = text
|
||||
self.profileHeaderViewController.profileHeaderView.statusDashboardView.followingDashboardMeterView.isAccessibilityElement = true
|
||||
self.profileHeaderViewController.profileHeaderView.statusDashboardView.followingDashboardMeterView.accessibilityLabel = L10n.Scene.Profile.Dashboard.Accessibility.countFollowing(count ?? 0)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
viewModel.followersCount
|
||||
|
@ -493,6 +497,8 @@ extension ProfileViewController {
|
|||
guard let self = self else { return }
|
||||
let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-"
|
||||
self.profileHeaderViewController.profileHeaderView.statusDashboardView.followersDashboardMeterView.numberLabel.text = text
|
||||
self.profileHeaderViewController.profileHeaderView.statusDashboardView.followersDashboardMeterView.isAccessibilityElement = true
|
||||
self.profileHeaderViewController.profileHeaderView.statusDashboardView.followersDashboardMeterView.accessibilityLabel = L10n.Scene.Profile.Dashboard.Accessibility.countFollowers(count ?? 0)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ extension UserTimelineViewModel {
|
|||
// set empty section to make update animation top-to-bottom style
|
||||
var snapshot = NSDiffableDataSourceSnapshot<StatusSection, Item>()
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems([.bottomLoader], toSection: .main)
|
||||
diffableDataSource?.apply(snapshot)
|
||||
}
|
||||
|
||||
|
|
|
@ -51,23 +51,27 @@ class SettingsViewController: UIViewController, NeedsDependency {
|
|||
return menu
|
||||
}
|
||||
|
||||
private(set) lazy var notifySectionHeader: UIView = {
|
||||
private let notifySectionHeaderStackView: UIStackView = {
|
||||
let view = UIStackView()
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.isLayoutMarginsRelativeArrangement = true
|
||||
//view.layoutMargins = UIEdgeInsets(top: 15, left: 4, bottom: 5, right: 4)
|
||||
view.axis = .horizontal
|
||||
view.alignment = .fill
|
||||
view.distribution = .equalSpacing
|
||||
view.spacing = 4
|
||||
|
||||
let notifyLabel = UILabel()
|
||||
return view
|
||||
}()
|
||||
|
||||
let notifyLabel = UILabel()
|
||||
private(set) lazy var notifySectionHeader: UIView = {
|
||||
let view = notifySectionHeaderStackView
|
||||
notifyLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
notifyLabel.font = UIFontMetrics(forTextStyle: .title3).scaledFont(for: UIFont.systemFont(ofSize: 20, weight: .semibold))
|
||||
notifyLabel.adjustsFontForContentSizeCategory = true
|
||||
notifyLabel.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: UIFont.systemFont(ofSize: 20, weight: .semibold))
|
||||
notifyLabel.textColor = Asset.Colors.Label.primary.color
|
||||
notifyLabel.text = L10n.Scene.Settings.Section.Notifications.Trigger.title
|
||||
view.addArrangedSubview(notifyLabel)
|
||||
view.addArrangedSubview(whoButton)
|
||||
whoButton.setContentHuggingPriority(.defaultHigh + 1, for: .horizontal)
|
||||
whoButton.setContentHuggingPriority(.defaultHigh + 1, for: .vertical)
|
||||
return view
|
||||
}()
|
||||
|
||||
|
@ -77,6 +81,7 @@ class SettingsViewController: UIViewController, NeedsDependency {
|
|||
whoButton.showsMenuAsPrimaryAction = true
|
||||
whoButton.setBackgroundColor(Asset.Colors.battleshipGrey.color, for: .normal)
|
||||
whoButton.setTitleColor(Asset.Colors.Label.primary.color, for: .normal)
|
||||
whoButton.titleLabel?.adjustsFontForContentSizeCategory = true
|
||||
whoButton.titleLabel?.font = UIFontMetrics(forTextStyle: .title3).scaledFont(for: UIFont.systemFont(ofSize: 20, weight: .semibold))
|
||||
whoButton.contentEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
|
||||
whoButton.layer.cornerRadius = 10
|
||||
|
@ -107,6 +112,7 @@ class SettingsViewController: UIViewController, NeedsDependency {
|
|||
view.alignment = .center
|
||||
|
||||
let label = ActiveLabel(style: .default)
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.textAlignment = .center
|
||||
label.configure(content: "Mastodon is open source software. You can contribute or report issues on GitHub at <a href=\"https://github.com/tootsuite/mastodon\">tootsuite/mastodon</a> (v3.3.0).", emojiDict: [:])
|
||||
label.delegate = self
|
||||
|
@ -138,7 +144,25 @@ class SettingsViewController: UIViewController, NeedsDependency {
|
|||
}
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
updateSectionHeaderStackViewLayout()
|
||||
}
|
||||
|
||||
|
||||
// MAKR: - Private methods
|
||||
private func updateSectionHeaderStackViewLayout() {
|
||||
// accessibility
|
||||
if traitCollection.preferredContentSizeCategory < .accessibilityMedium {
|
||||
notifySectionHeaderStackView.axis = .horizontal
|
||||
notifyLabel.numberOfLines = 1
|
||||
} else {
|
||||
notifySectionHeaderStackView.axis = .vertical
|
||||
notifyLabel.numberOfLines = 0
|
||||
}
|
||||
}
|
||||
|
||||
private func bindViewModel() {
|
||||
self.whoButton.setTitle(viewModel.setting.value.activeSubscription?.policy.title, for: .normal)
|
||||
viewModel.setting
|
||||
|
@ -173,6 +197,8 @@ class SettingsViewController: UIViewController, NeedsDependency {
|
|||
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
setupTableView()
|
||||
|
||||
updateSectionHeaderStackViewLayout()
|
||||
}
|
||||
|
||||
private func setupNavigation() {
|
||||
|
|
|
@ -15,6 +15,8 @@ protocol SettingsAppearanceTableViewCellDelegate: AnyObject {
|
|||
class AppearanceView: UIView {
|
||||
lazy var imageView: UIImageView = {
|
||||
let view = UIImageView()
|
||||
// accessibility
|
||||
view.accessibilityIgnoresInvertColors = true
|
||||
return view
|
||||
}()
|
||||
lazy var titleLabel: UILabel = {
|
||||
|
|
|
@ -31,6 +31,7 @@ final class MosaicImageViewContainer: UIView {
|
|||
let tapGesture = UITapGestureRecognizer.singleTapGestureRecognizer
|
||||
tapGesture.addTarget(self, action: #selector(MosaicImageViewContainer.photoTapGestureRecognizerHandler(_:)))
|
||||
imageView.addGestureRecognizer(tapGesture)
|
||||
imageView.isAccessibilityElement = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -65,6 +66,9 @@ extension MosaicImageViewContainer: ContentWarningOverlayViewDelegate {
|
|||
extension MosaicImageViewContainer {
|
||||
|
||||
private func _init() {
|
||||
// accessibility
|
||||
accessibilityIgnoresInvertColors = true
|
||||
|
||||
container.translatesAutoresizingMaskIntoConstraints = false
|
||||
container.axis = .horizontal
|
||||
container.distribution = .fillEqually
|
||||
|
|
|
@ -46,6 +46,9 @@ final class PlayerContainerView: UIView {
|
|||
|
||||
extension PlayerContainerView {
|
||||
private func _init() {
|
||||
// accessibility
|
||||
accessibilityIgnoresInvertColors = true
|
||||
|
||||
container.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(container)
|
||||
containerHeightLayoutConstraint = container.heightAnchor.constraint(equalToConstant: 162).priority(.required - 1)
|
||||
|
@ -72,16 +75,6 @@ extension PlayerContainerView {
|
|||
mediaTypeIndicotorView.widthAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.width).priority(.required - 1),
|
||||
])
|
||||
|
||||
contentWarningOverlayView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(contentWarningOverlayView)
|
||||
NSLayoutConstraint.activate([
|
||||
contentWarningOverlayView.topAnchor.constraint(equalTo: topAnchor),
|
||||
contentWarningOverlayView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
contentWarningOverlayView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
contentWarningOverlayView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||
])
|
||||
contentWarningOverlayView.delegate = self
|
||||
|
||||
mediaTypeIndicotorViewInContentWarningOverlay.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentWarningOverlayView.addSubview(mediaTypeIndicotorViewInContentWarningOverlay)
|
||||
NSLayoutConstraint.activate([
|
||||
|
@ -90,6 +83,8 @@ extension PlayerContainerView {
|
|||
mediaTypeIndicotorViewInContentWarningOverlay.heightAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.height).priority(.required - 1),
|
||||
mediaTypeIndicotorViewInContentWarningOverlay.widthAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.width).priority(.required - 1),
|
||||
])
|
||||
|
||||
contentWarningOverlayView.delegate = self
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -144,6 +139,16 @@ extension PlayerContainerView {
|
|||
containerHeightLayoutConstraint.constant = floor(rect.height)
|
||||
containerHeightLayoutConstraint.isActive = true
|
||||
|
||||
contentWarningOverlayView.removeFromSuperview()
|
||||
contentWarningOverlayView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(contentWarningOverlayView)
|
||||
NSLayoutConstraint.activate([
|
||||
contentWarningOverlayView.topAnchor.constraint(equalTo: touchBlockingView.topAnchor),
|
||||
contentWarningOverlayView.leadingAnchor.constraint(equalTo: touchBlockingView.leadingAnchor),
|
||||
contentWarningOverlayView.trailingAnchor.constraint(equalTo: touchBlockingView.trailingAnchor),
|
||||
contentWarningOverlayView.bottomAnchor.constraint(equalTo: touchBlockingView.bottomAnchor)
|
||||
])
|
||||
|
||||
bringSubviewToFront(mediaTypeIndicotorView)
|
||||
|
||||
return playerViewController
|
||||
|
|
|
@ -26,6 +26,7 @@ class ContentWarningOverlayView: UIView {
|
|||
label.text = L10n.Common.Controls.Status.mediaContentWarning
|
||||
label.textAlignment = .center
|
||||
label.numberOfLines = 0
|
||||
label.isAccessibilityElement = false
|
||||
return label
|
||||
}()
|
||||
|
||||
|
@ -36,19 +37,21 @@ class ContentWarningOverlayView: UIView {
|
|||
}()
|
||||
let blurContentWarningTitleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17))
|
||||
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17), maximumPointSize: 23)
|
||||
label.text = L10n.Common.Controls.Status.mediaContentWarning
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.textAlignment = .center
|
||||
label.isAccessibilityElement = false
|
||||
return label
|
||||
}()
|
||||
let blurContentWarningLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15))
|
||||
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15), maximumPointSize: 20)
|
||||
label.text = L10n.Common.Controls.Status.mediaContentWarning
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
label.textAlignment = .center
|
||||
label.layer.setupShadow()
|
||||
label.isAccessibilityElement = false
|
||||
return label
|
||||
}()
|
||||
|
||||
|
|
|
@ -75,7 +75,13 @@ final class StatusView: UIView {
|
|||
return label
|
||||
}()
|
||||
|
||||
let avatarView = UIView()
|
||||
let avatarView: UIView = {
|
||||
let view = UIView()
|
||||
view.isAccessibilityElement = true
|
||||
view.accessibilityTraits = .button
|
||||
view.accessibilityLabel = L10n.Common.Controls.Status.showUserProfile
|
||||
return view
|
||||
}()
|
||||
let avatarButton: UIButton = {
|
||||
let button = HighlightDimmableButton(type: .custom)
|
||||
let placeholderImage = UIImage.placeholder(size: avatarImageSize, color: .systemFill)
|
||||
|
@ -96,6 +102,7 @@ final class StatusView: UIView {
|
|||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
label.font = .systemFont(ofSize: 17)
|
||||
label.text = "·"
|
||||
label.isAccessibilityElement = false
|
||||
return label
|
||||
}()
|
||||
|
||||
|
@ -104,6 +111,7 @@ final class StatusView: UIView {
|
|||
label.font = .systemFont(ofSize: 15, weight: .regular)
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
label.text = "@alice"
|
||||
label.isAccessibilityElement = false
|
||||
return label
|
||||
}()
|
||||
|
||||
|
@ -295,7 +303,7 @@ extension StatusView {
|
|||
authorMetaContainerStackView.axis = .vertical
|
||||
authorMetaContainerStackView.spacing = 4
|
||||
|
||||
// title container: [display name | "·" | date]
|
||||
// title container: [display name | "·" | date | padding | visibility]
|
||||
let titleContainerStackView = UIStackView()
|
||||
authorMetaContainerStackView.addArrangedSubview(titleContainerStackView)
|
||||
titleContainerStackView.axis = .horizontal
|
||||
|
@ -308,12 +316,15 @@ extension StatusView {
|
|||
titleContainerStackView.alignment = .firstBaseline
|
||||
titleContainerStackView.addArrangedSubview(nameTrialingDotLabel)
|
||||
titleContainerStackView.addArrangedSubview(dateLabel)
|
||||
titleContainerStackView.addArrangedSubview(UIView()) // padding
|
||||
titleContainerStackView.addArrangedSubview(visibilityImageView)
|
||||
nameLabel.setContentHuggingPriority(.defaultHigh + 1, for: .horizontal)
|
||||
nameTrialingDotLabel.setContentHuggingPriority(.defaultHigh + 2, for: .horizontal)
|
||||
nameTrialingDotLabel.setContentCompressionResistancePriority(.required - 2, for: .horizontal)
|
||||
dateLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||
dateLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
|
||||
|
||||
visibilityImageView.setContentHuggingPriority(.defaultHigh + 3, for: .horizontal)
|
||||
|
||||
// subtitle container: [username]
|
||||
let subtitleContainerStackView = UIStackView()
|
||||
authorMetaContainerStackView.addArrangedSubview(subtitleContainerStackView)
|
||||
|
@ -324,10 +335,6 @@ extension StatusView {
|
|||
authorContainerStackView.addArrangedSubview(revealContentWarningButton)
|
||||
revealContentWarningButton.setContentHuggingPriority(.required - 2, for: .horizontal)
|
||||
|
||||
// visibility ImageView
|
||||
authorContainerStackView.addArrangedSubview(visibilityImageView)
|
||||
visibilityImageView.setContentHuggingPriority(.required - 2, for: .horizontal)
|
||||
|
||||
authorContainerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
authorContainerView.addSubview(authorContainerStackView)
|
||||
NSLayoutConstraint.activate([
|
||||
|
@ -353,7 +360,7 @@ extension StatusView {
|
|||
// only layout to top and left & right then draw image to fit size
|
||||
])
|
||||
// avoid overlay clip author view
|
||||
containerStackView.bringSubviewToFront(authorContainerStackView)
|
||||
containerStackView.bringSubviewToFront(authorContainerView)
|
||||
|
||||
// status
|
||||
statusContainerStackView.addArrangedSubview(activeTextLabel)
|
||||
|
|
|
@ -34,6 +34,14 @@ final class ThreadMetaView: UIView {
|
|||
return button
|
||||
}()
|
||||
|
||||
let containerStackView: UIStackView = {
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .horizontal
|
||||
stackView.spacing = 20
|
||||
return stackView
|
||||
}()
|
||||
let actionButtonStackView = UIStackView()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
|
@ -48,27 +56,53 @@ final class ThreadMetaView: UIView {
|
|||
|
||||
extension ThreadMetaView {
|
||||
private func _init() {
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .horizontal
|
||||
stackView.spacing = 20
|
||||
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(stackView)
|
||||
containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(containerStackView)
|
||||
NSLayoutConstraint.activate([
|
||||
stackView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
|
||||
stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
bottomAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 12),
|
||||
containerStackView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
|
||||
containerStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
containerStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
bottomAnchor.constraint(equalTo: containerStackView.bottomAnchor, constant: 12),
|
||||
])
|
||||
|
||||
stackView.addArrangedSubview(dateLabel)
|
||||
stackView.addArrangedSubview(reblogButton)
|
||||
stackView.addArrangedSubview(favoriteButton)
|
||||
containerStackView.addArrangedSubview(dateLabel)
|
||||
containerStackView.addArrangedSubview(actionButtonStackView)
|
||||
|
||||
actionButtonStackView.axis = .horizontal
|
||||
actionButtonStackView.spacing = 20
|
||||
actionButtonStackView.addArrangedSubview(reblogButton)
|
||||
actionButtonStackView.addArrangedSubview(favoriteButton)
|
||||
|
||||
dateLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
reblogButton.setContentHuggingPriority(.required - 2, for: .horizontal)
|
||||
favoriteButton.setContentHuggingPriority(.required - 1, for: .horizontal)
|
||||
|
||||
updateContainerLayout()
|
||||
|
||||
// TODO:
|
||||
reblogButton.isAccessibilityElement = false
|
||||
favoriteButton.isAccessibilityElement = false
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
updateContainerLayout()
|
||||
}
|
||||
|
||||
private func updateContainerLayout() {
|
||||
if traitCollection.preferredContentSizeCategory < .accessibilityMedium {
|
||||
containerStackView.axis = .horizontal
|
||||
containerStackView.spacing = 20
|
||||
dateLabel.numberOfLines = 1
|
||||
} else {
|
||||
containerStackView.axis = .vertical
|
||||
containerStackView.spacing = 4
|
||||
dateLabel.numberOfLines = 0
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#if canImport(SwiftUI) && DEBUG
|
||||
|
|
|
@ -81,6 +81,7 @@ final class StatusTableViewCell: UITableViewCell, StatusCell {
|
|||
threadMetaView.isHidden = true
|
||||
disposeBag.removeAll()
|
||||
observations.removeAll()
|
||||
isAccessibilityElement = false // reset behavior
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
|
@ -357,3 +358,10 @@ extension StatusTableViewCell: ActionToolbarContainerDelegate {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
extension StatusTableViewCell {
|
||||
override var accessibilityActivationPoint: CGPoint {
|
||||
get { return .zero }
|
||||
set { }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,9 +16,9 @@ class TimelineLoaderTableViewCell: UITableViewCell {
|
|||
static let labelFont = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .medium))
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
var stateBindDispose: AnyCancellable?
|
||||
|
||||
|
||||
let stackView = UIStackView()
|
||||
|
||||
let loadMoreButton: UIButton = {
|
||||
let button = HighlightDimmableButton()
|
||||
button.titleLabel?.font = TimelineLoaderTableViewCell.labelFont
|
||||
|
@ -86,7 +86,6 @@ class TimelineLoaderTableViewCell: UITableViewCell {
|
|||
])
|
||||
|
||||
// use stack view to alignlment content center
|
||||
let stackView = UIStackView()
|
||||
stackView.spacing = 4
|
||||
stackView.axis = .horizontal
|
||||
stackView.alignment = .center
|
||||
|
|
|
@ -105,6 +105,11 @@ extension ActionToolbarContainer {
|
|||
let starImage = UIImage(systemName: "star.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 17, weight: .bold))!.withRenderingMode(.alwaysTemplate)
|
||||
let moreImage = UIImage(systemName: "ellipsis", withConfiguration: UIImage.SymbolConfiguration(pointSize: 17, weight: .bold))!.withRenderingMode(.alwaysTemplate)
|
||||
|
||||
replyButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.reply
|
||||
reblogButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.reblog // needs update to follow state
|
||||
favoriteButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.favorite // needs update to follow state
|
||||
moreButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.menu
|
||||
|
||||
switch style {
|
||||
case .inline:
|
||||
buttons.forEach { button in
|
||||
|
@ -194,6 +199,14 @@ extension ActionToolbarContainer {
|
|||
|
||||
}
|
||||
|
||||
extension ActionToolbarContainer {
|
||||
|
||||
override var accessibilityElements: [Any]? {
|
||||
get { [replyButton, reblogButton, favoriteButton, moreButton] }
|
||||
set { }
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
import SwiftUI
|
||||
|
||||
|
|
|
@ -28,7 +28,8 @@ struct MosaicImageViewModel {
|
|||
let mosaicMeta = MosaicMeta(
|
||||
url: url,
|
||||
size: CGSize(width: width, height: height),
|
||||
blurhash: element.blurhash
|
||||
blurhash: element.blurhash,
|
||||
altText: element.descriptionString
|
||||
)
|
||||
metas.append(mosaicMeta)
|
||||
}
|
||||
|
@ -43,6 +44,7 @@ struct MosaicMeta {
|
|||
let url: URL
|
||||
let size: CGSize
|
||||
let blurhash: String?
|
||||
let altText: String?
|
||||
|
||||
let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.MosaicMeta.working-queue", qos: .userInitiated, attributes: .concurrent)
|
||||
|
||||
|
|
|
@ -73,22 +73,36 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
|||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.isUserInteractionEnabled = false
|
||||
imageView.image = transitionItem.image
|
||||
// accessibility
|
||||
imageView.accessibilityIgnoresInvertColors = true
|
||||
return imageView
|
||||
}()
|
||||
transitionItem.targetFrame = transitionTargetFrame
|
||||
transitionItem.imageView = transitionImageView
|
||||
transitionContext.containerView.addSubview(transitionImageView)
|
||||
|
||||
let animator = MediaHostToMediaPreviewViewControllerAnimatedTransitioning.animator(initialVelocity: .zero)
|
||||
toVC.closeButtonBackground.alpha = 0
|
||||
|
||||
if UIAccessibility.isReduceTransparencyEnabled {
|
||||
toVC.visualEffectView.alpha = 0
|
||||
}
|
||||
|
||||
let animator = MediaHostToMediaPreviewViewControllerAnimatedTransitioning.animator(initialVelocity: .zero)
|
||||
|
||||
animator.addAnimations {
|
||||
transitionImageView.frame = transitionTargetFrame
|
||||
toView.alpha = 1
|
||||
if UIAccessibility.isReduceTransparencyEnabled {
|
||||
toVC.visualEffectView.alpha = 1
|
||||
}
|
||||
}
|
||||
|
||||
animator.addCompletion { position in
|
||||
toVC.pagingViewConttroller.view.alpha = 1
|
||||
transitionImageView.removeFromSuperview()
|
||||
UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseInOut]) {
|
||||
toVC.closeButtonBackground.alpha = 1
|
||||
}
|
||||
transitionContext.completeTransition(position == .end)
|
||||
}
|
||||
|
||||
|
@ -119,9 +133,20 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
|||
transitionContext.completeTransition(false)
|
||||
fatalError()
|
||||
}
|
||||
mediaPreviewImageViewController.view.insertSubview(snapshot, aboveSubview: mediaPreviewImageViewController.previewImageView)
|
||||
|
||||
snapshot.center = transitionContext.containerView.center
|
||||
|
||||
let transitionMaskView = UIView(frame: transitionContext.containerView.bounds)
|
||||
transitionMaskView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
transitionContext.containerView.addSubview(transitionMaskView)
|
||||
|
||||
let maskLayer = CAShapeLayer()
|
||||
maskLayer.frame = transitionMaskView.bounds
|
||||
let maskLayerFromPath = UIBezierPath(rect: maskLayer.bounds).cgPath
|
||||
maskLayer.path = maskLayerFromPath
|
||||
transitionMaskView.layer.mask = maskLayer
|
||||
|
||||
transitionMaskView.addSubview(snapshot)
|
||||
snapshot.center = transitionMaskView.center
|
||||
fromVC.view.bringSubviewToFront(fromVC.closeButtonBackground)
|
||||
|
||||
transitionItem.imageView = imageView
|
||||
transitionItem.snapshotTransitioning = snapshot
|
||||
|
@ -134,6 +159,38 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
|||
let animator = popInteractiveTransitionAnimator
|
||||
|
||||
self.transitionItem.snapshotRaw?.alpha = 0.0
|
||||
|
||||
var needsMaskWithAnimation = true
|
||||
let maskLayerToRect: CGRect? = {
|
||||
guard case .mosaic = transitionItem.source else { return nil }
|
||||
guard let navigationBar = toVC.navigationController?.navigationBar else { return nil }
|
||||
let navigationBarFrameInWindow = toVC.view.convert(navigationBar.frame, to: nil)
|
||||
var rect = transitionMaskView.frame
|
||||
rect.origin.y = navigationBarFrameInWindow.maxY + UIView.separatorLineHeight(of: toVC.view) // extra hairline
|
||||
|
||||
if rect.minY < snapshot.frame.minY {
|
||||
needsMaskWithAnimation = false
|
||||
}
|
||||
|
||||
return rect
|
||||
}()
|
||||
let maskLayerToPath = maskLayerToRect.flatMap { UIBezierPath(rect: $0) }?.cgPath
|
||||
let maskLayerToFinalRect: CGRect? = {
|
||||
guard case .mosaic = transitionItem.source else { return nil }
|
||||
guard let tabBarController = toVC.tabBarController else { return nil }
|
||||
let tabBarFrameInWindow = toVC.view.convert(tabBarController.tabBar.frame, to: nil)
|
||||
var rect = maskLayerToRect ?? transitionMaskView.frame
|
||||
let offset = rect.maxY - tabBarFrameInWindow.minY
|
||||
guard offset > 0 else { return rect }
|
||||
rect.size.height -= offset
|
||||
return rect
|
||||
}()
|
||||
let maskLayerToFinalPath = maskLayerToFinalRect.flatMap { UIBezierPath(rect: $0) }?.cgPath
|
||||
|
||||
if !needsMaskWithAnimation, let maskLayerToPath = maskLayerToPath {
|
||||
maskLayer.path = maskLayerToPath
|
||||
}
|
||||
|
||||
animator.addAnimations {
|
||||
if let targetFrame = targetFrame {
|
||||
self.transitionItem.snapshotTransitioning?.frame = targetFrame
|
||||
|
@ -143,6 +200,12 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
|||
self.transitionItem.sourceImageViewCornerRadius.flatMap { self.transitionItem.snapshotTransitioning?.layer.cornerRadius = $0 }
|
||||
fromVC.closeButtonBackground.alpha = 0
|
||||
fromVC.visualEffectView.effect = nil
|
||||
if let maskLayerToFinalPath = maskLayerToFinalPath {
|
||||
maskLayer.path = maskLayerToFinalPath
|
||||
}
|
||||
if UIAccessibility.isReduceTransparencyEnabled {
|
||||
fromVC.visualEffectView.alpha = 0
|
||||
}
|
||||
}
|
||||
|
||||
animator.addCompletion { position in
|
||||
|
@ -197,9 +260,21 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
|||
transitionContext.completeTransition(false)
|
||||
return
|
||||
}
|
||||
mediaPreviewImageViewController.view.insertSubview(snapshot, aboveSubview: mediaPreviewImageViewController.previewImageView)
|
||||
|
||||
snapshot.center = transitionContext.containerView.center
|
||||
|
||||
let transitionMaskView = UIView(frame: transitionContext.containerView.bounds)
|
||||
transitionMaskView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
transitionContext.containerView.addSubview(transitionMaskView)
|
||||
transitionItem.interactiveTransitionMaskView = transitionMaskView
|
||||
|
||||
let maskLayer = CAShapeLayer()
|
||||
maskLayer.frame = transitionMaskView.bounds
|
||||
maskLayer.path = UIBezierPath(rect: maskLayer.bounds).cgPath
|
||||
transitionMaskView.layer.mask = maskLayer
|
||||
transitionItem.interactiveTransitionMaskLayer = maskLayer
|
||||
|
||||
transitionMaskView.addSubview(snapshot)
|
||||
snapshot.center = transitionMaskView.center
|
||||
fromVC.view.bringSubviewToFront(fromVC.closeButtonBackground)
|
||||
|
||||
transitionItem.imageView = imageView
|
||||
transitionItem.snapshotTransitioning = snapshot
|
||||
|
@ -214,6 +289,10 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
|||
let blurEffect = fromVC.visualEffectView.effect
|
||||
self.transitionItem.snapshotRaw?.alpha = 0.0
|
||||
|
||||
UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseInOut]) {
|
||||
fromVC.closeButtonBackground.alpha = 0
|
||||
}
|
||||
|
||||
animator.addAnimations {
|
||||
switch self.transitionItem.source {
|
||||
case .profileBanner:
|
||||
|
@ -221,9 +300,11 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
|||
default:
|
||||
break
|
||||
}
|
||||
fromVC.closeButtonBackground.alpha = 0
|
||||
fromVC.visualEffectView.effect = nil
|
||||
self.transitionItem.sourceImageViewCornerRadius.flatMap { self.transitionItem.snapshotTransitioning?.layer.cornerRadius = $0 }
|
||||
if UIAccessibility.isReduceTransparencyEnabled {
|
||||
fromVC.visualEffectView.alpha = 0
|
||||
}
|
||||
}
|
||||
|
||||
animator.addCompletion { position in
|
||||
|
@ -237,6 +318,13 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
|||
self.transitionItem.source.updateAppearance(position: position, index: nil)
|
||||
}
|
||||
fromVC.visualEffectView.effect = position == .end ? nil : blurEffect
|
||||
transitionMaskView.removeFromSuperview()
|
||||
UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseInOut]) {
|
||||
fromVC.closeButtonBackground.alpha = position == .end ? 0 : 1
|
||||
}
|
||||
if UIAccessibility.isReduceTransparencyEnabled {
|
||||
fromVC.visualEffectView.alpha = position == .end ? 0 : 1
|
||||
}
|
||||
transitionContext.completeTransition(position == .end)
|
||||
}
|
||||
}
|
||||
|
@ -313,8 +401,51 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
|||
let gestureVelocity = panGestureRecognizer.velocity(in: transitionContext.containerView)
|
||||
let velocity = convert(gestureVelocity, for: transitionItem)
|
||||
let itemAnimator = MediaHostToMediaPreviewViewControllerAnimatedTransitioning.animator(initialVelocity: velocity)
|
||||
|
||||
var maskLayerToFinalPath: CGPath?
|
||||
if toPosition == .end,
|
||||
let transitionMaskView = transitionItem.interactiveTransitionMaskView,
|
||||
let snapshot = transitionItem.snapshotTransitioning {
|
||||
let toVC = transitionItem.previewableViewController
|
||||
|
||||
var needsMaskWithAnimation = true
|
||||
let maskLayerToRect: CGRect? = {
|
||||
guard case .mosaic = transitionItem.source else { return nil }
|
||||
guard let navigationBar = toVC.navigationController?.navigationBar else { return nil }
|
||||
let navigationBarFrameInWindow = toVC.view.convert(navigationBar.frame, to: nil)
|
||||
var rect = transitionMaskView.frame
|
||||
rect.origin.y = navigationBarFrameInWindow.maxY
|
||||
|
||||
if rect.minY < snapshot.frame.minY {
|
||||
needsMaskWithAnimation = false
|
||||
}
|
||||
|
||||
return rect
|
||||
}()
|
||||
let maskLayerToPath = maskLayerToRect.flatMap { UIBezierPath(rect: $0) }?.cgPath
|
||||
|
||||
if let maskLayer = transitionItem.interactiveTransitionMaskLayer, !needsMaskWithAnimation {
|
||||
maskLayer.path = maskLayerToPath
|
||||
}
|
||||
|
||||
let maskLayerToFinalRect: CGRect? = {
|
||||
guard case .mosaic = transitionItem.source else { return nil }
|
||||
guard let tabBarController = toVC.tabBarController else { return nil }
|
||||
let tabBarFrameInWindow = toVC.view.convert(tabBarController.tabBar.frame, to: nil)
|
||||
var rect = maskLayerToRect ?? transitionMaskView.frame
|
||||
let offset = rect.maxY - tabBarFrameInWindow.minY
|
||||
guard offset > 0 else { return rect }
|
||||
rect.size.height -= offset
|
||||
return rect
|
||||
}()
|
||||
maskLayerToFinalPath = maskLayerToFinalRect.flatMap { UIBezierPath(rect: $0) }?.cgPath
|
||||
}
|
||||
|
||||
itemAnimator.addAnimations {
|
||||
if let maskLayer = self.transitionItem.interactiveTransitionMaskLayer,
|
||||
let maskLayerToFinalPath = maskLayerToFinalPath {
|
||||
maskLayer.path = maskLayerToFinalPath
|
||||
}
|
||||
if toPosition == .end {
|
||||
switch self.transitionItem.source {
|
||||
case .profileBanner where toPosition == .end:
|
||||
|
|
|
@ -30,6 +30,8 @@ class MediaPreviewTransitionItem: Identifiable {
|
|||
var snapshotRaw: UIView?
|
||||
var snapshotTransitioning: UIView?
|
||||
var touchOffset: CGVector = CGVector.zero
|
||||
var interactiveTransitionMaskView: UIView?
|
||||
var interactiveTransitionMaskLayer: CAShapeLayer?
|
||||
|
||||
init(id: UUID = UUID(), source: Source, previewableViewController: MediaPreviewableViewController) {
|
||||
self.id = id
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
protocol MediaPreviewableViewController: AnyObject {
|
||||
protocol MediaPreviewableViewController: UIViewController {
|
||||
var mediaPreviewTransitionController: MediaPreviewTransitionController { get }
|
||||
func sourceFrame(transitionItem: MediaPreviewTransitionItem, index: Int) -> CGRect?
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue