From 2ef6345d836c7d86d8485df3f521299a822a327f Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 10 May 2022 18:34:39 +0800 Subject: [PATCH] feat: add violates server rules report path --- Mastodon.xcodeproj/project.pbxproj | 78 ++++++-- .../xcschemes/xcschememanagement.plist | 6 +- Mastodon/Coordinator/SceneCoordinator.swift | 12 +- .../MastodonConfirmEmailViewController.swift | 5 + .../Register/MastodonRegisterView.swift | 3 - .../Report/Report/ReportViewController.swift | 165 ++++++++++++++++ .../Scene/Report/Report/ReportViewModel.swift | 176 ++++++++++++++++++ .../ReportReason/ReportReasonView.swift | 113 +++++++++++ .../ReportReasonViewController.swift | 116 ++++++++++++ .../ReportReason/ReportReasonViewModel.swift | 83 +++++++++ .../ReportResultViewController.swift | 16 ++ .../ReportServerRulesView.swift | 116 ++++++++++++ .../ReportServerRulesViewController.swift | 117 ++++++++++++ .../ReportServerRulesViewModel.swift | 35 ++++ ...swift => ReportStatusViewController.swift} | 76 ++++---- ...t => ReportStatusViewModel+Diffable.swift} | 6 +- ...wift => ReportStatusViewModel+State.swift} | 20 +- ...odel.swift => ReportStatusViewModel.swift} | 9 +- .../ReportSupplementaryViewController.swift | 65 +++---- ...eportSupplementaryViewModel+Diffable.swift | 4 +- .../ReportSupplementaryViewModel.swift | 50 +---- .../Cell/ReportCommentTableViewCell.swift | 17 +- .../ReportViewControllerAppearance.swift | 2 +- ...veStatusBarStyleNavigationController.swift | 8 +- 24 files changed, 1129 insertions(+), 169 deletions(-) create mode 100644 Mastodon/Scene/Report/Report/ReportViewController.swift create mode 100644 Mastodon/Scene/Report/Report/ReportViewModel.swift create mode 100644 Mastodon/Scene/Report/ReportReason/ReportReasonView.swift create mode 100644 Mastodon/Scene/Report/ReportReason/ReportReasonViewController.swift create mode 100644 Mastodon/Scene/Report/ReportReason/ReportReasonViewModel.swift create mode 100644 Mastodon/Scene/Report/ReportServerRules/ReportServerRulesView.swift create mode 100644 Mastodon/Scene/Report/ReportServerRules/ReportServerRulesViewController.swift create mode 100644 Mastodon/Scene/Report/ReportServerRules/ReportServerRulesViewModel.swift rename Mastodon/Scene/Report/ReportStatus/{ReportViewController.swift => ReportStatusViewController.swift} (75%) rename Mastodon/Scene/Report/ReportStatus/{ReportViewModel+Diffable.swift => ReportStatusViewModel+Diffable.swift} (93%) rename Mastodon/Scene/Report/ReportStatus/{ReportViewModel+State.swift => ReportStatusViewModel+State.swift} (92%) rename Mastodon/Scene/Report/ReportStatus/{ReportViewModel.swift => ReportStatusViewModel.swift} (91%) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index d5f5cbd79..400e4d73f 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -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 = ""; }; 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 = ""; }; 5B24BBD7262DB14800A9381B /* ReportViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReportViewModel.swift; sourceTree = ""; }; - 5B24BBD8262DB14800A9381B /* ReportViewModel+Diffable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ReportViewModel+Diffable.swift"; sourceTree = ""; }; + 5B24BBD8262DB14800A9381B /* ReportStatusViewModel+Diffable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ReportStatusViewModel+Diffable.swift"; sourceTree = ""; }; 5B24BBE1262DB19100A9381B /* APIService+Report.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+Report.swift"; sourceTree = ""; }; 5B90C456262599800002E742 /* SettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; 5B90C459262599800002E742 /* SettingsToggleTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsToggleTableViewCell.swift; sourceTree = ""; }; @@ -1199,7 +1207,7 @@ DB98337025C9443200AD9700 /* APIService+Authentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Authentication.swift"; sourceTree = ""; }; DB98337E25C9452D00AD9700 /* APIService+APIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+APIError.swift"; sourceTree = ""; }; DB98339B25C96DE600AD9700 /* APIService+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Account.swift"; sourceTree = ""; }; - DB98EB4627B0DFAA0082E365 /* ReportViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReportViewModel+State.swift"; sourceTree = ""; }; + DB98EB4627B0DFAA0082E365 /* ReportStatusViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReportStatusViewModel+State.swift"; sourceTree = ""; }; DB98EB4827B0F0CD0082E365 /* ReportStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportStatusTableViewCell.swift; sourceTree = ""; }; DB98EB4B27B0F2BC0082E365 /* ReportStatusTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReportStatusTableViewCell+ViewModel.swift"; sourceTree = ""; }; DB98EB5227B0F9890082E365 /* ReportHeadlineTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportHeadlineTableViewCell.swift; sourceTree = ""; }; @@ -1329,6 +1337,14 @@ DBEB19E927E4F37B00B0E80E /* ku */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ku; path = ku.lproj/Intents.strings; sourceTree = ""; }; DBEB19EA27E4F37B00B0E80E /* ku */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ku; path = ku.lproj/InfoPlist.strings; sourceTree = ""; }; DBEB19EB27E4F37B00B0E80E /* ku */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ku; path = ku.lproj/Intents.stringsdict; sourceTree = ""; }; + DBEFCD70282A12B200C0ABEA /* ReportReasonViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportReasonViewController.swift; sourceTree = ""; }; + DBEFCD73282A130400C0ABEA /* ReportReasonViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportReasonViewModel.swift; sourceTree = ""; }; + DBEFCD75282A143F00C0ABEA /* ReportStatusViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportStatusViewController.swift; sourceTree = ""; }; + DBEFCD78282A147000C0ABEA /* ReportStatusViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportStatusViewModel.swift; sourceTree = ""; }; + DBEFCD7A282A162400C0ABEA /* ReportReasonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportReasonView.swift; sourceTree = ""; }; + DBEFCD7C282A2A3B00C0ABEA /* ReportServerRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportServerRulesViewController.swift; sourceTree = ""; }; + DBEFCD7F282A2AA900C0ABEA /* ReportServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportServerRulesViewModel.swift; sourceTree = ""; }; + DBEFCD81282A2AB100C0ABEA /* ReportServerRulesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportServerRulesView.swift; sourceTree = ""; }; DBF156DE2701B17600EC00B7 /* SidebarAddAccountCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarAddAccountCollectionViewCell.swift; sourceTree = ""; }; DBF156E02702DA6800EC00B7 /* Mastodon-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Mastodon-Bridging-Header.h"; sourceTree = ""; }; DBF156E12702DA6900EC00B7 /* UIStatusBarManager+HandleTapAction.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIStatusBarManager+HandleTapAction.m"; sourceTree = ""; }; @@ -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 = ""; @@ -3177,6 +3196,35 @@ path = Favorite; sourceTree = ""; }; + DBEFCD72282A12B900C0ABEA /* ReportReason */ = { + isa = PBXGroup; + children = ( + DBEFCD70282A12B200C0ABEA /* ReportReasonViewController.swift */, + DBEFCD73282A130400C0ABEA /* ReportReasonViewModel.swift */, + DBEFCD7A282A162400C0ABEA /* ReportReasonView.swift */, + ); + path = ReportReason; + sourceTree = ""; + }; + DBEFCD77282A144D00C0ABEA /* Report */ = { + isa = PBXGroup; + children = ( + 5BB04FD4262E7AFF0043BFF6 /* ReportViewController.swift */, + 5B24BBD7262DB14800A9381B /* ReportViewModel.swift */, + ); + path = Report; + sourceTree = ""; + }; + DBEFCD7E282A2A3D00C0ABEA /* ReportServerRules */ = { + isa = PBXGroup; + children = ( + DBEFCD7C282A2A3B00C0ABEA /* ReportServerRulesViewController.swift */, + DBEFCD7F282A2AA900C0ABEA /* ReportServerRulesViewModel.swift */, + DBEFCD81282A2AB100C0ABEA /* ReportServerRulesView.swift */, + ); + path = ReportServerRules; + sourceTree = ""; + }; 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 */, diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 07a847a0c..7f54b9011 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -109,7 +109,7 @@ MastodonIntent.xcscheme_^#shared#^_ orderHint - 28 + 20 MastodonIntents.xcscheme_^#shared#^_ @@ -124,12 +124,12 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 27 + 19 ShareActionExtension.xcscheme_^#shared#^_ orderHint - 26 + 21 SuppressBuildableAutocreation diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 5e7fbf472..d149e63a2 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -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 diff --git a/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewController.swift b/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewController.swift index b1b2280d8..cb7a96f85 100644 --- a/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewController.swift +++ b/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewController.swift @@ -229,6 +229,11 @@ extension MastodonConfirmEmailViewController { } } +// MARK: - PanPopableViewController +extension MastodonConfirmEmailViewController: PanPopableViewController { + var isPanPopable: Bool { false } +} + // MARK: - OnboardingViewControllerAppearance extension MastodonConfirmEmailViewController: OnboardingViewControllerAppearance { } diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterView.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterView.swift index 1a47de22f..2be7c61d7 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterView.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterView.swift @@ -197,9 +197,6 @@ struct MastodonRegisterView: View { } } - - - } struct WidthKey: PreferenceKey { diff --git a/Mastodon/Scene/Report/Report/ReportViewController.swift b/Mastodon/Scene/Report/Report/ReportViewController.swift new file mode 100644 index 000000000..6d3f6da06 --- /dev/null +++ b/Mastodon/Scene/Report/Report/ReportViewController.swift @@ -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() + private var observations = Set() + + 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 + } + +} diff --git a/Mastodon/Scene/Report/Report/ReportViewModel.swift b/Mastodon/Scene/Report/Report/ReportViewModel.swift new file mode 100644 index 000000000..73bac19b0 --- /dev/null +++ b/Mastodon/Scene/Report/Report/ReportViewModel.swift @@ -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() + + let reportReasonViewModel: ReportReasonViewModel + let reportServerRulesViewModel: ReportServerRulesViewModel + let reportStatusViewModel: ReportStatusViewModel + let reportSupplementaryViewModel: ReportSupplementaryViewModel + + // input + let context: AppContext + let user: ManagedObjectRecord + let status: ManagedObjectRecord? + + // output + @Published var isReporting = false + @Published var isReportSuccess = false + + init( + context: AppContext, + user: ManagedObjectRecord, + status: ManagedObjectRecord? + ) { + 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 = "What’s 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 = "What’s wrong with @\(username)?" + } else { + reportReasonViewModel.headline = "What’s 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 + } + } +} diff --git a/Mastodon/Scene/Report/ReportReason/ReportReasonView.swift b/Mastodon/Scene/Report/ReportReason/ReportReasonView.swift new file mode 100644 index 000000000..3a8b4579f --- /dev/null +++ b/Mastodon/Scene/Report/ReportReason/ReportReasonView.swift @@ -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 diff --git a/Mastodon/Scene/Report/ReportReason/ReportReasonViewController.swift b/Mastodon/Scene/Report/ReportReason/ReportReasonViewController.swift new file mode 100644 index 000000000..b0651e42d --- /dev/null +++ b/Mastodon/Scene/Report/ReportReason/ReportReasonViewController.swift @@ -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() + private var observations = Set() + + 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) + } + +} diff --git a/Mastodon/Scene/Report/ReportReason/ReportReasonViewModel.swift b/Mastodon/Scene/Report/ReportReason/ReportReasonViewModel.swift new file mode 100644 index 000000000..b156ea311 --- /dev/null +++ b/Mastodon/Scene/Report/ReportReason/ReportReasonViewModel.swift @@ -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 don’t like it" + case .spam: + return "It’s spam" + case .violateRule: + return "It violates server rules" + case .other: + return "It’s 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 don’t like it" + case .spam: + return "It’s spam" + case .violateRule: + return "It violates server rules" + case .other: + return "It’s something else" + } + } + } +} diff --git a/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift b/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift index 26f56b98d..1073c21a2 100644 --- a/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift +++ b/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift @@ -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 } +} diff --git a/Mastodon/Scene/Report/ReportServerRules/ReportServerRulesView.swift b/Mastodon/Scene/Report/ReportServerRules/ReportServerRulesView.swift new file mode 100644 index 000000000..e8dcfa886 --- /dev/null +++ b/Mastodon/Scene/Report/ReportServerRules/ReportServerRulesView.swift @@ -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 don’t 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 diff --git a/Mastodon/Scene/Report/ReportServerRules/ReportServerRulesViewController.swift b/Mastodon/Scene/Report/ReportServerRules/ReportServerRulesViewController.swift new file mode 100644 index 000000000..330debbb8 --- /dev/null +++ b/Mastodon/Scene/Report/ReportServerRules/ReportServerRulesViewController.swift @@ -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() + private var observations = Set() + + 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) + } + +} diff --git a/Mastodon/Scene/Report/ReportServerRules/ReportServerRulesViewModel.swift b/Mastodon/Scene/Report/ReportServerRules/ReportServerRulesViewModel.swift new file mode 100644 index 000000000..1960899a9 --- /dev/null +++ b/Mastodon/Scene/Report/ReportServerRules/ReportServerRulesViewModel.swift @@ -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 + } + +} diff --git a/Mastodon/Scene/Report/ReportStatus/ReportViewController.swift b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewController.swift similarity index 75% rename from Mastodon/Scene/Report/ReportStatus/ReportViewController.swift rename to Mastodon/Scene/Report/ReportStatus/ReportStatusViewController.swift index 12291a964..d3844a3be 100644 --- a/Mastodon/Scene/Report/ReportStatus/ReportViewController.swift +++ b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewController.swift @@ -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() private var observations = Set() 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] = [] - 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 } diff --git a/Mastodon/Scene/Report/ReportStatus/ReportViewModel+Diffable.swift b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+Diffable.swift similarity index 93% rename from Mastodon/Scene/Report/ReportStatus/ReportViewModel+Diffable.swift rename to Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+Diffable.swift index 30ec5d872..5bcc2f3d6 100644 --- a/Mastodon/Scene/Report/ReportStatus/ReportViewModel+Diffable.swift +++ b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+Diffable.swift @@ -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() 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) diff --git a/Mastodon/Scene/Report/ReportStatus/ReportViewModel+State.swift b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+State.swift similarity index 92% rename from Mastodon/Scene/Report/ReportStatus/ReportViewModel+State.swift rename to Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+State.swift index 1bc43830f..c653fc4ad 100644 --- a/Mastodon/Scene/Report/ReportStatus/ReportViewModel+State.swift +++ b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+State.swift @@ -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 ?? "")") } @@ -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 } diff --git a/Mastodon/Scene/Report/ReportStatus/ReportViewModel.swift b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift similarity index 91% rename from Mastodon/Scene/Report/ReportStatus/ReportViewModel.swift rename to Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift index 46a475262..239960637 100644 --- a/Mastodon/Scene/Report/ReportStatus/ReportViewModel.swift +++ b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift @@ -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() + weak var delegate: ReportStatusViewControllerDelegate? + // input let context: AppContext let user: ManagedObjectRecord @@ -26,6 +28,7 @@ class ReportViewModel { let statusFetchedResultsController: StatusFetchedResultsController let listBatchFetchViewModel = ListBatchFetchViewModel() + @Published var isSkip = false @Published var selectStatuses = OrderedSet>() // output diff --git a/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewController.swift b/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewController.swift index 4f6e102b2..10191f6ed 100644 --- a/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewController.swift +++ b/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewController.swift @@ -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() private var observations = Set() - + 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) } } diff --git a/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel+Diffable.swift b/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel+Diffable.swift index 5fb9e7421..e59617c35 100644 --- a/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel+Diffable.swift +++ b/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel+Diffable.swift @@ -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( diff --git a/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel.swift b/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel.swift index e73e82dd6..4147d07d7 100644 --- a/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel.swift +++ b/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel.swift @@ -12,26 +12,26 @@ import MastodonSDK class ReportSupplementaryViewModel { + weak var delegate: ReportSupplementaryViewControllerDelegate? + // Input var context: AppContext let user: ManagedObjectRecord - let selectStatuses: [ManagedObjectRecord] let commentContext = ReportItem.CommentContext() + @Published var isSkip = false + @Published var isBusy = false + // output var diffableDataSource: UITableViewDiffableDataSource? @Published var isNextButtonEnabled = false - @Published var isReporting = false - @Published var isReportSuccess = false init( context: AppContext, - user: ManagedObjectRecord, - selectStatuses: [ManagedObjectRecord] + user: ManagedObjectRecord ) { 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 - } - } -} diff --git a/Mastodon/Scene/Report/Share/Cell/ReportCommentTableViewCell.swift b/Mastodon/Scene/Report/Share/Cell/ReportCommentTableViewCell.swift index b982ee5ac..d735a094c 100644 --- a/Mastodon/Scene/Report/Share/Cell/ReportCommentTableViewCell.swift +++ b/Mastodon/Scene/Report/Share/Cell/ReportCommentTableViewCell.swift @@ -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 + ] + ) + } + } diff --git a/Mastodon/Scene/Report/Share/ReportViewControllerAppearance.swift b/Mastodon/Scene/Report/Share/ReportViewControllerAppearance.swift index 6b35f3d89..fb9bcd63f 100644 --- a/Mastodon/Scene/Report/Share/ReportViewControllerAppearance.swift +++ b/Mastodon/Scene/Report/Share/ReportViewControllerAppearance.swift @@ -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() diff --git a/Mastodon/Scene/Share/NavigationController/AdaptiveStatusBarStyleNavigationController.swift b/Mastodon/Scene/Share/NavigationController/AdaptiveStatusBarStyleNavigationController.swift index f4ad467da..ec145f86d 100644 --- a/Mastodon/Scene/Share/NavigationController/AdaptiveStatusBarStyleNavigationController.swift +++ b/Mastodon/Scene/Share/NavigationController/AdaptiveStatusBarStyleNavigationController.swift @@ -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 } +}