feat: add violates server rules report path

This commit is contained in:
CMK 2022-05-10 18:34:39 +08:00
parent e0f6940e28
commit 2ef6345d83
24 changed files with 1129 additions and 169 deletions

View File

@ -87,7 +87,7 @@
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF123A625C3B0210020F248 /* ActiveLabel.swift */; };
2DF75BA725D10E1000694EC8 /* APIService+Favorite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BA625D10E1000694EC8 /* APIService+Favorite.swift */; };
5B24BBDA262DB14800A9381B /* ReportViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B24BBD7262DB14800A9381B /* ReportViewModel.swift */; };
5B24BBDB262DB14800A9381B /* ReportViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B24BBD8262DB14800A9381B /* ReportViewModel+Diffable.swift */; };
5B24BBDB262DB14800A9381B /* ReportStatusViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B24BBD8262DB14800A9381B /* ReportStatusViewModel+Diffable.swift */; };
5B24BBE2262DB19100A9381B /* APIService+Report.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B24BBE1262DB19100A9381B /* APIService+Report.swift */; };
5B90C45E262599800002E742 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C456262599800002E742 /* SettingsViewModel.swift */; };
5B90C45F262599800002E742 /* SettingsToggleTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C459262599800002E742 /* SettingsToggleTableViewCell.swift */; };
@ -440,7 +440,7 @@
DB98337125C9443200AD9700 /* APIService+Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337025C9443200AD9700 /* APIService+Authentication.swift */; };
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337E25C9452D00AD9700 /* APIService+APIError.swift */; };
DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98339B25C96DE600AD9700 /* APIService+Account.swift */; };
DB98EB4727B0DFAA0082E365 /* ReportViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB4627B0DFAA0082E365 /* ReportViewModel+State.swift */; };
DB98EB4727B0DFAA0082E365 /* ReportStatusViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB4627B0DFAA0082E365 /* ReportStatusViewModel+State.swift */; };
DB98EB4927B0F0CD0082E365 /* ReportStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB4827B0F0CD0082E365 /* ReportStatusTableViewCell.swift */; };
DB98EB4C27B0F2BC0082E365 /* ReportStatusTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB4B27B0F2BC0082E365 /* ReportStatusTableViewCell+ViewModel.swift */; };
DB98EB5327B0F9890082E365 /* ReportHeadlineTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB5227B0F9890082E365 /* ReportHeadlineTableViewCell.swift */; };
@ -566,6 +566,14 @@
DBE3CE07261D6A0E00430CC6 /* FavoriteViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE3CE06261D6A0E00430CC6 /* FavoriteViewModel+Diffable.swift */; };
DBE54AC62636C89F004E7C0B /* NotificationPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE54AC52636C89F004E7C0B /* NotificationPreference.swift */; };
DBE54ACC2636C8FD004E7C0B /* NotificationPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE54AC52636C89F004E7C0B /* NotificationPreference.swift */; };
DBEFCD71282A12B200C0ABEA /* ReportReasonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBEFCD70282A12B200C0ABEA /* ReportReasonViewController.swift */; };
DBEFCD74282A130400C0ABEA /* ReportReasonViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBEFCD73282A130400C0ABEA /* ReportReasonViewModel.swift */; };
DBEFCD76282A143F00C0ABEA /* ReportStatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBEFCD75282A143F00C0ABEA /* ReportStatusViewController.swift */; };
DBEFCD79282A147000C0ABEA /* ReportStatusViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBEFCD78282A147000C0ABEA /* ReportStatusViewModel.swift */; };
DBEFCD7B282A162400C0ABEA /* ReportReasonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBEFCD7A282A162400C0ABEA /* ReportReasonView.swift */; };
DBEFCD7D282A2A3B00C0ABEA /* ReportServerRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBEFCD7C282A2A3B00C0ABEA /* ReportServerRulesViewController.swift */; };
DBEFCD80282A2AA900C0ABEA /* ReportServerRulesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBEFCD7F282A2AA900C0ABEA /* ReportServerRulesViewModel.swift */; };
DBEFCD82282A2AB100C0ABEA /* ReportServerRulesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBEFCD81282A2AB100C0ABEA /* ReportServerRulesView.swift */; };
DBF156DF2701B17600EC00B7 /* SidebarAddAccountCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF156DE2701B17600EC00B7 /* SidebarAddAccountCollectionViewCell.swift */; };
DBF156E22702DA6900EC00B7 /* UIStatusBarManager+HandleTapAction.m in Sources */ = {isa = PBXBuildFile; fileRef = DBF156E12702DA6900EC00B7 /* UIStatusBarManager+HandleTapAction.m */; };
DBF156E42702DB3F00EC00B7 /* HandleTapAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF156E32702DB3F00EC00B7 /* HandleTapAction.swift */; };
@ -789,7 +797,7 @@
459EA4F43058CAB47719E963 /* Pods-Mastodon-MastodonUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.debug.xcconfig"; sourceTree = "<group>"; };
46DAB0EBDDFB678347CD96FF /* Pods-MastodonTests.asdk - release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.asdk - release.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.asdk - release.xcconfig"; sourceTree = "<group>"; };
5B24BBD7262DB14800A9381B /* ReportViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReportViewModel.swift; sourceTree = "<group>"; };
5B24BBD8262DB14800A9381B /* ReportViewModel+Diffable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ReportViewModel+Diffable.swift"; sourceTree = "<group>"; };
5B24BBD8262DB14800A9381B /* ReportStatusViewModel+Diffable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ReportStatusViewModel+Diffable.swift"; sourceTree = "<group>"; };
5B24BBE1262DB19100A9381B /* APIService+Report.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+Report.swift"; sourceTree = "<group>"; };
5B90C456262599800002E742 /* SettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = "<group>"; };
5B90C459262599800002E742 /* SettingsToggleTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsToggleTableViewCell.swift; sourceTree = "<group>"; };
@ -1199,7 +1207,7 @@
DB98337025C9443200AD9700 /* APIService+Authentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Authentication.swift"; sourceTree = "<group>"; };
DB98337E25C9452D00AD9700 /* APIService+APIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+APIError.swift"; sourceTree = "<group>"; };
DB98339B25C96DE600AD9700 /* APIService+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Account.swift"; sourceTree = "<group>"; };
DB98EB4627B0DFAA0082E365 /* ReportViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReportViewModel+State.swift"; sourceTree = "<group>"; };
DB98EB4627B0DFAA0082E365 /* ReportStatusViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReportStatusViewModel+State.swift"; sourceTree = "<group>"; };
DB98EB4827B0F0CD0082E365 /* ReportStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportStatusTableViewCell.swift; sourceTree = "<group>"; };
DB98EB4B27B0F2BC0082E365 /* ReportStatusTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReportStatusTableViewCell+ViewModel.swift"; sourceTree = "<group>"; };
DB98EB5227B0F9890082E365 /* ReportHeadlineTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportHeadlineTableViewCell.swift; sourceTree = "<group>"; };
@ -1329,6 +1337,14 @@
DBEB19E927E4F37B00B0E80E /* ku */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ku; path = ku.lproj/Intents.strings; sourceTree = "<group>"; };
DBEB19EA27E4F37B00B0E80E /* ku */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ku; path = ku.lproj/InfoPlist.strings; sourceTree = "<group>"; };
DBEB19EB27E4F37B00B0E80E /* ku */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ku; path = ku.lproj/Intents.stringsdict; sourceTree = "<group>"; };
DBEFCD70282A12B200C0ABEA /* ReportReasonViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportReasonViewController.swift; sourceTree = "<group>"; };
DBEFCD73282A130400C0ABEA /* ReportReasonViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportReasonViewModel.swift; sourceTree = "<group>"; };
DBEFCD75282A143F00C0ABEA /* ReportStatusViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportStatusViewController.swift; sourceTree = "<group>"; };
DBEFCD78282A147000C0ABEA /* ReportStatusViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportStatusViewModel.swift; sourceTree = "<group>"; };
DBEFCD7A282A162400C0ABEA /* ReportReasonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportReasonView.swift; sourceTree = "<group>"; };
DBEFCD7C282A2A3B00C0ABEA /* ReportServerRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportServerRulesViewController.swift; sourceTree = "<group>"; };
DBEFCD7F282A2AA900C0ABEA /* ReportServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportServerRulesViewModel.swift; sourceTree = "<group>"; };
DBEFCD81282A2AB100C0ABEA /* ReportServerRulesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportServerRulesView.swift; sourceTree = "<group>"; };
DBF156DE2701B17600EC00B7 /* SidebarAddAccountCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarAddAccountCollectionViewCell.swift; sourceTree = "<group>"; };
DBF156E02702DA6800EC00B7 /* Mastodon-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Mastodon-Bridging-Header.h"; sourceTree = "<group>"; };
DBF156E12702DA6900EC00B7 /* UIStatusBarManager+HandleTapAction.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIStatusBarManager+HandleTapAction.m"; sourceTree = "<group>"; };
@ -1842,6 +1858,9 @@
isa = PBXGroup;
children = (
DB98EB5727B0FF1F0082E365 /* Share */,
DBEFCD77282A144D00C0ABEA /* Report */,
DBEFCD72282A12B900C0ABEA /* ReportReason */,
DBEFCD7E282A2A3D00C0ABEA /* ReportServerRules */,
DB98EB4F27B0F9300082E365 /* ReportStatus */,
DB98EB5A27B109900082E365 /* ReportSupplementary */,
DB98EB6327B216490082E365 /* ReportResult */,
@ -2208,8 +2227,8 @@
DB427DD425BAA00100D1B89D /* Mastodon */ = {
isa = PBXGroup;
children = (
DB427DE325BAA00100D1B89D /* Info.plist */,
DB89BA1025C10FF5008580ED /* Mastodon.entitlements */,
DB427DE325BAA00100D1B89D /* Info.plist */,
2D76319C25C151DE00929FB9 /* Diffiable */,
DB8AF52A25C13561002E6C99 /* State */,
2D61335525C1886800CAE157 /* Service */,
@ -2850,10 +2869,10 @@
DB98EB4F27B0F9300082E365 /* ReportStatus */ = {
isa = PBXGroup;
children = (
5BB04FD4262E7AFF0043BFF6 /* ReportViewController.swift */,
5B24BBD7262DB14800A9381B /* ReportViewModel.swift */,
5B24BBD8262DB14800A9381B /* ReportViewModel+Diffable.swift */,
DB98EB4627B0DFAA0082E365 /* ReportViewModel+State.swift */,
DBEFCD75282A143F00C0ABEA /* ReportStatusViewController.swift */,
DBEFCD78282A147000C0ABEA /* ReportStatusViewModel.swift */,
5B24BBD8262DB14800A9381B /* ReportStatusViewModel+Diffable.swift */,
DB98EB4627B0DFAA0082E365 /* ReportStatusViewModel+State.swift */,
);
path = ReportStatus;
sourceTree = "<group>";
@ -3177,6 +3196,35 @@
path = Favorite;
sourceTree = "<group>";
};
DBEFCD72282A12B900C0ABEA /* ReportReason */ = {
isa = PBXGroup;
children = (
DBEFCD70282A12B200C0ABEA /* ReportReasonViewController.swift */,
DBEFCD73282A130400C0ABEA /* ReportReasonViewModel.swift */,
DBEFCD7A282A162400C0ABEA /* ReportReasonView.swift */,
);
path = ReportReason;
sourceTree = "<group>";
};
DBEFCD77282A144D00C0ABEA /* Report */ = {
isa = PBXGroup;
children = (
5BB04FD4262E7AFF0043BFF6 /* ReportViewController.swift */,
5B24BBD7262DB14800A9381B /* ReportViewModel.swift */,
);
path = Report;
sourceTree = "<group>";
};
DBEFCD7E282A2A3D00C0ABEA /* ReportServerRules */ = {
isa = PBXGroup;
children = (
DBEFCD7C282A2A3B00C0ABEA /* ReportServerRulesViewController.swift */,
DBEFCD7F282A2AA900C0ABEA /* ReportServerRulesViewModel.swift */,
DBEFCD81282A2AB100C0ABEA /* ReportServerRulesView.swift */,
);
path = ReportServerRules;
sourceTree = "<group>";
};
DBF1D24F269DAF6100C1C08A /* SearchDetail */ = {
isa = PBXGroup;
children = (
@ -3873,6 +3921,7 @@
2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */,
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */,
DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */,
DBEFCD80282A2AA900C0ABEA /* ReportServerRulesViewModel.swift in Sources */,
DB0617FF27855D6C0030EE79 /* MastodonServerRulesViewModel+Diffable.swift in Sources */,
DBB5255E2611F07A002F1F29 /* ProfileViewModel.swift in Sources */,
DB0FCB982797F6BF006C02E2 /* UserTableViewCell+ViewModel.swift in Sources */,
@ -3900,6 +3949,7 @@
DB63F76F279A7D1100455B82 /* NotificationTableViewCell.swift in Sources */,
DB297B1B2679FAE200704C90 /* PlaceholderImageCacheService.swift in Sources */,
DB0FCB8C2796BF8D006C02E2 /* SearchViewModel+Diffable.swift in Sources */,
DBEFCD76282A143F00C0ABEA /* ReportStatusViewController.swift in Sources */,
2D8FCA082637EABB00137F46 /* APIService+FollowRequest.swift in Sources */,
DBDFF1952805561700557A48 /* DiscoveryPostsViewModel+Diffable.swift in Sources */,
DB03A795272A981400EE37C5 /* ContentSplitViewController.swift in Sources */,
@ -4030,7 +4080,7 @@
DB63F75C279956D000455B82 /* Persistence+Tag.swift in Sources */,
2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */,
DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */,
5B24BBDB262DB14800A9381B /* ReportViewModel+Diffable.swift in Sources */,
5B24BBDB262DB14800A9381B /* ReportStatusViewModel+Diffable.swift in Sources */,
DB4F0968269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift in Sources */,
0FB3D2FE25E4CB6400AAD544 /* OnboardingHeadlineTableViewCell.swift in Sources */,
5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */,
@ -4042,6 +4092,7 @@
DBBF1DC92652538500E5B703 /* AutoCompleteSection.swift in Sources */,
DB3E6FE72806A7A200B035AE /* DiscoveryItem.swift in Sources */,
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */,
DBEFCD79282A147000C0ABEA /* ReportStatusViewModel.swift in Sources */,
DB7F48452620241000796008 /* ProfileHeaderViewModel.swift in Sources */,
DB647C5926F1EA2700F7F82C /* WizardPreference.swift in Sources */,
DB0A322E280EE9FD001729D2 /* DiscoveryIntroBannerView.swift in Sources */,
@ -4092,7 +4143,7 @@
DBA94434265CBB5300C537E1 /* ProfileFieldSection.swift in Sources */,
DB336F28278D6EC70031E64B /* MastodonFieldContainer.swift in Sources */,
DBF156E42702DB3F00EC00B7 /* HandleTapAction.swift in Sources */,
DB98EB4727B0DFAA0082E365 /* ReportViewModel+State.swift in Sources */,
DB98EB4727B0DFAA0082E365 /* ReportStatusViewModel+State.swift in Sources */,
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */,
DB6B74F6272FBCDB00C70B6E /* FollowerListViewModel+State.swift in Sources */,
DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */,
@ -4150,6 +4201,7 @@
DB6D9F8426358EEC008423CD /* SettingsItem.swift in Sources */,
2D364F7825E66D8300204FDC /* MastodonResendEmailViewModel.swift in Sources */,
DBA465932696B495002B41DB /* APIService+WebFinger.swift in Sources */,
DBEFCD7B282A162400C0ABEA /* ReportReasonView.swift in Sources */,
DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */,
DB63F77B279ACAE500455B82 /* DataSourceFacade+Favorite.swift in Sources */,
DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */,
@ -4164,6 +4216,7 @@
DB4F097526A037F500D62E92 /* SearchHistoryViewModel.swift in Sources */,
DB3EA8E9281B7A3700598866 /* DiscoveryCommunityViewModel.swift in Sources */,
DB6180F826391D660018D199 /* MediaPreviewingViewController.swift in Sources */,
DBEFCD71282A12B200C0ABEA /* ReportReasonViewController.swift in Sources */,
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
DB98EB5627B0FF1B0082E365 /* ReportViewControllerAppearance.swift in Sources */,
DB938F1526241FDF00E5B6C1 /* APIService+Thread.swift in Sources */,
@ -4200,6 +4253,7 @@
2D7867192625B77500211898 /* NotificationItem.swift in Sources */,
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */,
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */,
DBEFCD74282A130400C0ABEA /* ReportReasonViewModel.swift in Sources */,
2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */,
DB36679D268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift in Sources */,
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */,
@ -4287,6 +4341,7 @@
DB0617EB277EF3820030EE79 /* GradientBorderView.swift in Sources */,
DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */,
DB63F74B279914A000455B82 /* FollowingListViewController+DataSourceProvider.swift in Sources */,
DBEFCD7D282A2A3B00C0ABEA /* ReportServerRulesViewController.swift in Sources */,
DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */,
DB938F3326243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift in Sources */,
DB98EB4927B0F0CD0082E365 /* ReportStatusTableViewCell.swift in Sources */,
@ -4297,6 +4352,7 @@
DB98EB6527B216500082E365 /* ReportResultViewModel.swift in Sources */,
DB4F096A269EDAD200D62E92 /* SearchResultViewModel+State.swift in Sources */,
5BB04FF5262F0E6D0043BFF6 /* ReportSection.swift in Sources */,
DBEFCD82282A2AB100C0ABEA /* ReportServerRulesView.swift in Sources */,
DBA94436265CBB7400C537E1 /* ProfileFieldItem.swift in Sources */,
DB023D2A27A0FE5C005AC798 /* DataSourceProvider+NotificationTableViewCellDelegate.swift in Sources */,
DB98EB6027B10E150082E365 /* ReportCommentTableViewCell.swift in Sources */,

View File

@ -109,7 +109,7 @@
<key>MastodonIntent.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>28</integer>
<integer>20</integer>
</dict>
<key>MastodonIntents.xcscheme_^#shared#^_</key>
<dict>
@ -124,12 +124,12 @@
<key>NotificationService.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>27</integer>
<integer>19</integer>
</dict>
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>26</integer>
<integer>21</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>

View File

@ -158,7 +158,7 @@ extension SceneCoordinator {
case mastodonServerRules(viewModel: MastodonServerRulesViewModel)
case mastodonConfirmEmail(viewModel: MastodonConfirmEmailViewModel)
case mastodonResendEmail(viewModel: MastodonResendEmailViewModel)
case mastodonWebView(viewModel:WebViewModel)
case mastodonWebView(viewModel: WebViewModel)
// search
case searchDetail(viewModel: SearchDetailViewModel)
@ -184,6 +184,8 @@ extension SceneCoordinator {
// report
case report(viewModel: ReportViewModel)
case reportServerRules(viewModel: ReportServerRulesViewModel)
case reportStatus(viewModel: ReportStatusViewModel)
case reportSupplementary(viewModel: ReportSupplementaryViewModel)
case reportResult(viewModel: ReportResultViewModel)
@ -447,6 +449,14 @@ private extension SceneCoordinator {
let _viewController = ReportViewController()
_viewController.viewModel = viewModel
viewController = _viewController
case .reportServerRules(let viewModel):
let _viewController = ReportServerRulesViewController()
_viewController.viewModel = viewModel
viewController = _viewController
case .reportStatus(let viewModel):
let _viewController = ReportStatusViewController()
_viewController.viewModel = viewModel
viewController = _viewController
case .reportSupplementary(let viewModel):
let _viewController = ReportSupplementaryViewController()
_viewController.viewModel = viewModel

View File

@ -229,6 +229,11 @@ extension MastodonConfirmEmailViewController {
}
}
// MARK: - PanPopableViewController
extension MastodonConfirmEmailViewController: PanPopableViewController {
var isPanPopable: Bool { false }
}
// MARK: - OnboardingViewControllerAppearance
extension MastodonConfirmEmailViewController: OnboardingViewControllerAppearance { }

View File

@ -197,9 +197,6 @@ struct MastodonRegisterView: View {
}
}
}
struct WidthKey: PreferenceKey {

View File

@ -0,0 +1,165 @@
//
// ReportViewController.swift
// Mastodon
//
// Created by ihugo on 2021/4/20.
//
import os.log
import UIKit
import Combine
import CoreDataStack
import MastodonAsset
import MastodonLocalization
class ReportViewController: UIViewController, NeedsDependency, ReportViewControllerAppearance {
let logger = Logger(subsystem: "ReportViewController", category: "ViewController")
var disposeBag = Set<AnyCancellable>()
private var observations = Set<NSKeyValueObservation>()
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
var viewModel: ReportViewModel!
deinit {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
}
extension ReportViewController {
override func viewDidLoad() {
super.viewDidLoad()
setupAppearance()
defer { setupNavigationBarBackgroundView() }
viewModel.reportReasonViewModel.delegate = self
viewModel.reportServerRulesViewModel.delegate = self
viewModel.reportStatusViewModel.delegate = self
viewModel.reportSupplementaryViewModel.delegate = self
let reportReasonViewController = ReportReasonViewController()
reportReasonViewController.context = context
reportReasonViewController.coordinator = coordinator
reportReasonViewController.viewModel = viewModel.reportReasonViewModel
addChild(reportReasonViewController)
reportReasonViewController.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(reportReasonViewController.view)
reportReasonViewController.didMove(toParent: self)
NSLayoutConstraint.activate([
reportReasonViewController.view.topAnchor.constraint(equalTo: view.topAnchor),
reportReasonViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
reportReasonViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
reportReasonViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
}
}
// MARK: - UIAdaptivePresentationControllerDelegate
extension ReportViewController: UIAdaptivePresentationControllerDelegate {
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
return viewModel.isReportSuccess
}
}
// MARK: - ReportReasonViewControllerDelegate
extension ReportViewController: ReportReasonViewControllerDelegate {
func reportReasonViewController(_ viewController: ReportReasonViewController, nextButtonPressed button: UIButton) {
guard let reason = viewController.viewModel.selectReason else { return }
switch reason {
case .violateRule:
coordinator.present(
scene: .reportServerRules(viewModel: viewModel.reportServerRulesViewModel),
from: self,
transition: .show
)
default:
break
}
}
}
// MARK: - ReportServerRulesViewControllerDelegate
extension ReportViewController: ReportServerRulesViewControllerDelegate {
func reportServerRulesViewController(_ viewController: ReportServerRulesViewController, nextButtonPressed button: UIButton) {
if viewController.viewModel.isDislike {
} else if viewController.viewModel.selectRule != nil {
coordinator.present(
scene: .reportStatus(viewModel: viewModel.reportStatusViewModel),
from: self,
transition: .show
)
} else {
assertionFailure()
}
}
}
// MARK: - ReportStatusViewControllerDelegate
extension ReportViewController: ReportStatusViewControllerDelegate {
func reportStatusViewController(_ viewController: ReportStatusViewController, skipButtonDidPressed button: UIButton) {
coordinateToReportSupplementary()
}
func reportStatusViewController(_ viewController: ReportStatusViewController, nextButtonDidPressed button: UIButton) {
coordinateToReportSupplementary()
}
private func coordinateToReportSupplementary() {
coordinator.present(
scene: .reportSupplementary(viewModel: viewModel.reportSupplementaryViewModel),
from: self,
transition: .show
)
}
}
// MARK: - ReportSupplementaryViewControllerDelegate
extension ReportViewController: ReportSupplementaryViewControllerDelegate {
func reportSupplementaryViewController(_ viewController: ReportSupplementaryViewController, skipButtonDidPressed button: UIButton) {
report()
}
func reportSupplementaryViewController(_ viewController: ReportSupplementaryViewController, nextButtonDidPressed button: UIButton) {
report()
}
private func report() {
Task { @MainActor in
do {
let _ = try await viewModel.report()
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): report success")
let reportResultViewModel = ReportResultViewModel(
context: context,
user: viewModel.user
)
coordinator.present(
scene: .reportResult(viewModel: reportResultViewModel),
from: self,
transition: .show
)
} catch {
let alertController = UIAlertController(for: error, title: nil, preferredStyle: .alert)
let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil)
alertController.addAction(okAction)
self.coordinator.present(
scene: .alertController(alertController: alertController),
from: nil,
transition: .alertController(animated: true, completion: nil)
)
}
} // end Task
}
}

View File

@ -0,0 +1,176 @@
//
// ReportViewModel.swift
// Mastodon
//
// Created by ihugo on 2021/4/19.
//
import Combine
import CoreData
import CoreDataStack
import Foundation
import GameplayKit
import MastodonSDK
import OrderedCollections
import os.log
import UIKit
class ReportViewModel {
var disposeBag = Set<AnyCancellable>()
let reportReasonViewModel: ReportReasonViewModel
let reportServerRulesViewModel: ReportServerRulesViewModel
let reportStatusViewModel: ReportStatusViewModel
let reportSupplementaryViewModel: ReportSupplementaryViewModel
// input
let context: AppContext
let user: ManagedObjectRecord<MastodonUser>
let status: ManagedObjectRecord<Status>?
// output
@Published var isReporting = false
@Published var isReportSuccess = false
init(
context: AppContext,
user: ManagedObjectRecord<MastodonUser>,
status: ManagedObjectRecord<Status>?
) {
self.context = context
self.user = user
self.status = status
self.reportReasonViewModel = ReportReasonViewModel(context: context)
self.reportServerRulesViewModel = ReportServerRulesViewModel(context: context)
self.reportStatusViewModel = ReportStatusViewModel(context: context, user: user, status: status)
self.reportSupplementaryViewModel = ReportSupplementaryViewModel(context: context, user: user)
// end init
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
return
}
// setup reason viewModel
if status != nil {
// TODO: i18n
reportReasonViewModel.headline = "Whats wrong with post?"
} else {
Task { @MainActor in
let managedObjectContext = context.managedObjectContext
let _username: String? = try? await managedObjectContext.perform {
let user = user.object(in: managedObjectContext)
return user?.acctWithDomain
}
if let username = _username {
reportReasonViewModel.headline = "Whats wrong with @\(username)?"
} else {
reportReasonViewModel.headline = "Whats wrong with this account?"
}
} // end Task
}
// bind server rules
Task { @MainActor in
do {
let response = try await context.apiService.instance(domain: authenticationBox.domain)
.timeout(3, scheduler: DispatchQueue.main)
.singleOutput()
let rules = response.value.rules ?? []
reportReasonViewModel.serverRules = rules
reportServerRulesViewModel.serverRules = rules
} catch {
reportReasonViewModel.serverRules = []
reportServerRulesViewModel.serverRules = []
}
} // end Task
$isReporting
.assign(to: &reportSupplementaryViewModel.$isBusy)
}
}
extension ReportViewModel {
@MainActor
func report() async throws {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value,
!isReporting
else {
assertionFailure()
return
}
let managedObjectContext = context.managedObjectContext
let _query: Mastodon.API.Reports.FileReportQuery? = try await managedObjectContext.perform {
guard let user = self.user.object(in: managedObjectContext) else { return nil }
let statusIDs: [Status.ID]? = {
if self.reportStatusViewModel.isSkip {
let _id: Status.ID? = self.reportStatusViewModel.status.flatMap { record -> Status.ID? in
guard let status = record.object(in: managedObjectContext) else { return nil }
return status.id
}
return _id.flatMap { [$0] }
} else {
return self.reportStatusViewModel.selectStatuses.compactMap { record -> Status.ID? in
guard let status = record.object(in: managedObjectContext) else { return nil }
return status.id
}
}
}()
let comment: String? = {
var suffixes: [String] = []
let content: String?
if let reason = self.reportReasonViewModel.selectReason {
switch reason {
case .spam:
suffixes.append(reason.rawValue)
case .violateRule:
suffixes.append(reason.rawValue)
if let rule = self.reportServerRulesViewModel.selectRule {
suffixes.append(rule.text)
}
case .dislike, .other:
break
}
}
content = self.reportSupplementaryViewModel.isSkip ? nil : self.reportSupplementaryViewModel.commentContext.comment
let suffix: String? = {
let text = suffixes.joined(separator: ". ")
guard !text.isEmpty else { return nil }
return "<" + text + ">"
}()
let comment = [content, suffix]
.compactMap { $0 }
.joined(separator: " ")
return comment.isEmpty ? nil : comment
}()
return Mastodon.API.Reports.FileReportQuery(
accountID: user.id,
statusIDs: statusIDs,
comment: comment,
forward: true
)
}
guard let query = _query else { return }
do {
isReporting = true
try await Task.sleep(nanoseconds: .second * 3)
// let _ = try await context.apiService.report(
// query: query,
// authenticationBox: authenticationBox
// )
isReportSuccess = true
} catch {
isReporting = false
throw error
}
}
}

View File

@ -0,0 +1,113 @@
//
// ReportReasonView.swift
// Mastodon
//
// Created by MainasuK on 2022-5-10.
//
import UIKit
import SwiftUI
import MastodonLocalization
import MastodonSDK
import MastodonAsset
struct ReportReasonView: View {
@ObservedObject var viewModel: ReportReasonViewModel
// TODO: i18n
var body: some View {
ScrollView(.vertical) {
HStack {
VStack(alignment: .leading, spacing: 8) {
Text("Step 1 of 4")
.foregroundColor(Color(Asset.Colors.Label.secondary.color))
.font(Font(UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)) as CTFont))
Text(viewModel.headline)
.foregroundColor(Color(Asset.Colors.Label.primary.color))
.font(Font(UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: .systemFont(ofSize: 28, weight: .bold)) as CTFont))
Text("Select the best match")
.foregroundColor(Color(Asset.Colors.Label.secondary.color))
.font(Font(UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)) as CTFont))
}
Spacer()
}
.padding()
VStack(spacing: 16) {
if let serverRules = viewModel.serverRules {
ForEach(ReportReasonViewModel.Reason.allCases, id: \.self) { reason in
switch reason {
case .violateRule where serverRules.isEmpty:
EmptyView()
default:
ReportReasonRowView(reason: reason, isSelect: reason == viewModel.selectReason)
.background(
Color(viewModel.backgroundColor)
)
.onTapGesture {
viewModel.selectReason = reason
}
}
}
} else {
ProgressView()
}
}
.padding()
.transition(.opacity)
.animation(.easeInOut)
Spacer()
.frame(minHeight: viewModel.bottomPaddingHeight)
}
.background(
Color(viewModel.backgroundColor)
)
}
}
struct ReportReasonRowView: View {
var reason: ReportReasonViewModel.Reason
var isSelect: Bool
var body: some View {
HStack(spacing: 14) {
Image(systemName: isSelect ? "checkmark.circle.fill" : "circle")
.resizable()
.frame(width: 28, height: 28, alignment: .center)
VStack(alignment: .leading, spacing: 4) {
Text(reason.title)
.foregroundColor(Color(Asset.Colors.Label.primary.color))
.font(.headline)
Text(reason.subtitle)
.font(.subheadline)
.foregroundColor(Color(Asset.Colors.Label.secondary.color))
}
Spacer()
}
}
}
#if DEBUG
struct ReportReasonView_Previews: PreviewProvider {
static var previews: some View {
Group {
NavigationView {
ReportReasonView(viewModel: ReportReasonViewModel(context: .shared))
.navigationBarTitle(Text(""))
.navigationBarTitleDisplayMode(.inline)
}
NavigationView {
ReportReasonView(viewModel: ReportReasonViewModel(context: .shared))
.navigationBarTitle(Text(""))
.navigationBarTitleDisplayMode(.inline)
}
.preferredColorScheme(.dark)
}
}
}
#endif

View File

@ -0,0 +1,116 @@
//
// ReportReasonViewController.swift
// Mastodon
//
// Created by MainasuK on 2022-5-10.
//
import os.log
import UIKit
import SwiftUI
import Combine
import MastodonUI
import MastodonAsset
import MastodonLocalization
protocol ReportReasonViewControllerDelegate: AnyObject {
func reportReasonViewController(_ viewController: ReportReasonViewController, nextButtonPressed button: UIButton)
}
final class ReportReasonViewController: UIViewController, NeedsDependency, ReportViewControllerAppearance {
let logger = Logger(subsystem: "ReportReasonViewController", category: "ViewController")
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
var disposeBag = Set<AnyCancellable>()
private var observations = Set<NSKeyValueObservation>()
var viewModel: ReportReasonViewModel!
private(set) lazy var reportReasonView = ReportReasonView(viewModel: viewModel)
lazy var cancelBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .cancel,
target: self,
action: #selector(ReportReasonViewController.cancelBarButtonItemDidPressed(_:))
)
let navigationActionView: NavigationActionView = {
let navigationActionView = NavigationActionView()
navigationActionView.backgroundColor = Asset.Scene.Onboarding.background.color
navigationActionView.hidesBackButton = true
return navigationActionView
}()
deinit {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
}
extension ReportReasonViewController {
override func viewDidLoad() {
super.viewDidLoad()
setupAppearance()
defer { setupNavigationBarBackgroundView() }
navigationItem.rightBarButtonItem = cancelBarButtonItem
let hostingViewController = UIHostingController(rootView: reportReasonView)
hostingViewController.view.preservesSuperviewLayoutMargins = true
addChild(hostingViewController)
hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(hostingViewController.view)
NSLayoutConstraint.activate([
hostingViewController.view.topAnchor.constraint(equalTo: view.topAnchor),
hostingViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hostingViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
hostingViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
navigationActionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(navigationActionView)
defer {
view.bringSubviewToFront(navigationActionView)
}
NSLayoutConstraint.activate([
navigationActionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
navigationActionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
view.bottomAnchor.constraint(equalTo: navigationActionView.bottomAnchor),
])
navigationActionView
.observe(\.bounds, options: [.initial, .new]) { [weak self] navigationActionView, _ in
guard let self = self else { return }
let inset = navigationActionView.frame.height
self.viewModel.bottomPaddingHeight = inset
}
.store(in: &observations)
viewModel.$selectReason
.map { $0 != nil }
.assign(to: \.isEnabled, on: navigationActionView.nextButton)
.store(in: &disposeBag)
navigationActionView.nextButton.addTarget(self, action: #selector(ReportReasonViewController.nextButtonPressed(_:)), for: .touchUpInside)
}
}
extension ReportReasonViewController {
@objc private func cancelBarButtonItemDidPressed(_ sender: UIBarButtonItem) {
dismiss(animated: true, completion: nil)
}
@objc private func nextButtonPressed(_ sender: UIButton) {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
assert(viewModel.delegate != nil)
viewModel.delegate?.reportReasonViewController(self, nextButtonPressed: sender)
}
}

View File

@ -0,0 +1,83 @@
//
// ReportReasonViewModel.swift
// Mastodon
//
// Created by MainasuK on 2022-5-10.
//
import UIKit
import SwiftUI
import MastodonAsset
import MastodonSDK
final class ReportReasonViewModel: ObservableObject {
weak var delegate: ReportReasonViewControllerDelegate?
// input
let context: AppContext
@Published var headline = "What's wrong with this account?"
@Published var serverRules: [Mastodon.Entity.Instance.Rule]?
@Published var bottomPaddingHeight: CGFloat = .zero
@Published var backgroundColor: UIColor = Asset.Scene.Report.background.color
// output
@Published var selectReason: Reason?
init(context: AppContext) {
self.context = context
// end init
}
}
extension ReportReasonViewModel {
enum Reason: Hashable, CaseIterable {
case dislike
case spam
case violateRule
case other
var title: String {
switch self {
case .dislike:
return "I dont like it"
case .spam:
return "Its spam"
case .violateRule:
return "It violates server rules"
case .other:
return "Its something else"
}
}
var subtitle: String {
switch self {
case .dislike:
return "It is not something you want to see"
case .spam:
return "Malicious links, fake engagement, or repetetive replies"
case .violateRule:
return "You are aware that it breaks specific rules"
case .other:
return "The issue does not fit into other categories"
}
}
// do not i18n this
var rawValue: String {
switch self {
case .dislike:
return "I dont like it"
case .spam:
return "Its spam"
case .violateRule:
return "It violates server rules"
case .other:
return "Its something else"
}
}
}
}

View File

@ -21,6 +21,12 @@ final class ReportResultViewController: UIViewController, NeedsDependency, Repor
var viewModel: ReportResultViewModel!
lazy var doneBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .done,
target: self,
action: #selector(ReportResultViewController.doneBarButtonItemDidPressed(_:))
)
let tableView: UITableView = {
let tableView = ControlContainableTableView()
tableView.backgroundColor = Asset.Scene.Report.background.color
@ -60,6 +66,7 @@ extension ReportResultViewController {
defer { setupNavigationBarBackgroundView() }
navigationItem.hidesBackButton = true
navigationItem.rightBarButtonItem = doneBarButtonItem
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
@ -102,6 +109,10 @@ extension ReportResultViewController {
}
extension ReportResultViewController {
@objc func doneBarButtonItemDidPressed(_ sender: UIButton) {
dismiss(animated: true, completion: nil)
}
@objc func nextButtonDidPressed(_ sender: UIButton) {
dismiss(animated: true, completion: nil)
@ -111,3 +122,8 @@ extension ReportResultViewController {
// MARK: - UITableViewDelegate
extension ReportResultViewController: UITableViewDelegate { }
// MARK: - PanPopableViewController
extension ReportResultViewController: PanPopableViewController {
var isPanPopable: Bool { false }
}

View File

@ -0,0 +1,116 @@
//
// ReportServerRulesView.swift
// Mastodon
//
// Created by MainasuK on 2022-5-10.
//
import UIKit
import SwiftUI
import MastodonLocalization
import MastodonSDK
import MastodonAsset
struct ReportServerRulesView: View {
@ObservedObject var viewModel: ReportServerRulesViewModel
// TODO: i18n
var body: some View {
ScrollView(.vertical) {
HStack {
VStack(alignment: .leading, spacing: 8) {
Text("Step 2 of 4")
.foregroundColor(Color(Asset.Colors.Label.secondary.color))
.font(Font(UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)) as CTFont))
Text(viewModel.headline)
.foregroundColor(Color(Asset.Colors.Label.primary.color))
.font(Font(UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: .systemFont(ofSize: 28, weight: .bold)) as CTFont))
Text("Select all that apply")
.foregroundColor(Color(Asset.Colors.Label.secondary.color))
.font(Font(UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)) as CTFont))
}
Spacer()
}
.padding()
VStack(spacing: 32) {
ForEach(viewModel.serverRules, id: \.self) { rule in
ReportServerRulesRowView(
title: rule.text,
isSelect: rule == viewModel.selectRule
)
.background(
Color(viewModel.backgroundColor)
)
.onTapGesture {
viewModel.selectRule = rule
viewModel.isDislike = false
}
}
ReportServerRulesRowView(
title: "I just dont like it",
isSelect: viewModel.isDislike
)
.background(
Color(viewModel.backgroundColor)
)
.onTapGesture {
viewModel.selectRule = nil
viewModel.isDislike = true
}
}
.padding()
.transition(.opacity)
.animation(.easeInOut)
Spacer()
.frame(minHeight: viewModel.bottomPaddingHeight)
}
.background(
Color(viewModel.backgroundColor)
)
}
}
struct ReportServerRulesRowView: View {
var title: String
var isSelect: Bool
var body: some View {
HStack(spacing: 14) {
Image(systemName: isSelect ? "checkmark.circle.fill" : "circle")
.resizable()
.frame(width: 28, height: 28, alignment: .center)
VStack(alignment: .leading, spacing: 4) {
Text(title)
.foregroundColor(Color(Asset.Colors.Label.primary.color))
.font(.headline)
}
Spacer()
}
}
}
#if DEBUG
struct ReportServerRulesView_Previews: PreviewProvider {
static var previews: some View {
Group {
NavigationView {
ReportServerRulesView(viewModel: ReportServerRulesViewModel(context: .shared))
.navigationBarTitle(Text(""))
.navigationBarTitleDisplayMode(.inline)
}
NavigationView {
ReportServerRulesView(viewModel: ReportServerRulesViewModel(context: .shared))
.navigationBarTitle(Text(""))
.navigationBarTitleDisplayMode(.inline)
}
.preferredColorScheme(.dark)
}
}
}
#endif

View File

@ -0,0 +1,117 @@
//
// ReportServerRulesViewController.swift
// Mastodon
//
// Created by MainasuK on 2022-5-10.
//
import os.log
import UIKit
import SwiftUI
import Combine
import MastodonUI
import MastodonAsset
import MastodonLocalization
protocol ReportServerRulesViewControllerDelegate: AnyObject {
func reportServerRulesViewController(_ viewController: ReportServerRulesViewController, nextButtonPressed button: UIButton)
}
final class ReportServerRulesViewController: UIViewController, NeedsDependency, ReportViewControllerAppearance {
let logger = Logger(subsystem: "ReportReasonViewController", category: "ViewController")
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
var disposeBag = Set<AnyCancellable>()
private var observations = Set<NSKeyValueObservation>()
var viewModel: ReportServerRulesViewModel!
private(set) lazy var reportServerRulesView = ReportServerRulesView(viewModel: viewModel)
lazy var cancelBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .cancel,
target: self,
action: #selector(ReportServerRulesViewController.cancelBarButtonItemDidPressed(_:))
)
let navigationActionView: NavigationActionView = {
let navigationActionView = NavigationActionView()
navigationActionView.backgroundColor = Asset.Scene.Onboarding.background.color
navigationActionView.hidesBackButton = true
return navigationActionView
}()
deinit {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
}
extension ReportServerRulesViewController {
override func viewDidLoad() {
super.viewDidLoad()
setupAppearance()
defer { setupNavigationBarBackgroundView() }
let hostingViewController = UIHostingController(rootView: reportServerRulesView)
hostingViewController.view.preservesSuperviewLayoutMargins = true
addChild(hostingViewController)
hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(hostingViewController.view)
NSLayoutConstraint.activate([
hostingViewController.view.topAnchor.constraint(equalTo: view.topAnchor),
hostingViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hostingViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
hostingViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
navigationActionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(navigationActionView)
defer {
view.bringSubviewToFront(navigationActionView)
}
NSLayoutConstraint.activate([
navigationActionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
navigationActionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
view.bottomAnchor.constraint(equalTo: navigationActionView.bottomAnchor),
])
navigationActionView
.observe(\.bounds, options: [.initial, .new]) { [weak self] navigationActionView, _ in
guard let self = self else { return }
let inset = navigationActionView.frame.height
self.viewModel.bottomPaddingHeight = inset
}
.store(in: &observations)
Publishers.CombineLatest(
viewModel.$selectRule,
viewModel.$isDislike
)
.map { $0 != nil || $1 }
.assign(to: \.isEnabled, on: navigationActionView.nextButton)
.store(in: &disposeBag)
navigationActionView.nextButton.addTarget(self, action: #selector(ReportServerRulesViewController.nextButtonPressed(_:)), for: .touchUpInside)
}
}
extension ReportServerRulesViewController {
@objc private func cancelBarButtonItemDidPressed(_ sender: UIBarButtonItem) {
dismiss(animated: true, completion: nil)
}
@objc private func nextButtonPressed(_ sender: UIButton) {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
assert(viewModel.delegate != nil)
viewModel.delegate?.reportServerRulesViewController(self, nextButtonPressed: sender)
}
}

View File

@ -0,0 +1,35 @@
//
// ReportServerRulesViewModel.swift
// Mastodon
//
// Created by MainasuK on 2022-5-10.
//
import UIKit
import SwiftUI
import MastodonAsset
import MastodonSDK
final class ReportServerRulesViewModel: ObservableObject {
weak var delegate: ReportServerRulesViewControllerDelegate?
// input
let context: AppContext
@Published var headline = "Which rules are being violated?"
@Published var serverRules: [Mastodon.Entity.Instance.Rule] = []
@Published var bottomPaddingHeight: CGFloat = .zero
@Published var backgroundColor: UIColor = Asset.Scene.Report.background.color
// output
@Published var selectRule: Mastodon.Entity.Instance.Rule?
@Published var isDislike: Bool = false
init(context: AppContext) {
self.context = context
// end init
}
}

View File

@ -1,8 +1,8 @@
//
// ReportViewController.swift
// ReportStatusViewController.swift
// Mastodon
//
// Created by ihugo on 2021/4/20.
// Created by MainasuK on 2022-5-10.
//
import os.log
@ -12,21 +12,29 @@ import CoreDataStack
import MastodonAsset
import MastodonLocalization
class ReportViewController: UIViewController, NeedsDependency, ReportViewControllerAppearance {
protocol ReportStatusViewControllerDelegate: AnyObject {
func reportStatusViewController(_ viewController: ReportStatusViewController, skipButtonDidPressed button: UIButton)
func reportStatusViewController(_ viewController: ReportStatusViewController, nextButtonDidPressed button: UIButton)
}
class ReportStatusViewController: UIViewController, NeedsDependency, ReportViewControllerAppearance {
let logger = Logger(subsystem: "ReportStatusViewController", category: "ViewController")
var disposeBag = Set<AnyCancellable>()
private var observations = Set<NSKeyValueObservation>()
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
var viewModel: ReportViewModel!
var viewModel: ReportStatusViewModel!
// MAKK: - UI
lazy var cancelBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .cancel,
target: self,
action: #selector(ReportViewController.cancelBarButtonItemDidPressed(_:))
action: #selector(ReportStatusViewController.cancelBarButtonItemDidPressed(_:))
)
let tableView: UITableView = {
@ -58,7 +66,7 @@ class ReportViewController: UIViewController, NeedsDependency, ReportViewControl
}
extension ReportViewController {
extension ReportStatusViewController {
override func viewDidLoad() {
super.viewDidLoad()
@ -67,7 +75,7 @@ extension ReportViewController {
defer { setupNavigationBarBackgroundView() }
navigationItem.rightBarButtonItem = cancelBarButtonItem
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
NSLayoutConstraint.activate([
@ -109,7 +117,7 @@ extension ReportViewController {
.sink { [weak self] _ in
guard let self = self else { return }
guard self.view.window != nil else { return }
self.viewModel.stateMachine.enter(ReportViewModel.State.Loading.self)
self.viewModel.stateMachine.enter(ReportStatusViewModel.State.Loading.self)
}
.store(in: &disposeBag)
@ -118,56 +126,38 @@ extension ReportViewController {
.assign(to: \.isEnabled, on: navigationActionView.nextButton)
.store(in: &disposeBag)
navigationActionView.backButton.addTarget(self, action: #selector(ReportViewController.skipButtonDidPressed(_:)), for: .touchUpInside)
navigationActionView.nextButton.addTarget(self, action: #selector(ReportViewController.nextButtonDidPressed(_:)), for: .touchUpInside)
navigationActionView.backButton.addTarget(self, action: #selector(ReportStatusViewController.skipButtonDidPressed(_:)), for: .touchUpInside)
navigationActionView.nextButton.addTarget(self, action: #selector(ReportStatusViewController.nextButtonDidPressed(_:)), for: .touchUpInside)
}
}
extension ReportViewController {
extension ReportStatusViewController {
@objc private func cancelBarButtonItemDidPressed(_ sender: UIBarButtonItem) {
dismiss(animated: true, completion: nil)
}
@objc func skipButtonDidPressed(_ sender: UIButton) {
var selectStatuses: [ManagedObjectRecord<Status>] = []
if let selectStatus = viewModel.status {
selectStatuses.append(selectStatus)
}
@objc private func skipButtonDidPressed(_ sender: UIButton) {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
let reportSupplementaryViewModel = ReportSupplementaryViewModel(
context: context,
user: viewModel.user,
selectStatuses: selectStatuses
)
coordinator.present(
scene: .reportSupplementary(viewModel: reportSupplementaryViewModel),
from: self,
transition: .show
)
assert(viewModel.delegate != nil)
viewModel.isSkip = true
viewModel.delegate?.reportStatusViewController(self, skipButtonDidPressed: sender)
}
@objc func nextButtonDidPressed(_ sender: UIButton) {
let selectStatuses = Array(viewModel.selectStatuses)
guard !selectStatuses.isEmpty else { return }
@objc private func nextButtonDidPressed(_ sender: UIButton) {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
let reportSupplementaryViewModel = ReportSupplementaryViewModel(
context: context,
user: viewModel.user,
selectStatuses: selectStatuses
)
coordinator.present(
scene: .reportSupplementary(viewModel: reportSupplementaryViewModel),
from: self,
transition: .show
)
assert(viewModel.delegate != nil)
viewModel.isSkip = false
viewModel.delegate?.reportStatusViewController(self, nextButtonDidPressed: sender)
}
}
// MARK: - UITableViewDelegate
extension ReportViewController: UITableViewDelegate {
extension ReportStatusViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
guard let item = viewModel.diffableDataSource?.itemIdentifier(for: indexPath),
case .status = item
@ -214,7 +204,7 @@ extension ReportViewController: UITableViewDelegate {
}
// MARK: - UIAdaptivePresentationControllerDelegate
extension ReportViewController: UIAdaptivePresentationControllerDelegate {
extension ReportStatusViewController: UIAdaptivePresentationControllerDelegate {
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
return false
}

View File

@ -12,11 +12,11 @@ import CoreDataStack
import MastodonAsset
import MastodonLocalization
extension ReportViewModel {
extension ReportStatusViewModel {
static let reportItemHeaderContext = ReportItem.HeaderContext(
primaryLabelText: L10n.Scene.Report.content1,
secondaryLabelText: L10n.Scene.Report.step1
secondaryLabelText: "Step 3 of 4"
)
func setupDiffableDataSource(
@ -41,7 +41,7 @@ extension ReportViewModel {
var snapshot = NSDiffableDataSourceSnapshot<ReportSection, ReportItem>()
snapshot.appendSections([.main])
snapshot.appendItems([.header(context: ReportViewModel.reportItemHeaderContext)], toSection: .main)
snapshot.appendItems([.header(context: ReportStatusViewModel.reportItemHeaderContext)], toSection: .main)
let items = records.map { ReportItem.status(record: $0) }
snapshot.appendItems(items, toSection: .main)

View File

@ -12,7 +12,7 @@ import CoreData
import CoreDataStack
import GameplayKit
extension ReportViewModel {
extension ReportStatusViewModel {
class State: GKState {
let logger = Logger(subsystem: "ReportViewModel.State", category: "StateMachine")
@ -23,15 +23,15 @@ extension ReportViewModel {
String(describing: Self.self)
}
weak var viewModel: ReportViewModel?
weak var viewModel: ReportStatusViewModel?
init(viewModel: ReportViewModel) {
init(viewModel: ReportStatusViewModel) {
self.viewModel = viewModel
}
override func didEnter(from previousState: GKState?) {
super.didEnter(from: previousState)
let previousState = previousState as? ReportViewModel.State
let previousState = previousState as? ReportStatusViewModel.State
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] enter \(self.name), previous: \(previousState?.name ?? "<nil>")")
}
@ -46,8 +46,8 @@ extension ReportViewModel {
}
}
extension ReportViewModel.State {
class Initial: ReportViewModel.State {
extension ReportStatusViewModel.State {
class Initial: ReportStatusViewModel.State {
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
guard let _ = viewModel else { return false }
switch stateClass {
@ -59,7 +59,7 @@ extension ReportViewModel.State {
}
}
class Loading: ReportViewModel.State {
class Loading: ReportStatusViewModel.State {
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
switch stateClass {
case is Fail.Type:
@ -128,7 +128,7 @@ extension ReportViewModel.State {
}
}
class Fail: ReportViewModel.State {
class Fail: ReportStatusViewModel.State {
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
switch stateClass {
case is Loading.Type:
@ -139,7 +139,7 @@ extension ReportViewModel.State {
}
}
class Idle: ReportViewModel.State {
class Idle: ReportStatusViewModel.State {
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
switch stateClass {
case is Loading.Type:
@ -150,7 +150,7 @@ extension ReportViewModel.State {
}
}
class NoMore: ReportViewModel.State {
class NoMore: ReportStatusViewModel.State {
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
return false
}

View File

@ -1,8 +1,8 @@
//
// ReportViewModel.swift
// ReportStatusViewModel.swift
// Mastodon
//
// Created by ihugo on 2021/4/19.
// Created by MainasuK on 2022-5-10.
//
import Combine
@ -15,10 +15,12 @@ import OrderedCollections
import os.log
import UIKit
class ReportViewModel {
class ReportStatusViewModel {
var disposeBag = Set<AnyCancellable>()
weak var delegate: ReportStatusViewControllerDelegate?
// input
let context: AppContext
let user: ManagedObjectRecord<MastodonUser>
@ -26,6 +28,7 @@ class ReportViewModel {
let statusFetchedResultsController: StatusFetchedResultsController
let listBatchFetchViewModel = ListBatchFetchViewModel()
@Published var isSkip = false
@Published var selectStatuses = OrderedSet<ManagedObjectRecord<Status>>()
// output

View File

@ -11,20 +11,25 @@ import Combine
import MastodonAsset
import MastodonLocalization
protocol ReportSupplementaryViewControllerDelegate: AnyObject {
func reportSupplementaryViewController(_ viewController: ReportSupplementaryViewController, skipButtonDidPressed button: UIButton)
func reportSupplementaryViewController(_ viewController: ReportSupplementaryViewController, nextButtonDidPressed button: UIButton)
}
final class ReportSupplementaryViewController: UIViewController, NeedsDependency, ReportViewControllerAppearance {
let logger = Logger(subsystem: "ReportSupplementaryViewController", category: "ViewController")
var disposeBag = Set<AnyCancellable>()
private var observations = Set<NSKeyValueObservation>()
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
var viewModel: ReportSupplementaryViewModel! { willSet { precondition(!isViewLoaded) } }
// MAKK: - UI
lazy var cancelBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .cancel,
target: self,
@ -74,16 +79,14 @@ extension ReportSupplementaryViewController {
setupAppearance()
defer { setupNavigationBarBackgroundView() }
navigationItem.rightBarButtonItem = cancelBarButtonItem
viewModel.$isReporting
viewModel.$isBusy
.receive(on: DispatchQueue.main)
.sink { [weak self] isReporting in
.sink { [weak self] isBusy in
guard let self = self else { return }
self.navigationActionView.isUserInteractionEnabled = !isReporting
self.navigationItem.rightBarButtonItem = isBusy ? self.activityIndicatorBarButtonItem : self.cancelBarButtonItem
}
.store(in: &disposeBag)
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
NSLayoutConstraint.activate([
@ -130,49 +133,25 @@ extension ReportSupplementaryViewController {
}
extension ReportSupplementaryViewController {
private func report(withComment: Bool) {
Task {
do {
let _ = try await viewModel.report(withComment: withComment)
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): report success")
let reportResultViewModel = ReportResultViewModel(
context: context,
user: viewModel.user
)
coordinator.present(
scene: .reportResult(viewModel: reportResultViewModel),
from: self,
transition: .show
)
} catch {
let alertController = UIAlertController(for: error, title: nil, preferredStyle: .alert)
let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil)
alertController.addAction(okAction)
self.coordinator.present(
scene: .alertController(alertController: alertController),
from: nil,
transition: .alertController(animated: true, completion: nil)
)
}
} // end Task
}
}
extension ReportSupplementaryViewController {
@objc private func cancelBarButtonItemDidPressed(_ sender: UIBarButtonItem) {
dismiss(animated: true, completion: nil)
}
@objc func skipButtonDidPressed(_ sender: UIButton) {
report(withComment: false)
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
assert(viewModel.delegate != nil)
viewModel.isSkip = true
viewModel.delegate?.reportSupplementaryViewController(self, skipButtonDidPressed: sender)
}
@objc func nextButtonDidPressed(_ sender: UIButton) {
report(withComment: true)
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
assert(viewModel.delegate != nil)
viewModel.isSkip = false
viewModel.delegate?.reportSupplementaryViewController(self, nextButtonDidPressed: sender)
}
}

View File

@ -15,8 +15,8 @@ import MastodonLocalization
extension ReportSupplementaryViewModel {
static let reportItemHeaderContext = ReportItem.HeaderContext(
primaryLabelText: L10n.Scene.Report.content2,
secondaryLabelText: L10n.Scene.Report.step2
primaryLabelText: "Is there anything else we should know?",
secondaryLabelText: "Step 4 of 4"
)
func setupDiffableDataSource(

View File

@ -12,26 +12,26 @@ import MastodonSDK
class ReportSupplementaryViewModel {
weak var delegate: ReportSupplementaryViewControllerDelegate?
// Input
var context: AppContext
let user: ManagedObjectRecord<MastodonUser>
let selectStatuses: [ManagedObjectRecord<Status>]
let commentContext = ReportItem.CommentContext()
@Published var isSkip = false
@Published var isBusy = false
// output
var diffableDataSource: UITableViewDiffableDataSource<ReportSection, ReportItem>?
@Published var isNextButtonEnabled = false
@Published var isReporting = false
@Published var isReportSuccess = false
init(
context: AppContext,
user: ManagedObjectRecord<MastodonUser>,
selectStatuses: [ManagedObjectRecord<Status>]
user: ManagedObjectRecord<MastodonUser>
) {
self.context = context
self.user = user
self.selectStatuses = selectStatuses
// end init
commentContext.$comment
@ -42,41 +42,3 @@ class ReportSupplementaryViewModel {
}
}
extension ReportSupplementaryViewModel {
func report(withComment: Bool) async throws {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
assertionFailure()
return
}
let managedObjectContext = context.managedObjectContext
let _query: Mastodon.API.Reports.FileReportQuery? = try await managedObjectContext.perform {
guard let user = self.user.object(in: managedObjectContext) else { return nil }
let statusIDs = self.selectStatuses.compactMap { record -> Status.ID? in
guard let status = record.object(in: managedObjectContext) else { return nil }
return status.id
}
return Mastodon.API.Reports.FileReportQuery(
accountID: user.id,
statusIDs: statusIDs,
comment: withComment ? self.commentContext.comment : nil,
forward: nil
)
}
guard let query = _query else { return }
do {
isReporting = true
let _ = try await context.apiService.report(
query: query,
authenticationBox: authenticationBox
)
isReportSuccess = true
} catch {
isReporting = false
throw error
}
}
}

View File

@ -8,6 +8,7 @@
import UIKit
import Combine
import MastodonUI
import MastodonAsset
import MastodonLocalization
import UITextView_Placeholder
@ -27,7 +28,8 @@ final class ReportCommentTableViewCell: UITableViewCell {
textView.attributedPlaceholder = NSAttributedString(
string: L10n.Scene.Report.textPlaceholder,
attributes: [
.font: font
.font: font,
.foregroundColor: Asset.Colors.Label.secondary.color
]
)
textView.textContainerInset = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
@ -80,4 +82,17 @@ extension ReportCommentTableViewCell {
commentTextView.heightAnchor.constraint(greaterThanOrEqualToConstant: 100).priority(.defaultHigh),
])
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
commentTextView.attributedPlaceholder = NSAttributedString(
string: L10n.Scene.Report.textPlaceholder,
attributes: [
.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)),
.foregroundColor: Asset.Colors.Label.secondary.color
]
)
}
}

View File

@ -19,7 +19,7 @@ extension ReportViewControllerAppearance {
func setupAppearance() {
title = L10n.Scene.Report.titleReport
// title = L10n.Scene.Report.titleReport
view.backgroundColor = Asset.Scene.Report.background.color
setupNavigationBarAppearance()

View File

@ -23,6 +23,7 @@ extension AdaptiveStatusBarStyleNavigationController {
override func viewDidLoad() {
super.viewDidLoad()
setupFullWidthBackGesture()
}
@ -45,6 +46,11 @@ extension AdaptiveStatusBarStyleNavigationController: UIGestureRecognizerDelegat
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
let isSystemSwipeToBackEnabled = interactivePopGestureRecognizer?.isEnabled == true
let isThereStackedViewControllers = viewControllers.count > 1
return isSystemSwipeToBackEnabled && isThereStackedViewControllers
let isPanPopable = (topViewController as? PanPopableViewController)?.isPanPopable ?? true
return isSystemSwipeToBackEnabled && isThereStackedViewControllers && isPanPopable
}
}
protocol PanPopableViewController: UIViewController {
var isPanPopable: Bool { get }
}