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