Merge tag '0.4.0' into develop

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

View File

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

View File

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

View File

@ -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 cant view Artbots 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"
}
}
}
}

View File

@ -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" */ = {

View File

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

View File

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

View File

@ -50,6 +50,42 @@ extension CategoryPickerItem {
}
}
}
var accessibilityDescription: String {
switch self {
case .all:
return L10n.Scene.ServerPicker.Button.Category.allAccessiblityDescription
case .category(let category):
switch category.category {
case .academia:
return L10n.Scene.ServerPicker.Button.Category.academia
case .activism:
return L10n.Scene.ServerPicker.Button.Category.activism
case .food:
return L10n.Scene.ServerPicker.Button.Category.food
case .furry:
return L10n.Scene.ServerPicker.Button.Category.furry
case .games:
return L10n.Scene.ServerPicker.Button.Category.games
case .general:
return L10n.Scene.ServerPicker.Button.Category.general
case .journalism:
return L10n.Scene.ServerPicker.Button.Category.journalism
case .lgbt:
return L10n.Scene.ServerPicker.Button.Category.lgbt
case .regional:
return L10n.Scene.ServerPicker.Button.Category.regional
case .art:
return L10n.Scene.ServerPicker.Button.Category.art
case .music:
return L10n.Scene.ServerPicker.Button.Category.music
case .tech:
return L10n.Scene.ServerPicker.Button.Category.tech
case ._other:
return "" // FIXME:
}
}
}
}
extension CategoryPickerItem: Equatable {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -32,12 +32,14 @@ extension ActiveLabel {
text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
#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
// }
}

View File

@ -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 cant view Artbots 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...

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,303 @@
"Common.Alerts.BlockDomain.BlockEntireDomain" = "Block entire domain";
"Common.Alerts.BlockDomain.Title" = "Are you really, really sure you want to block the entire %@? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.";
"Common.Alerts.Common.PleaseTryAgain" = "Please try again.";
"Common.Alerts.Common.PleaseTryAgainLater" = "Please try again later.";
"Common.Alerts.DeletePost.Delete" = "Delete";
"Common.Alerts.DeletePost.Title" = "Are you sure you want to delete this post?";
"Common.Alerts.DiscardPostContent.Message" = "Confirm discard composed post content.";
"Common.Alerts.DiscardPostContent.Title" = "Discard Publish";
"Common.Alerts.PublishPostFailure.Message" = "Failed to publish the post.
Please check your internet connection.";
"Common.Alerts.PublishPostFailure.Title" = "Publish Failure";
"Common.Alerts.SavePhotoFailure.Message" = "Please enable photo libaray access permission to save photo.";
"Common.Alerts.SavePhotoFailure.Title" = "Save Photo Failure";
"Common.Alerts.ServerError.Title" = "Server Error";
"Common.Alerts.SignOut.Confirm" = "Sign Out";
"Common.Alerts.SignOut.Message" = "Are you sure you want to sign out?";
"Common.Alerts.SignOut.Title" = "Sign out";
"Common.Alerts.SignUpFailure.Title" = "Sign Up Failure";
"Common.Alerts.VoteFailure.PollExpired" = "The poll has expired";
"Common.Alerts.VoteFailure.Title" = "Vote Failure";
"Common.Controls.Actions.Add" = "Add";
"Common.Controls.Actions.Back" = "Back";
"Common.Controls.Actions.BlockDomain" = "Block %@";
"Common.Controls.Actions.Cancel" = "Cancel";
"Common.Controls.Actions.Confirm" = "Confirm";
"Common.Controls.Actions.Continue" = "Continue";
"Common.Controls.Actions.Delete" = "Delete";
"Common.Controls.Actions.Discard" = "Discard";
"Common.Controls.Actions.Done" = "Done";
"Common.Controls.Actions.Edit" = "Edit";
"Common.Controls.Actions.FindPeople" = "Find people to follow";
"Common.Controls.Actions.ManuallySearch" = "Manually search instead";
"Common.Controls.Actions.Ok" = "OK";
"Common.Controls.Actions.OpenInSafari" = "Open in Safari";
"Common.Controls.Actions.Preview" = "Preview";
"Common.Controls.Actions.Remove" = "Remove";
"Common.Controls.Actions.ReportUser" = "Report %@";
"Common.Controls.Actions.Save" = "Save";
"Common.Controls.Actions.SavePhoto" = "Save photo";
"Common.Controls.Actions.SeeMore" = "See More";
"Common.Controls.Actions.Settings" = "Settings";
"Common.Controls.Actions.Share" = "Share";
"Common.Controls.Actions.SharePost" = "Share post";
"Common.Controls.Actions.ShareUser" = "Share %@";
"Common.Controls.Actions.SignIn" = "Sign In";
"Common.Controls.Actions.SignUp" = "Sign Up";
"Common.Controls.Actions.Skip" = "Skip";
"Common.Controls.Actions.TakePhoto" = "Take photo";
"Common.Controls.Actions.TryAgain" = "Try Again";
"Common.Controls.Actions.UnblockDomain" = "Unblock %@";
"Common.Controls.Firendship.Block" = "Block";
"Common.Controls.Firendship.BlockDomain" = "Block %@";
"Common.Controls.Firendship.BlockUser" = "Block %@";
"Common.Controls.Firendship.Blocked" = "Blocked";
"Common.Controls.Firendship.EditInfo" = "Edit info";
"Common.Controls.Firendship.Follow" = "Follow";
"Common.Controls.Firendship.Following" = "Following";
"Common.Controls.Firendship.Mute" = "Mute";
"Common.Controls.Firendship.MuteUser" = "Mute %@";
"Common.Controls.Firendship.Muted" = "Muted";
"Common.Controls.Firendship.Pending" = "Pending";
"Common.Controls.Firendship.Request" = "Request";
"Common.Controls.Firendship.Unblock" = "Unblock";
"Common.Controls.Firendship.UnblockUser" = "Unblock %@";
"Common.Controls.Firendship.Unmute" = "Unmute";
"Common.Controls.Firendship.UnmuteUser" = "Unmute %@";
"Common.Controls.Status.Actions.Favorite" = "Favorite";
"Common.Controls.Status.Actions.Menu" = "Menu";
"Common.Controls.Status.Actions.Reblog" = "Reblog";
"Common.Controls.Status.Actions.Reply" = "Reply";
"Common.Controls.Status.Actions.Unfavorite" = "Unfavorite";
"Common.Controls.Status.Actions.Unreblog" = "Unreblog";
"Common.Controls.Status.ContentWarning" = "content warning";
"Common.Controls.Status.ContentWarningText" = "cw: %@";
"Common.Controls.Status.MediaContentWarning" = "Tap to reveal that may be sensitive";
"Common.Controls.Status.Poll.Closed" = "Closed";
"Common.Controls.Status.Poll.TimeLeft" = "%@ left";
"Common.Controls.Status.Poll.Vote" = "Vote";
"Common.Controls.Status.Poll.VoteCount.Multiple" = "%d votes";
"Common.Controls.Status.Poll.VoteCount.Single" = "%d vote";
"Common.Controls.Status.Poll.VoterCount.Multiple" = "%d voters";
"Common.Controls.Status.Poll.VoterCount.Single" = "%d voter";
"Common.Controls.Status.ShowPost" = "Show Post";
"Common.Controls.Status.ShowUserProfile" = "Show user profile";
"Common.Controls.Status.Tag.Email" = "Email";
"Common.Controls.Status.Tag.Emoji" = "Emoji";
"Common.Controls.Status.Tag.Hashtag" = "Hashtag";
"Common.Controls.Status.Tag.Link" = "Link";
"Common.Controls.Status.Tag.Mention" = "Mention";
"Common.Controls.Status.Tag.Url" = "URL";
"Common.Controls.Status.UserReblogged" = "%@ reblogged";
"Common.Controls.Status.UserRepliedTo" = "Replied to %@";
"Common.Controls.Tabs.Home" = "Home";
"Common.Controls.Tabs.Notification" = "Notification";
"Common.Controls.Tabs.Profile" = "Profile";
"Common.Controls.Tabs.Search" = "Search";
"Common.Controls.Timeline.Accessibility.CountFavorites" = "%@ favorites";
"Common.Controls.Timeline.Accessibility.CountReblogs" = "%@ reblogs";
"Common.Controls.Timeline.Accessibility.CountReplies" = "%@ replies";
"Common.Controls.Timeline.Header.BlockedWarning" = "You cant view Artbots profile
until they unblock you.";
"Common.Controls.Timeline.Header.BlockingWarning" = "You cant view Artbots profile
until you unblock them.
Your account looks like this to them.";
"Common.Controls.Timeline.Header.NoStatusFound" = "No Status Found";
"Common.Controls.Timeline.Header.SuspendedWarning" = "This account has been suspended.";
"Common.Controls.Timeline.Header.UserSuspendedWarning" = "%@'s account has been suspended.";
"Common.Controls.Timeline.Loader.LoadMissingPosts" = "Load missing posts";
"Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Loading missing posts...";
"Common.Controls.Timeline.Loader.ShowMoreReplies" = "Show more replies";
"Common.Countable.Photo.Multiple" = "photos";
"Common.Countable.Photo.Single" = "photo";
"Scene.Compose.Accessibility.AppendAttachment" = "Append attachment";
"Scene.Compose.Accessibility.AppendPoll" = "Append poll";
"Scene.Compose.Accessibility.CustomEmojiPicker" = "Custom emoji picker";
"Scene.Compose.Accessibility.DisableContentWarning" = "Disable content warning";
"Scene.Compose.Accessibility.EnableContentWarning" = "Enable content warning";
"Scene.Compose.Accessibility.InputLimitExceedsCount" = "Input limit exceeds %ld";
"Scene.Compose.Accessibility.InputLimitRemainsCount" = "Input limit remains %ld";
"Scene.Compose.Accessibility.PostVisibilityMenu" = "Post visibility menu";
"Scene.Compose.Accessibility.RemovePoll" = "Remove poll";
"Scene.Compose.Attachment.AttachmentBroken" = "This %@ is broken and can't be
uploaded to Mastodon.";
"Scene.Compose.Attachment.DescriptionPhoto" = "Describe photo for low vision people...";
"Scene.Compose.Attachment.DescriptionVideo" = "Describe whats happening for low vision people...";
"Scene.Compose.Attachment.Photo" = "photo";
"Scene.Compose.Attachment.Video" = "video";
"Scene.Compose.ComposeAction" = "Publish";
"Scene.Compose.ContentInputPlaceholder" = "Type or paste what's on your mind";
"Scene.Compose.ContentWarning.Placeholder" = "Write an accurate warning here...";
"Scene.Compose.MediaSelection.Browse" = "Browse";
"Scene.Compose.MediaSelection.Camera" = "Take Photo";
"Scene.Compose.MediaSelection.PhotoLibrary" = "Photo Library";
"Scene.Compose.Poll.DurationTime" = "Duration: %@";
"Scene.Compose.Poll.OneDay" = "1 Day";
"Scene.Compose.Poll.OneHour" = "1 Hour";
"Scene.Compose.Poll.OptionNumber" = "Option %ld";
"Scene.Compose.Poll.SevenDays" = "7 Days";
"Scene.Compose.Poll.SixHours" = "6 Hours";
"Scene.Compose.Poll.ThirtyMinutes" = "30 minutes";
"Scene.Compose.Poll.ThreeDays" = "3 Days";
"Scene.Compose.ReplyingToUser" = "replying to %@";
"Scene.Compose.Title.NewPost" = "New Post";
"Scene.Compose.Title.NewReply" = "New Reply";
"Scene.Compose.Visibility.Direct" = "Only people I mention";
"Scene.Compose.Visibility.Private" = "Followers only";
"Scene.Compose.Visibility.Public" = "Public";
"Scene.Compose.Visibility.Unlisted" = "Unlisted";
"Scene.ConfirmEmail.Button.DontReceiveEmail" = "I never got an email";
"Scene.ConfirmEmail.Button.OpenEmailApp" = "Open Email App";
"Scene.ConfirmEmail.DontReceiveEmail.Description" = "Check if your email address is correct as well as your junk folder if you havent.";
"Scene.ConfirmEmail.DontReceiveEmail.ResendEmail" = "Resend Email";
"Scene.ConfirmEmail.DontReceiveEmail.Title" = "Check your email";
"Scene.ConfirmEmail.OpenEmailApp.Description" = "We just sent you an email. Check your junk folder if you havent.";
"Scene.ConfirmEmail.OpenEmailApp.Mail" = "Mail";
"Scene.ConfirmEmail.OpenEmailApp.OpenEmailClient" = "Open Email Client";
"Scene.ConfirmEmail.OpenEmailApp.Title" = "Check your inbox.";
"Scene.ConfirmEmail.Subtitle" = "We just sent an email to %@,
tap the link to confirm your account.";
"Scene.ConfirmEmail.Title" = "One last thing.";
"Scene.Favorite.Title" = "Your Favorites";
"Scene.Hashtag.Prompt" = "%@ people talking";
"Scene.HomeTimeline.NavigationBarState.NewPosts" = "See new posts";
"Scene.HomeTimeline.NavigationBarState.Offline" = "Offline";
"Scene.HomeTimeline.NavigationBarState.Published" = "Published!";
"Scene.HomeTimeline.NavigationBarState.Publishing" = "Publishing post...";
"Scene.HomeTimeline.Title" = "Home";
"Scene.Notification.Action.Favourite" = "favorited your post";
"Scene.Notification.Action.Follow" = "followed you";
"Scene.Notification.Action.FollowRequest" = "request to follow you";
"Scene.Notification.Action.Mention" = "mentioned you";
"Scene.Notification.Action.Poll" = "Your poll has ended";
"Scene.Notification.Action.Reblog" = "rebloged your post";
"Scene.Notification.Title.Everything" = "Everything";
"Scene.Notification.Title.Mentions" = "Mentions";
"Scene.Profile.Dashboard.Accessibility.CountFollowers" = "%ld followers";
"Scene.Profile.Dashboard.Accessibility.CountFollowing" = "%ld following";
"Scene.Profile.Dashboard.Accessibility.CountPosts" = "%ld posts";
"Scene.Profile.Dashboard.Followers" = "followers";
"Scene.Profile.Dashboard.Following" = "following";
"Scene.Profile.Dashboard.Posts" = "posts";
"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.Message" = "Confirm unblock %@";
"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.Title" = "Unblock Account";
"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Confirm unmute %@";
"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Title" = "Unmute Account";
"Scene.Profile.SegmentedControl.Media" = "Media";
"Scene.Profile.SegmentedControl.Posts" = "Posts";
"Scene.Profile.SegmentedControl.Replies" = "Replies";
"Scene.Profile.Subtitle" = "%@ posts";
"Scene.PublicTimeline.Title" = "Public";
"Scene.Register.Error.Item.Agreement" = "Agreement";
"Scene.Register.Error.Item.Email" = "Email";
"Scene.Register.Error.Item.Locale" = "Locale";
"Scene.Register.Error.Item.Password" = "Password";
"Scene.Register.Error.Item.Reason" = "Reason";
"Scene.Register.Error.Item.Username" = "Username";
"Scene.Register.Error.Reason.Accepted" = "%@ must be accepted";
"Scene.Register.Error.Reason.Blank" = "%@ is required";
"Scene.Register.Error.Reason.Blocked" = "%@ contains a disallowed e-mail provider";
"Scene.Register.Error.Reason.Inclusion" = "%@ is not a supported value";
"Scene.Register.Error.Reason.Invalid" = "%@ is invalid";
"Scene.Register.Error.Reason.Reserved" = "%@ is a reserved keyword";
"Scene.Register.Error.Reason.Taken" = "%@ is already in use";
"Scene.Register.Error.Reason.TooLong" = "%@ is too long";
"Scene.Register.Error.Reason.TooShort" = "%@ is too short";
"Scene.Register.Error.Reason.Unreachable" = "%@ does not seem to exist";
"Scene.Register.Error.Special.EmailInvalid" = "This is not a valid e-mail address";
"Scene.Register.Error.Special.PasswordTooShort" = "Password is too short (must be at least 8 characters)";
"Scene.Register.Error.Special.UsernameInvalid" = "Username must only contain alphanumeric characters and underscores";
"Scene.Register.Error.Special.UsernameTooLong" = "Username is too long (can't be longer than 30 characters)";
"Scene.Register.Input.Avatar.Delete" = "Delete";
"Scene.Register.Input.DisplayName.Placeholder" = "display name";
"Scene.Register.Input.Email.Placeholder" = "email";
"Scene.Register.Input.Invite.RegistrationUserInviteRequest" = "Why do you want to join?";
"Scene.Register.Input.Password.Hint" = "Your password needs at least eight characters";
"Scene.Register.Input.Password.Placeholder" = "password";
"Scene.Register.Input.Username.DuplicatePrompt" = "This username is taken.";
"Scene.Register.Input.Username.Placeholder" = "username";
"Scene.Register.Title" = "Tell us about you.";
"Scene.Report.Content1" = "Are there any other posts youd like to add to the report?";
"Scene.Report.Content2" = "Is there anything the moderators should know about this report?";
"Scene.Report.Send" = "Send Report";
"Scene.Report.SkipToSend" = "Send without comment";
"Scene.Report.Step1" = "Step 1 of 2";
"Scene.Report.Step2" = "Step 2 of 2";
"Scene.Report.TextPlaceholder" = "Type or paste additional comments";
"Scene.Report.Title" = "Report %@";
"Scene.Search.Recommend.Accounts.Description" = "You may like to follow these accounts";
"Scene.Search.Recommend.Accounts.Follow" = "Follow";
"Scene.Search.Recommend.Accounts.Title" = "Accounts you might like";
"Scene.Search.Recommend.ButtonText" = "See All";
"Scene.Search.Recommend.HashTag.Description" = "Hashtags that are getting quite a bit of attention among people you follow";
"Scene.Search.Recommend.HashTag.PeopleTalking" = "%@ people are talking";
"Scene.Search.Recommend.HashTag.Title" = "Trending in your timeline";
"Scene.Search.Searchbar.Cancel" = "Cancel";
"Scene.Search.Searchbar.Placeholder" = "Search hashtags and users";
"Scene.Search.Searching.Clear" = "clear";
"Scene.Search.Searching.RecentSearch" = "Recent searches";
"Scene.Search.Searching.Segment.All" = "All";
"Scene.Search.Searching.Segment.Hashtags" = "Hashtags";
"Scene.Search.Searching.Segment.People" = "People";
"Scene.ServerPicker.Button.Category.Academia" = "academia";
"Scene.ServerPicker.Button.Category.Activism" = "activism";
"Scene.ServerPicker.Button.Category.All" = "All";
"Scene.ServerPicker.Button.Category.AllAccessiblityDescription" = "Category: All";
"Scene.ServerPicker.Button.Category.Art" = "art";
"Scene.ServerPicker.Button.Category.Food" = "food";
"Scene.ServerPicker.Button.Category.Furry" = "furry";
"Scene.ServerPicker.Button.Category.Games" = "games";
"Scene.ServerPicker.Button.Category.General" = "general";
"Scene.ServerPicker.Button.Category.Journalism" = "journalism";
"Scene.ServerPicker.Button.Category.Lgbt" = "lgbt";
"Scene.ServerPicker.Button.Category.Music" = "music";
"Scene.ServerPicker.Button.Category.Regional" = "regional";
"Scene.ServerPicker.Button.Category.Tech" = "tech";
"Scene.ServerPicker.Button.SeeLess" = "See Less";
"Scene.ServerPicker.Button.SeeMore" = "See More";
"Scene.ServerPicker.EmptyState.BadNetwork" = "Something went wrong while loading data. Check your internet connection.";
"Scene.ServerPicker.EmptyState.FindingServers" = "Finding available servers...";
"Scene.ServerPicker.EmptyState.NoResults" = "No results";
"Scene.ServerPicker.Input.Placeholder" = "Find a server or join your own...";
"Scene.ServerPicker.Label.Category" = "CATEGORY";
"Scene.ServerPicker.Label.Language" = "LANGUAGE";
"Scene.ServerPicker.Label.Users" = "USERS";
"Scene.ServerPicker.Title" = "Pick a Server,
any server.";
"Scene.ServerRules.Button.Confirm" = "I Agree";
"Scene.ServerRules.PrivacyPolicy" = "privacy policy";
"Scene.ServerRules.Prompt" = "By continuing, you're subject to the terms of service and privacy policy for %@.";
"Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@.";
"Scene.ServerRules.TermsOfService" = "terms of service";
"Scene.ServerRules.Title" = "Some ground rules.";
"Scene.Settings.Section.Appearance.Automatic" = "Automatic";
"Scene.Settings.Section.Appearance.Dark" = "Always Dark";
"Scene.Settings.Section.Appearance.Light" = "Always Light";
"Scene.Settings.Section.Appearance.Title" = "Appearance";
"Scene.Settings.Section.Boringzone.Privacy" = "Privacy Policy";
"Scene.Settings.Section.Boringzone.Terms" = "Terms of Service";
"Scene.Settings.Section.Boringzone.Title" = "The Boring zone";
"Scene.Settings.Section.Notifications.Boosts" = "Reblogs my post";
"Scene.Settings.Section.Notifications.Favorites" = "Favorites my post";
"Scene.Settings.Section.Notifications.Follows" = "Follows me";
"Scene.Settings.Section.Notifications.Mentions" = "Mentions me";
"Scene.Settings.Section.Notifications.Title" = "Notifications";
"Scene.Settings.Section.Notifications.Trigger.Anyone" = "anyone";
"Scene.Settings.Section.Notifications.Trigger.Follow" = "anyone I follow";
"Scene.Settings.Section.Notifications.Trigger.Follower" = "a follower";
"Scene.Settings.Section.Notifications.Trigger.Noone" = "no one";
"Scene.Settings.Section.Notifications.Trigger.Title" = "Notify me when";
"Scene.Settings.Section.Spicyzone.Clear" = "Clear Media Cache";
"Scene.Settings.Section.Spicyzone.Signout" = "Sign Out";
"Scene.Settings.Section.Spicyzone.Title" = "The spicy zone";
"Scene.Settings.Title" = "Settings";
"Scene.SuggestionAccount.FollowExplain" = "When you follow someone, youll see their posts in your home feed.";
"Scene.SuggestionAccount.Title" = "Find People to Follow";
"Scene.Thread.BackTitle" = "Post";
"Scene.Thread.Favorite.Multiple" = "%@ favorites";
"Scene.Thread.Favorite.Single" = "%@ favorite";
"Scene.Thread.Reblog.Multiple" = "%@ reblogs";
"Scene.Thread.Reblog.Single" = "%@ reblog";
"Scene.Thread.Title" = "Post from %@";
"Scene.Welcome.Slogan" = "Social networking
back in your hands.";

View File

@ -64,6 +64,12 @@ Please check your internet connection.";
"Common.Controls.Firendship.UnblockUser" = "Unblock %@";
"Common.Controls.Firendship.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 cant view Artbots profile
until they unblock you.";
"Common.Controls.Timeline.Header.BlockingWarning" = "You cant view Artbots profile
@ -90,6 +110,15 @@ Your account looks like this to them.";
"Common.Controls.Timeline.Loader.ShowMoreReplies" = "Show more replies";
"Common.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";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
])
}
}
}
}

View File

@ -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,
])
}
}
}
}

View File

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

View File

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

View File

@ -110,3 +110,17 @@ extension PickServerCategoriesCell: UICollectionViewDelegateFlowLayout {
}
}
extension PickServerCategoriesCell {
override func accessibilityElementCount() -> Int {
guard let diffableDataSource = diffableDataSource else { return 0 }
return diffableDataSource.snapshot().itemIdentifiers.count
}
override func accessibilityElement(at index: Int) -> Any? {
guard let item = collectionView.cellForItem(at: IndexPath(item: index, section: 0)) else { return nil }
return item
}
}

View File

@ -34,7 +34,7 @@ class PickServerCell: UITableViewCell {
let domainLabel: UILabel = {
let 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)

View File

@ -0,0 +1,86 @@
//
// PickServerLoaderTableViewCell.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-5-13.
//
import UIKit
import Combine
final class PickServerLoaderTableViewCell: TimelineLoaderTableViewCell {
let containerView: UIView = {
let view = UIView()
view.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 10, right: 16)
view.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
let seperator: UIView = {
let view = UIView()
view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
let emptyStatusLabel: UILabel = {
let label = UILabel()
label.text = L10n.Scene.ServerPicker.EmptyState.noResults
label.textColor = Asset.Colors.Label.secondary.color
label.textAlignment = .center
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 14, weight: .semibold), maximumPointSize: 19)
return label
}()
override func _init() {
super._init()
contentView.addSubview(containerView)
contentView.addSubview(seperator)
NSLayoutConstraint.activate([
// Set background view
containerView.topAnchor.constraint(equalTo: contentView.topAnchor),
containerView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
contentView.readableContentGuide.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 1),
// Set bottom separator
seperator.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
containerView.trailingAnchor.constraint(equalTo: seperator.trailingAnchor),
containerView.topAnchor.constraint(equalTo: seperator.topAnchor),
seperator.heightAnchor.constraint(equalToConstant: 1).priority(.defaultHigh),
])
emptyStatusLabel.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(emptyStatusLabel)
NSLayoutConstraint.activate([
emptyStatusLabel.leadingAnchor.constraint(equalTo: containerView.readableContentGuide.leadingAnchor),
containerView.readableContentGuide.trailingAnchor.constraint(equalTo: emptyStatusLabel.trailingAnchor),
emptyStatusLabel.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
])
emptyStatusLabel.isHidden = true
contentView.bringSubviewToFront(stackView)
activityIndicatorView.isHidden = false
startAnimating()
}
}
#if canImport(SwiftUI) && DEBUG
import SwiftUI
struct PickServerLoaderTableViewCell_Previews: PreviewProvider {
static var previews: some View {
UIViewPreview(width: 375) {
PickServerLoaderTableViewCell()
}
.previewLayout(.fixed(width: 375, height: 100))
}
}
#endif

View File

@ -16,6 +16,9 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
static let avatarImageMaxSizeInPixel = CGSize(width: 400, height: 400)
static let 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
}()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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