From e3df692c3f86c757cd16470e5de100a2f8c8ae8a Mon Sep 17 00:00:00 2001 From: ihugo Date: Mon, 19 Apr 2021 20:34:08 +0800 Subject: [PATCH 01/18] feat: report --- CoreDataStack/Entity/Status.swift | 4 + Localization/app.json | 17 +- Mastodon.xcodeproj/project.pbxproj | 44 +++ Mastodon/Coordinator/SceneCoordinator.swift | 25 +- .../Diffiable/Section/ReportSection.swift | 60 ++++ Mastodon/Generated/Assets.swift | 2 + Mastodon/Generated/Strings.swift | 22 ++ .../elevatedPrimary.colorset/Contents.json | 38 +++ .../secondary.colorset/Contents.json | 38 +++ .../Resources/en.lproj/Localizable.strings | 11 +- ...meTimelineViewController+DebugAction.swift | 34 +++ Mastodon/Scene/Report/ReportView.swift | 201 +++++++++++++ .../Scene/Report/ReportViewController.swift | 267 ++++++++++++++++++ .../Scene/Report/ReportViewModel+Data.swift | 98 +++++++ .../Report/ReportViewModel+Diffable.swift | 37 +++ .../Report/ReportViewModel+Provider.swift | 86 ++++++ Mastodon/Scene/Report/ReportViewModel.swift | 249 ++++++++++++++++ .../Report/ReportedStatusTableviewCell.swift | 176 ++++++++++++ .../Settings/SettingsViewController.swift | 2 +- .../APIService/APIService+Report.swift | 23 ++ .../MastodonSDK/API/Mastodon+API+Report.swift | 79 ++++++ .../MastodonSDK/API/Mastodon+API.swift | 1 + 22 files changed, 1504 insertions(+), 10 deletions(-) create mode 100644 Mastodon/Diffiable/Section/ReportSection.swift create mode 100644 Mastodon/Resources/Assets.xcassets/Colors/Background/elevatedPrimary.colorset/Contents.json create mode 100644 Mastodon/Resources/Assets.xcassets/Colors/Background/secondary.colorset/Contents.json create mode 100644 Mastodon/Scene/Report/ReportView.swift create mode 100644 Mastodon/Scene/Report/ReportViewController.swift create mode 100644 Mastodon/Scene/Report/ReportViewModel+Data.swift create mode 100644 Mastodon/Scene/Report/ReportViewModel+Diffable.swift create mode 100644 Mastodon/Scene/Report/ReportViewModel+Provider.swift create mode 100644 Mastodon/Scene/Report/ReportViewModel.swift create mode 100644 Mastodon/Scene/Report/ReportedStatusTableviewCell.swift create mode 100644 Mastodon/Service/APIService/APIService+Report.swift create mode 100644 MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Report.swift diff --git a/CoreDataStack/Entity/Status.swift b/CoreDataStack/Entity/Status.swift index f40f78639..81ace7a5d 100644 --- a/CoreDataStack/Entity/Status.swift +++ b/CoreDataStack/Entity/Status.swift @@ -337,4 +337,8 @@ extension Status { public static func deleted() -> NSPredicate { return NSPredicate(format: "%K != nil", #keyPath(Status.deletedAt)) } + + public static func author(author: MastodonUser) -> NSPredicate { + return NSPredicate(format: "%K == %@", #keyPath(Status.author), author) + } } diff --git a/Localization/app.json b/Localization/app.json index 0aa622718..18d7a193e 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -51,7 +51,8 @@ "preview": "Preview", "share": "Share", "share_user": "Share %s", - "open_in_safari": "Open in Safari" + "open_in_safari": "Open in Safari", + "skip": "Skip" }, "status": { "user_reblogged": "%s reblogged", @@ -329,7 +330,7 @@ }, "favorite": { "title": "Your Favorites" - }, + }, "notification": { "title": { "Everything": "Everything", @@ -341,6 +342,7 @@ "reblog": "rebloged your post", "poll": "Your poll has ended", "mention": "mentioned you" + } }, "thread": { "back_title": "Post", @@ -388,6 +390,17 @@ "signout": "Sign Out" } } + }, + "report": { + "title": "Report %s", + "step1": "Step 1 of 2", + "step2": "Step 2 of 2", + "content1": "Are there any other posts you’d like to add to the report?", + "content2": "Is there anything the moderators should know about this report?", + "send": "Send Report", + "skipToSend": "Send without comment", + "textPlaceHolder": "|Type or paste additional comments" } } } + diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 7b147991f..67bd4878a 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -133,6 +133,10 @@ 2DF75BC725D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BC625D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift */; }; 2DFAD5272616F9D300F9EE7C /* SearchViewController+Searching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFAD5262616F9D300F9EE7C /* SearchViewController+Searching.swift */; }; 2DFAD5372617010500F9EE7C /* SearchingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFAD5362617010500F9EE7C /* SearchingTableViewCell.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 */; }; + 5B24BBDC262DB14800A9381B /* ReportViewModel+Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B24BBD9262DB14800A9381B /* ReportViewModel+Provider.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 */; }; 5B90C460262599800002E742 /* SettingsAppearanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C45A262599800002E742 /* SettingsAppearanceTableViewCell.swift */; }; @@ -144,6 +148,11 @@ 5B90C47F26259BA90002E742 /* SubscriptionAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C47E26259BA90002E742 /* SubscriptionAlerts.swift */; }; 5B90C48526259BF10002E742 /* APIService+Subscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C48426259BF10002E742 /* APIService+Subscriptions.swift */; }; 5B90C48B26259C120002E742 /* APIService+CoreData+Subscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C48A26259C120002E742 /* APIService+CoreData+Subscriptions.swift */; }; + 5BB04FD5262E7AFF0043BFF6 /* ReportViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB04FD4262E7AFF0043BFF6 /* ReportViewController.swift */; }; + 5BB04FDB262EA3070043BFF6 /* ReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB04FDA262EA3070043BFF6 /* ReportView.swift */; }; + 5BB04FE9262EFC300043BFF6 /* ReportedStatusTableviewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB04FE8262EFC300043BFF6 /* ReportedStatusTableviewCell.swift */; }; + 5BB04FEF262F0DCB0043BFF6 /* ReportViewModel+Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB04FEE262F0DCB0043BFF6 /* ReportViewModel+Data.swift */; }; + 5BB04FF5262F0E6D0043BFF6 /* ReportSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */; }; 5D0393902612D259007FE196 /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D03938F2612D259007FE196 /* WebViewController.swift */; }; 5D0393962612D266007FE196 /* WebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D0393952612D266007FE196 /* WebViewModel.swift */; }; 5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* MastodonSDK */; }; @@ -547,6 +556,10 @@ 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = ""; }; + 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 = ""; }; + 5B24BBD9262DB14800A9381B /* ReportViewModel+Provider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ReportViewModel+Provider.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 = ""; }; 5B90C45A262599800002E742 /* SettingsAppearanceTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsAppearanceTableViewCell.swift; sourceTree = ""; }; @@ -558,6 +571,11 @@ 5B90C47E26259BA90002E742 /* SubscriptionAlerts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionAlerts.swift; sourceTree = ""; }; 5B90C48426259BF10002E742 /* APIService+Subscriptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+Subscriptions.swift"; sourceTree = ""; }; 5B90C48A26259C120002E742 /* APIService+CoreData+Subscriptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Subscriptions.swift"; sourceTree = ""; }; + 5BB04FD4262E7AFF0043BFF6 /* ReportViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportViewController.swift; sourceTree = ""; }; + 5BB04FDA262EA3070043BFF6 /* ReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportView.swift; sourceTree = ""; }; + 5BB04FE8262EFC300043BFF6 /* ReportedStatusTableviewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportedStatusTableviewCell.swift; sourceTree = ""; }; + 5BB04FEE262F0DCB0043BFF6 /* ReportViewModel+Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReportViewModel+Data.swift"; sourceTree = ""; }; + 5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportSection.swift; sourceTree = ""; }; 5D03938F2612D259007FE196 /* WebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = ""; }; 5D0393952612D266007FE196 /* WebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewModel.swift; sourceTree = ""; }; 5DA732CB2629CEF500A92342 /* UIView+Remove.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Remove.swift"; sourceTree = ""; }; @@ -1114,6 +1132,7 @@ 2D198648261C0B8500F0B013 /* SearchResultSection.swift */, DB66729525F9F91600D60309 /* ComposeStatusSection.swift */, DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */, + 5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */, ); path = Section; sourceTree = ""; @@ -1217,6 +1236,20 @@ name = Frameworks; sourceTree = ""; }; + 5B24BBD6262DB14800A9381B /* Report */ = { + isa = PBXGroup; + children = ( + 5B24BBD7262DB14800A9381B /* ReportViewModel.swift */, + 5B24BBD8262DB14800A9381B /* ReportViewModel+Diffable.swift */, + 5B24BBD9262DB14800A9381B /* ReportViewModel+Provider.swift */, + 5BB04FEE262F0DCB0043BFF6 /* ReportViewModel+Data.swift */, + 5BB04FD4262E7AFF0043BFF6 /* ReportViewController.swift */, + 5BB04FDA262EA3070043BFF6 /* ReportView.swift */, + 5BB04FE8262EFC300043BFF6 /* ReportedStatusTableviewCell.swift */, + ); + path = Report; + sourceTree = ""; + }; 5B90C455262599800002E742 /* Settings */ = { isa = PBXGroup; children = ( @@ -1429,6 +1462,7 @@ 2D34D9DA261494120081BFC0 /* APIService+Search.swift */, 0F202212261351F5000C64BF /* APIService+HashtagTimeline.swift */, DBCC3B9426157E6E0045B23D /* APIService+Relationship.swift */, + 5B24BBE1262DB19100A9381B /* APIService+Report.swift */, DBAE3F932616E28B004B8251 /* APIService+Follow.swift */, DBAE3F8D2616E0B1004B8251 /* APIService+Block.swift */, DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */, @@ -1668,6 +1702,7 @@ DB01409B25C40BB600F9F3CF /* Onboarding */, 2D38F1D325CD463600561493 /* HomeTimeline */, 2D76316325C14BAC00929FB9 /* PublicTimeline */, + 5B24BBD6262DB14800A9381B /* Report */, 0F2021F5261325ED000C64BF /* HashtagTimeline */, DB9D6BEE25E4F5370051B173 /* Search */, 5B90C455262599800002E742 /* Settings */, @@ -2327,10 +2362,12 @@ 2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */, 2DFAD5372617010500F9EE7C /* SearchingTableViewCell.swift in Sources */, DB447681260B3ED600B66B82 /* CustomEmojiPickerSection.swift in Sources */, + 5BB04FEF262F0DCB0043BFF6 /* ReportViewModel+Data.swift in Sources */, DBB525502611ED6D002F1F29 /* ProfileHeaderView.swift in Sources */, 0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */, DB71FD5225F8CCAA00512AE1 /* APIService+Status.swift in Sources */, 5D0393962612D266007FE196 /* WebViewModel.swift in Sources */, + 5B24BBDA262DB14800A9381B /* ReportViewModel.swift in Sources */, 2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */, DB1E347825F519300079D7DF /* PickServerItem.swift in Sources */, DB1FD45A25F27898004CFCFC /* CategoryPickerItem.swift in Sources */, @@ -2439,6 +2476,7 @@ 2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */, DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */, 2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */, + 5B24BBDB262DB14800A9381B /* ReportViewModel+Diffable.swift in Sources */, DB71FD3C25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift in Sources */, 2DA6055125F74407006356F9 /* AudioContainerViewModel.swift in Sources */, 0FB3D2FE25E4CB6400AAD544 /* PickServerTitleCell.swift in Sources */, @@ -2490,6 +2528,7 @@ DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */, 2D61254D262547C200299647 /* APIService+Notification.swift in Sources */, DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */, + 5BB04FE9262EFC300043BFF6 /* ReportedStatusTableviewCell.swift in Sources */, DBAE3F822615DDA3004B8251 /* ProfileViewController+UserProvider.swift in Sources */, DB938EED2623F79B00E5B6C1 /* ThreadViewModel.swift in Sources */, DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */, @@ -2497,6 +2536,7 @@ DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */, DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */, 0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */, + 5BB04FDB262EA3070043BFF6 /* ReportView.swift in Sources */, DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */, DB68A04A25E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift in Sources */, 0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */, @@ -2519,6 +2559,7 @@ 2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */, DB35FC252612FD7A006193C9 /* ProfileFieldView.swift in Sources */, DB938F0326240EA300E5B6C1 /* CachedThreadViewModel.swift in Sources */, + 5B24BBDC262DB14800A9381B /* ReportViewModel+Provider.swift in Sources */, 2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */, 2D24E12D2626FD2E00A59D4F /* NotificationViewModel+LoadOldestState.swift in Sources */, 2DB72C8C262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift in Sources */, @@ -2594,6 +2635,8 @@ DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */, DB938F3326243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift in Sources */, 2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */, + 5B24BBE2262DB19100A9381B /* APIService+Report.swift in Sources */, + 5BB04FF5262F0E6D0043BFF6 /* ReportSection.swift in Sources */, DB938F25262438D600E5B6C1 /* ThreadViewController+StatusProvider.swift in Sources */, DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */, 0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */, @@ -2604,6 +2647,7 @@ DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */, DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift in Sources */, DBE3CE13261D7D4200430CC6 /* StatusTableViewControllerAspect.swift in Sources */, + 5BB04FD5262E7AFF0043BFF6 /* ReportViewController.swift in Sources */, DBAE3F942616E28B004B8251 /* APIService+Follow.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index c2608fe83..93393d542 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -65,10 +65,10 @@ extension SceneCoordinator { case safari(url: URL) case alertController(alertController: UIAlertController) case activityViewController(activityViewController: UIActivityViewController, sourceView: UIView?, barButtonItem: UIBarButtonItem?) - + case settings + case report(userId: String, statusId: String?) #if DEBUG case publicTimeline - case settings #endif var isOnboarding: Bool { @@ -265,15 +265,28 @@ private extension SceneCoordinator { activityViewController.popoverPresentationController?.sourceView = sourceView activityViewController.popoverPresentationController?.barButtonItem = barButtonItem viewController = activityViewController + case .settings: + let _viewController = SettingsViewController() + _viewController.viewModel = SettingsViewModel(context: appContext, coordinator: self) + viewController = _viewController + case .report(let userId, let statusId): + guard let authenticationBox = appContext.authenticationService.activeMastodonAuthenticationBox.value else { + return nil + } + let _viewController = ReportViewController() + _viewController.viewModel = ReportViewModel( + context: appContext, + coordinator: self, + domain: authenticationBox.domain, + userId: userId, + statusId: statusId + ) + viewController = _viewController #if DEBUG case .publicTimeline: let _viewController = PublicTimelineViewController() _viewController.viewModel = PublicTimelineViewModel(context: appContext) viewController = _viewController - case .settings: - let _viewController = SettingsViewController() - _viewController.viewModel = SettingsViewModel(context: appContext, coordinator: self) - viewController = _viewController #endif } diff --git a/Mastodon/Diffiable/Section/ReportSection.swift b/Mastodon/Diffiable/Section/ReportSection.swift new file mode 100644 index 000000000..7567488c5 --- /dev/null +++ b/Mastodon/Diffiable/Section/ReportSection.swift @@ -0,0 +1,60 @@ +// +// ReportSection.swift +// Mastodon +// +// Created by ihugo on 2021/4/20. +// + +import Combine +import CoreData +import CoreDataStack +import Foundation +import MastodonSDK +import UIKit +import AVKit +import os.log + +enum ReportSection: Equatable, Hashable { + case main +} + +extension ReportSection { + static func tableViewDiffableDataSource( + for tableView: UITableView, + dependency: NeedsDependency, + managedObjectContext: NSManagedObjectContext, + timestampUpdatePublisher: AnyPublisher, + reportdStatusDelegate: ReportedStatusTableViewCellDelegate + ) -> UITableViewDiffableDataSource { + UITableViewDiffableDataSource(tableView: tableView) {[ + weak dependency + ] tableView, indexPath, item -> UITableViewCell? in + guard let dependency = dependency else { return UITableViewCell() } + + switch item { + case .status(let objectID, let attribute): + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ReportedStatusTableViewCell.self), for: indexPath) as! ReportedStatusTableViewCell + let activeMastodonAuthenticationBox = dependency.context.authenticationService.activeMastodonAuthenticationBox.value + let requestUserID = activeMastodonAuthenticationBox?.userID ?? "" + managedObjectContext.performAndWait { + let status = managedObjectContext.object(with: objectID) as! Status + StatusSection.configure( + cell: cell, + dependency: dependency, + readableLayoutFrame: tableView.readableContentGuide.layoutFrame, + timestampUpdatePublisher: timestampUpdatePublisher, + status: status, + requestUserID: requestUserID, + statusItemAttribute: attribute + ) + } + + let isSelected = reportdStatusDelegate.reportedStatus(cell: cell, isSelected: indexPath) + cell.setupSelected(isSelected) + return cell + default: + return nil + } + } + } +} diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift index ce9e33e2b..aba5e4f6b 100644 --- a/Mastodon/Generated/Assets.swift +++ b/Mastodon/Generated/Assets.swift @@ -41,9 +41,11 @@ internal enum Asset { internal static let alertYellow = ColorAsset(name: "Colors/Background/alert.yellow") internal static let dangerBorder = ColorAsset(name: "Colors/Background/danger.border") internal static let danger = ColorAsset(name: "Colors/Background/danger") + internal static let elevatedPrimary = ColorAsset(name: "Colors/Background/elevatedPrimary") internal static let mediaTypeIndicotor = ColorAsset(name: "Colors/Background/media.type.indicotor") internal static let navigationBar = ColorAsset(name: "Colors/Background/navigationBar") internal static let onboardingBackground = ColorAsset(name: "Colors/Background/onboarding.background") + internal static let secondary = ColorAsset(name: "Colors/Background/secondary") internal static let secondaryGroupedSystemBackground = ColorAsset(name: "Colors/Background/secondary.grouped.system.background") internal static let secondarySystemBackground = ColorAsset(name: "Colors/Background/secondary.system.background") internal static let systemBackground = ColorAsset(name: "Colors/Background/system.background") diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index b7bd3d0a8..1fb5c5f59 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -96,6 +96,8 @@ internal enum L10n { internal static let signIn = L10n.tr("Localizable", "Common.Controls.Actions.SignIn") /// Sign Up internal static let signUp = L10n.tr("Localizable", "Common.Controls.Actions.SignUp") + /// Skip + internal static let skip = L10n.tr("Localizable", "Common.Controls.Actions.Skip") /// Take photo internal static let takePhoto = L10n.tr("Localizable", "Common.Controls.Actions.TakePhoto") /// Try Again @@ -523,6 +525,26 @@ internal enum L10n { } } } + internal enum Report { + /// Are there any other posts you’d like to add to the report? + internal static let content1 = L10n.tr("Localizable", "Scene.Report.Content1") + /// Is there anything the moderators should know about this report? + internal static let content2 = L10n.tr("Localizable", "Scene.Report.Content2") + /// Send Report + internal static let send = L10n.tr("Localizable", "Scene.Report.Send") + /// Send without comment + internal static let skiptosend = L10n.tr("Localizable", "Scene.Report.Skiptosend") + /// Step 1 of 2 + internal static let step1 = L10n.tr("Localizable", "Scene.Report.Step1") + /// Step 2 of 2 + internal static let step2 = L10n.tr("Localizable", "Scene.Report.Step2") + /// |Type or paste additional comments + internal static let textplaceholder = L10n.tr("Localizable", "Scene.Report.Textplaceholder") + /// Report %@ + internal static func title(_ p1: Any) -> String { + return L10n.tr("Localizable", "Scene.Report.Title", String(describing: p1)) + } + } internal enum Search { internal enum Recommend { /// See All diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/elevatedPrimary.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/elevatedPrimary.colorset/Contents.json new file mode 100644 index 000000000..82edd034b --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/Background/elevatedPrimary.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "30", + "green" : "28", + "red" : "28" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/secondary.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/secondary.colorset/Contents.json new file mode 100644 index 000000000..5e7067405 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/Background/secondary.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "254", + "green" : "255", + "red" : "254" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "46", + "green" : "44", + "red" : "44" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 253b65d95..b413d887c 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -31,6 +31,7 @@ Please check your internet connection."; "Common.Controls.Actions.ShareUser" = "Share %@"; "Common.Controls.Actions.SignIn" = "Sign In"; "Common.Controls.Actions.SignUp" = "Sign Up"; +"Common.Controls.Actions.Skip" = "Skip"; "Common.Controls.Actions.TakePhoto" = "Take photo"; "Common.Controls.Actions.TryAgain" = "Try Again"; "Common.Controls.Firendship.Block" = "Block"; @@ -168,6 +169,14 @@ tap the link to confirm your account."; "Scene.Register.Input.Username.DuplicatePrompt" = "This username is taken."; "Scene.Register.Input.Username.Placeholder" = "username"; "Scene.Register.Title" = "Tell us about you."; +"Scene.Report.Content1" = "Are there any other posts you’d like to add to the report?"; +"Scene.Report.Content2" = "Is there anything the moderators should know about this report?"; +"Scene.Report.Send" = "Send Report"; +"Scene.Report.Skiptosend" = "Send without comment"; +"Scene.Report.Step1" = "Step 1 of 2"; +"Scene.Report.Step2" = "Step 2 of 2"; +"Scene.Report.Textplaceholder" = "|Type or paste additional comments"; +"Scene.Report.Title" = "Report %@"; "Scene.Search.Recommend.Accounts.Description" = "Except for Sam, you will not like his account."; "Scene.Search.Recommend.Accounts.Follow" = "Follow"; "Scene.Search.Recommend.Accounts.Title" = "Accounts you might like"; @@ -227,4 +236,4 @@ any server."; "Scene.Thread.Reblog.Single" = "%@ reblog"; "Scene.Thread.Title" = "Post from %@"; "Scene.Welcome.Slogan" = "Social networking -back in your hands."; +back in your hands."; \ No newline at end of file diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift index 8bbf9436e..459652008 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift @@ -41,6 +41,10 @@ extension HomeTimelineViewController { guard let self = self else { return } self.showSettings(action) }, + UIAction(title: "Report", image: UIImage(systemName: "exclamationmark.bubble"), attributes: []) { [weak self] action in + guard let self = self else { return } + self.showReportAction(action) + }, UIAction(title: "Sign Out", image: UIImage(systemName: "escape"), attributes: .destructive) { [weak self] action in guard let self = self else { return } self.signOutAction(action) @@ -330,5 +334,35 @@ extension HomeTimelineViewController { @objc private func showSettings(_ sender: UIAction) { coordinator.present(scene: .settings, from: self, transition: .modal(animated: true, completion: nil)) } + + @objc private func showReportAction(_ sender: UIAction) { + let alertController = UIAlertController(title: "Enter User ID", message: nil, preferredStyle: .alert) + alertController.addTextField() + alertController.addTextField() + guard let accountTextField = alertController.textFields?.first else { return } + guard let statusTextField = alertController.textFields?.last else { return } + accountTextField.placeholder = "User ID" + statusTextField.placeholder = "Status ID" + accountTextField.text = "212477" + statusTextField.text = "106103767536113615" + let showAction = UIAlertAction(title: "Show", style: .default) { [weak self] _ in + guard let self = self else { return } + + guard let userId = accountTextField.text else { return } + guard let statusId = statusTextField.text else { return } + + // itodo: delete them + // 31803 + // 106093402888557459 + self.coordinator.present( + scene: .report(userId: userId, statusId: statusId), + from: self, transition: .modal(animated: true, completion: nil)) + } + alertController.addAction(showAction) + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) + alertController.addAction(cancelAction) + coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil)) + } + } #endif diff --git a/Mastodon/Scene/Report/ReportView.swift b/Mastodon/Scene/Report/ReportView.swift new file mode 100644 index 000000000..9166259fe --- /dev/null +++ b/Mastodon/Scene/Report/ReportView.swift @@ -0,0 +1,201 @@ +// +// ReportView.swift +// Mastodon +// +// Created by ihugo on 2021/4/20. +// + +import UIKit + +struct ReportView { + static var horizontalMargin: CGFloat { return 12 } + static var verticalMargin: CGFloat { return 22 } + static var buttonHeight: CGFloat { return 46 } + static var skipBottomMargin: CGFloat { return 8 } + static var continuTopMargin: CGFloat { return 22 } +} + +final class ReportViewHeader: UIView { + enum Step: Int { + case one + case two + } + + lazy var titleLabel: UILabel = { + let label = UILabel() + label.textColor = Asset.Colors.Label.secondary.color + label.font = UIFont.preferredFont(forTextStyle: .subheadline) + label.numberOfLines = 0 + return label + }() + + lazy var contentLabel: UILabel = { + let label = UILabel() + label.textColor = Asset.Colors.Label.primary.color + label.font = UIFont.preferredFont(forTextStyle: .title3) + label.numberOfLines = 0 + return label + }() + + lazy var stackview: UIStackView = { + let view = UIStackView() + view.axis = .vertical + view.alignment = .leading + view.spacing = 2 + return view + }() + + var step: Step = .one { + didSet { + switch step { + case .one: + titleLabel.text = L10n.Scene.Report.step1 + contentLabel.text = L10n.Scene.Report.content1 + case .two: + titleLabel.text = L10n.Scene.Report.step2 + contentLabel.text = L10n.Scene.Report.content2 + } + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + + self.backgroundColor = Asset.Colors.Background.elevatedPrimary.color + stackview.addArrangedSubview(titleLabel) + stackview.addArrangedSubview(contentLabel) + addSubview(stackview) + + stackview.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + stackview.safeAreaLayoutGuide.topAnchor.constraint( + equalTo: self.topAnchor, + constant: ReportView.verticalMargin + ), + stackview.leadingAnchor.constraint( + equalTo: self.readableContentGuide.leadingAnchor, + constant: ReportView.horizontalMargin + ), + stackview.bottomAnchor.constraint( + equalTo: self.bottomAnchor, + constant: -1 * ReportView.verticalMargin + ), + stackview.trailingAnchor.constraint( + equalTo: self.readableContentGuide.trailingAnchor, + constant: -1 * ReportView.horizontalMargin + ) + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +final class ReportViewFooter: UIView { + enum Step: Int { + case one + case two + } + + lazy var stackview: UIStackView = { + let view = UIStackView() + view.axis = .vertical + view.alignment = .fill + view.spacing = 8 + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + lazy var nextStepButton: PrimaryActionButton = { + let button = PrimaryActionButton() + button.setTitle(L10n.Common.Controls.Actions.continue, for: .normal) + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + lazy var skipButton: UIButton = { + let button = UIButton(type: .system) + button.tintColor = Asset.Colors.brandBlue.color + button.setTitle(L10n.Common.Controls.Actions.skip, for: .normal) + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + var step: Step = .one { + didSet { + switch step { + case .one: + nextStepButton.setTitle(L10n.Common.Controls.Actions.continue, for: .normal) + skipButton.setTitle(L10n.Common.Controls.Actions.skip, for: .normal) + case .two: + nextStepButton.setTitle(L10n.Scene.Report.send, for: .normal) + skipButton.setTitle(L10n.Scene.Report.skiptosend, for: .normal) + } + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + + self.backgroundColor = Asset.Colors.Background.elevatedPrimary.color + + stackview.addArrangedSubview(nextStepButton) + stackview.addArrangedSubview(skipButton) + addSubview(stackview) + + NSLayoutConstraint.activate([ + stackview.topAnchor.constraint( + equalTo: self.topAnchor, + constant: ReportView.continuTopMargin + ), + stackview.leadingAnchor.constraint( + equalTo: self.readableContentGuide.leadingAnchor, + constant: ReportView.horizontalMargin + ), + stackview.bottomAnchor.constraint( + equalTo: self.safeAreaLayoutGuide.bottomAnchor, + constant: -1 * ReportView.skipBottomMargin + ), + stackview.trailingAnchor.constraint( + equalTo: self.readableContentGuide.trailingAnchor, + constant: -1 * ReportView.horizontalMargin + ), + nextStepButton.heightAnchor.constraint( + equalToConstant: ReportView.buttonHeight + ), + skipButton.heightAnchor.constraint( + equalTo: nextStepButton.heightAnchor + ) + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +#if canImport(SwiftUI) && DEBUG +import SwiftUI + +struct ReportView_Previews: PreviewProvider { + static var previews: some View { + Group { + UIViewPreview { () -> UIView in + let view = ReportViewHeader() + view.step = .one + view.contentLabel.preferredMaxLayoutWidth = 335 + return view + } + .previewLayout(.fixed(width: 375, height: 110)) + + UIViewPreview(width: 375) { () -> UIView in + return ReportViewFooter(frame: CGRect(origin: .zero, size: CGSize(width: 375, height: 164))) + } + .previewLayout(.fixed(width: 375, height: 164)) + } + } + +} + +#endif diff --git a/Mastodon/Scene/Report/ReportViewController.swift b/Mastodon/Scene/Report/ReportViewController.swift new file mode 100644 index 000000000..7d46ae6b9 --- /dev/null +++ b/Mastodon/Scene/Report/ReportViewController.swift @@ -0,0 +1,267 @@ +// +// ReportViewController.swift +// Mastodon +// +// Created by ihugo on 2021/4/20. +// + +import AVKit +import Combine +import CoreData +import CoreDataStack +import os.log +import UIKit +import TwitterTextEditor + +class ReportViewController: UIViewController, NeedsDependency { + static let kAnimationDuration: TimeInterval = 0.33 + + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } + weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } + + var viewModel: ReportViewModel! { willSet { precondition(!isViewLoaded) } } + var disposeBag = Set() + let didToggleSelected = PassthroughSubject() + let comment = CurrentValueSubject(nil) + let step1Continue = PassthroughSubject() + let step1Skip = PassthroughSubject() + let step2Continue = PassthroughSubject() + let step2Skip = PassthroughSubject() + let cancel = PassthroughSubject() + + // MAKK: - UI + lazy var header: ReportViewHeader = { + let view = ReportViewHeader() + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + lazy var footer: ReportViewFooter = { + let view = ReportViewFooter() + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + lazy var contentView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.setContentHuggingPriority(.defaultLow, for: .vertical) + view.backgroundColor = Asset.Colors.Background.elevatedPrimary.color + return view + }() + + lazy var stackview: UIStackView = { + let view = UIStackView() + view.axis = .vertical + view.alignment = .fill + view.distribution = .fill + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + lazy var tableView: UITableView = { + let tableView = ControlContainableTableView() + tableView.register(ReportedStatusTableViewCell.self, forCellReuseIdentifier: String(describing: ReportedStatusTableViewCell.self)) + tableView.rowHeight = UITableView.automaticDimension + tableView.separatorStyle = .none + tableView.backgroundColor = .clear + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.delegate = self + return tableView + }() + + lazy var textView: UITextView = { + let textView = UITextView() + textView.font = .preferredFont(forTextStyle: .body) + textView.isScrollEnabled = false + textView.placeholder = L10n.Scene.Report.textplaceholder + textView.backgroundColor = .clear + textView.delegate = self + return textView + }() + + override func viewDidLoad() { + super.viewDidLoad() + + setupView() + bindViewModel() + bindActions() + } + + // MAKR: - Private methods + private func setupView() { + view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color + setupNavigation() + + stackview.addArrangedSubview(header) + stackview.addArrangedSubview(contentView) + stackview.addArrangedSubview(footer) + + contentView.addSubview(tableView) + + view.addSubview(stackview) + NSLayoutConstraint.activate([ + stackview.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + stackview.leadingAnchor.constraint(equalTo: view.leadingAnchor), + stackview.bottomAnchor.constraint(equalTo: view.bottomAnchor), + stackview.trailingAnchor.constraint(equalTo: view.trailingAnchor), + tableView.topAnchor.constraint(equalTo: contentView.topAnchor), + tableView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + tableView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + tableView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor) + ]) + + header.step = .one + } + + private func bindActions() { + footer.nextStepButton.addTarget(self, action: #selector(continueButtonDidClick), for: .touchUpInside) + footer.skipButton.addTarget(self, action: #selector(skipButtonDidClick), for: .touchUpInside) + } + + private func bindViewModel() { + let input = ReportViewModel.Input( + didToggleSelected: didToggleSelected.eraseToAnyPublisher(), + comment: comment.eraseToAnyPublisher(), + step1Continue: step1Continue.eraseToAnyPublisher(), + step1Skip: step1Skip.eraseToAnyPublisher(), + step2Continue: step2Continue.eraseToAnyPublisher(), + step2Skip: step2Skip.eraseToAnyPublisher(), + cancel: cancel.eraseToAnyPublisher(), + tableView: tableView + ) + let output = viewModel.transform(input: input) + output?.currentStep + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] (step) in + guard step == .two else { return } + guard let self = self else { return } + + self.header.step = .two + self.footer.step = .two + self.switchToStep2Content() + }) + .store(in: &disposeBag) + + output?.continueEnableSubject + .receive(on: DispatchQueue.main) + .filter { [weak self] _ in + guard let step = self?.viewModel.currentStep.value, step == .one else { return false } + return true + } + .assign(to: \.nextStepButton.isEnabled, on: footer) + .store(in: &disposeBag) + + output?.sendEnableSubject + .receive(on: DispatchQueue.main) + .filter { [weak self] _ in + guard let step = self?.viewModel.currentStep.value, step == .two else { return false } + return true + } + .assign(to: \.nextStepButton.isEnabled, on: footer) + .store(in: &disposeBag) + + output?.reportSuccess + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] (_) in + self?.dismiss(animated: true, completion: nil) + }) + .store(in: &disposeBag) + } + + private func setupNavigation() { + navigationItem.rightBarButtonItem + = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.cancel, + target: self, + action: #selector(doneButtonDidClick)) + navigationItem.rightBarButtonItem?.tintColor = Asset.Colors.Label.highlight.color + + // fetch old mastodon user + let beReportedUser: MastodonUser? = { + guard let domain = context.authenticationService.activeMastodonAuthenticationBox.value?.domain else { + return nil + } + let request = MastodonUser.sortedFetchRequest + request.predicate = MastodonUser.predicate(domain: domain, id: viewModel.userId) + request.fetchLimit = 1 + request.returnsObjectsAsFaults = false + do { + return try viewModel.statusFetchedResultsController.fetchedResultsController.managedObjectContext.fetch(request).first + } catch { + assertionFailure(error.localizedDescription) + return nil + } + }() + + navigationItem.title = L10n.Scene.Report.title( + "\(beReportedUser?.displayName ?? "@\(beReportedUser?.acct ?? "")")" + ) + } + + private func switchToStep2Content() { + self.contentView.addSubview(self.textView) + self.textView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + self.textView.topAnchor.constraint(equalTo: self.contentView.topAnchor), + self.textView.leadingAnchor.constraint( + equalTo: self.contentView.readableContentGuide.leadingAnchor, + constant: ReportView.horizontalMargin + ), + self.textView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor), + self.textView.trailingAnchor.constraint( + equalTo: self.contentView.safeAreaLayoutGuide.trailingAnchor, + constant: -1 * ReportView.horizontalMargin + ), + ]) + self.textView.layoutIfNeeded() + + UIView.transition( + with: contentView, + duration: ReportViewController.kAnimationDuration, + options: UIView.AnimationOptions.transitionCrossDissolve) { + [weak self] in + guard let self = self else { return } + + self.contentView.addSubview(self.textView) + self.tableView.isHidden = true + } completion: { (_) in + } + } + + // Mark: - Actions + @objc func doneButtonDidClick() { + dismiss(animated: true, completion: nil) + } + + @objc func continueButtonDidClick() { + if viewModel.currentStep.value == .one { + step1Continue.send() + } else { + step2Continue.send() + } + } + + @objc func skipButtonDidClick() { + if viewModel.currentStep.value == .one { + step1Skip.send() + } else { + step2Skip.send() + } + } +} + +extension ReportViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let item = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { + return + } + + didToggleSelected.send(item) + } +} + +extension ReportViewController: UITextViewDelegate { + func textViewDidChange(_ textView: UITextView) { + self.comment.send(textView.text) + } +} diff --git a/Mastodon/Scene/Report/ReportViewModel+Data.swift b/Mastodon/Scene/Report/ReportViewModel+Data.swift new file mode 100644 index 000000000..4df2ccb20 --- /dev/null +++ b/Mastodon/Scene/Report/ReportViewModel+Data.swift @@ -0,0 +1,98 @@ +// +// ReportViewModel+Data.swift +// Mastodon +// +// Created by ihugo on 2021/4/20. +// + +import Combine +import CoreData +import CoreDataStack +import Foundation +import MastodonSDK +import UIKit +import os.log + +extension ReportViewModel { + func requestRecentStatus( + domain: String, + accountId: String, + authorizationBox: AuthenticationService.MastodonAuthenticationBox + ) { + context.apiService.userTimeline( + domain: domain, + accountID: accountId, + authorizationBox: authorizationBox + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] completion in + switch completion { + case .failure(let error): + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch user timeline fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + guard let self = self else { return } + guard let reportStatusId = self.statusId else { return } + var statusIDs = self.statusFetchedResultsController.statusIDs.value + guard statusIDs.contains(reportStatusId) else { return } + + statusIDs.append(reportStatusId) + self.statusFetchedResultsController.statusIDs.value = statusIDs + case .finished: + break + } + } receiveValue: { [weak self] response in + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + guard let self = self else { return } + + var statusIDs = response.value.map { $0.id } + if let reportStatusId = self.statusId, !statusIDs.contains(reportStatusId) { + statusIDs.append(reportStatusId) + } + + self.statusFetchedResultsController.statusIDs.value = statusIDs + } + .store(in: &disposeBag) + } + + func fetchStatus() { + let managedObjectContext = self.statusFetchedResultsController.fetchedResultsController.managedObjectContext + statusFetchedResultsController.objectIDs.eraseToAnyPublisher() + .receive(on: DispatchQueue.main) + .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) + .sink { [weak self] objectIDs in + guard let self = self else { return } + guard let diffableDataSource = self.diffableDataSource else { return } + + var items: [Item] = [] + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + + defer { + // not animate when empty items fix loader first appear layout issue + diffableDataSource.apply(snapshot, animatingDifferences: !items.isEmpty) + } + + var oldSnapshotAttributeDict: [NSManagedObjectID : Item.StatusAttribute] = [:] + let oldSnapshot = diffableDataSource.snapshot() + for item in oldSnapshot.itemIdentifiers { + guard case let .status(objectID, attribute) = item else { continue } + oldSnapshotAttributeDict[objectID] = attribute + } + + for objectID in objectIDs { + let attribute = oldSnapshotAttributeDict[objectID] ?? Item.StatusAttribute() + let item = Item.status(objectID: objectID, attribute: attribute) + items.append(item) + + guard let status = managedObjectContext.object(with: objectID) as? Status else { + continue + } + if status.id == self.statusId { + self.selectedItems.append(item) + self.continueEnableSubject.send(true) + } + } + snapshot.appendItems(items, toSection: .main) + } + .store(in: &disposeBag) + } +} diff --git a/Mastodon/Scene/Report/ReportViewModel+Diffable.swift b/Mastodon/Scene/Report/ReportViewModel+Diffable.swift new file mode 100644 index 000000000..38f2edb19 --- /dev/null +++ b/Mastodon/Scene/Report/ReportViewModel+Diffable.swift @@ -0,0 +1,37 @@ +// +// ReportViewModel+Diffable.swift +// Mastodon +// +// Created by ihugo on 2021/4/19. +// + +import UIKit +import Combine +import CoreData +import CoreDataStack + +extension ReportViewModel { + func setupDiffableDataSource( + for tableView: UITableView, + dependency: NeedsDependency, + reportdStatusDelegate: ReportedStatusTableViewCellDelegate + ) { + let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common) + .autoconnect() + .share() + .eraseToAnyPublisher() + + diffableDataSource = ReportSection.tableViewDiffableDataSource( + for: tableView, + dependency: dependency, + managedObjectContext: statusFetchedResultsController.fetchedResultsController.managedObjectContext, + timestampUpdatePublisher: timestampUpdatePublisher, + reportdStatusDelegate: reportdStatusDelegate + ) + + // set empty section to make update animation top-to-bottom style + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + diffableDataSource?.apply(snapshot) + } +} diff --git a/Mastodon/Scene/Report/ReportViewModel+Provider.swift b/Mastodon/Scene/Report/ReportViewModel+Provider.swift new file mode 100644 index 000000000..052fc2ba1 --- /dev/null +++ b/Mastodon/Scene/Report/ReportViewModel+Provider.swift @@ -0,0 +1,86 @@ +//// +//// ReportViewModel+Provider.swift +//// Mastodon +//// +//// Created by ihugo on 2021/4/19. +//// +// +//import Combine +//import CoreData +//import CoreDataStack +//import Foundation +//import MastodonSDK +//import UIKit +//import os.log +// +//extension ReportViewController: StatusProvider { +// func status() -> Future { +// return Future { promise in promise(.success(nil)) } +// } +// +// func status(for cell: UITableViewCell?, indexPath: IndexPath?) -> Future { +// return Future { promise in +// guard let diffableDataSource = self.viewModel.diffableDataSource else { +// assertionFailure() +// promise(.success(nil)) +// return +// } +// guard let indexPath = indexPath ?? cell.flatMap({ self.tableView.indexPath(for: $0) }), +// let item = diffableDataSource.itemIdentifier(for: indexPath) else { +// promise(.success(nil)) +// return +// } +// +// switch item { +// case .status(let objectID, _): +// let managedObjectContext = self.viewModel.statusFetchedResultsController.fetchedResultsController.managedObjectContext +// managedObjectContext.perform { +// let status = managedObjectContext.object(with: objectID) as? Status +// promise(.success(status)) +// } +// default: +// promise(.success(nil)) +// } +// } +// } +// +// func status(for cell: UICollectionViewCell) -> Future { +// return Future { promise in promise(.success(nil)) } +// } +// +// var managedObjectContext: NSManagedObjectContext { +// return viewModel.statusFetchedResultsController.fetchedResultsController.managedObjectContext +// } +// +// var tableViewDiffableDataSource: UITableViewDiffableDataSource? { +// return viewModel.diffableDataSource +// } +// +// func item(for cell: UITableViewCell?, indexPath: IndexPath?) -> Item? { +// guard let diffableDataSource = self.viewModel.diffableDataSource else { +// assertionFailure() +// return nil +// } +// +// guard let indexPath = indexPath ?? cell.flatMap({ self.tableView.indexPath(for: $0) }), +// let item = diffableDataSource.itemIdentifier(for: indexPath) else { +// return nil +// } +// +// return item +// } +// +// func items(indexPaths: [IndexPath]) -> [Item] { +// guard let diffableDataSource = self.viewModel.diffableDataSource else { +// assertionFailure() +// return [] +// } +// +// var items: [Item] = [] +// for indexPath in indexPaths { +// guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { continue } +// items.append(item) +// } +// return items +// } +//} diff --git a/Mastodon/Scene/Report/ReportViewModel.swift b/Mastodon/Scene/Report/ReportViewModel.swift new file mode 100644 index 000000000..94404af10 --- /dev/null +++ b/Mastodon/Scene/Report/ReportViewModel.swift @@ -0,0 +1,249 @@ +// +// ReportViewModel.swift +// Mastodon +// +// Created by ihugo on 2021/4/19. +// + +import Combine +import CoreData +import CoreDataStack +import Foundation +import MastodonSDK +import UIKit +import os.log + +class ReportViewModel: NSObject, NeedsDependency { + typealias FileReportQuery = Mastodon.API.Reports.FileReportQuery + + enum Step: Int { + case one + case two + } + + // confirm set only once + weak var context: AppContext! { willSet { precondition(context == nil) } } + weak var coordinator: SceneCoordinator! { willSet { precondition(coordinator == nil) } } + var userId: String + var statusId: String? + var selectedItems = [Item]() + var comment: String? + + internal var reportQuery: FileReportQuery + internal var disposeBag = Set() + internal let currentStep = CurrentValueSubject(.one) + internal let statusFetchedResultsController: StatusFetchedResultsController + internal var diffableDataSource: UITableViewDiffableDataSource? + internal let continueEnableSubject = CurrentValueSubject(false) + internal let sendEnableSubject = CurrentValueSubject(false) + internal let reportSuccess = PassthroughSubject() + + struct Input { + let didToggleSelected: AnyPublisher + let comment: AnyPublisher + let step1Continue: AnyPublisher + let step1Skip: AnyPublisher + let step2Continue: AnyPublisher + let step2Skip: AnyPublisher + let cancel: AnyPublisher + let tableView: UITableView + } + + struct Output { + let currentStep: AnyPublisher + let continueEnableSubject: AnyPublisher + let sendEnableSubject: AnyPublisher + let reportSuccess: AnyPublisher + } + + init(context: AppContext, + coordinator: SceneCoordinator, + domain: String, + userId: String, + statusId: String? + ) { + self.context = context + self.coordinator = coordinator + self.userId = userId + self.statusId = statusId + self.statusFetchedResultsController = StatusFetchedResultsController( + managedObjectContext: context.managedObjectContext, + domain: domain, + additionalTweetPredicate: Status.notDeleted() + ) + + self.reportQuery = FileReportQuery( + accountId: userId, + statusIds: nil, + comment: nil, + forward: nil + ) + super.init() + } + + func transform(input: Input?) -> Output? { + guard let input = input else { return nil } + guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { + return nil + } + let domain = activeMastodonAuthenticationBox.domain + + setupDiffableDataSource( + for: input.tableView, + dependency: self, + reportdStatusDelegate: self + ) + + // data binding + bindData(input: input) + + // step1 and step2 binding + bindForStep1(input: input) + bindForStep2( + input: input, + domain: domain, + activeMastodonAuthenticationBox: activeMastodonAuthenticationBox + ) + + requestRecentStatus( + domain: domain, + accountId: self.userId, + authorizationBox: activeMastodonAuthenticationBox + ) + + fetchStatus() + + return Output( + currentStep: currentStep.eraseToAnyPublisher(), + continueEnableSubject: continueEnableSubject.eraseToAnyPublisher(), + sendEnableSubject: sendEnableSubject.eraseToAnyPublisher(), + reportSuccess: reportSuccess.eraseToAnyPublisher() + ) + } + + // MARK: - Private methods + func bindData(input: Input) { + input.didToggleSelected.sink { [weak self] (item) in + guard let self = self else { return } + guard case let .status(objectID, attribute) = item else { return } + guard var snapshot = self.diffableDataSource?.snapshot() else { return } + let managedObjectContext = self.statusFetchedResultsController.fetchedResultsController.managedObjectContext + guard let status = managedObjectContext.object(with: objectID) as? Status else { + return + } + + var items = [Item]() + if let index = self.selectedItems.firstIndex(of: item) { + self.selectedItems.remove(at: index) + items.append(.status(objectID: objectID, attribute: attribute)) + + if let index = self.reportQuery.statusIds?.firstIndex(of: status.id) { + self.reportQuery.statusIds?.remove(at: index) + } + } else { + self.selectedItems.append(item) + items.append(.status(objectID: objectID, attribute: attribute)) + self.reportQuery.statusIds?.append(status.id) + } + + snapshot.reloadItems([item]) + self.diffableDataSource?.apply(snapshot, animatingDifferences: false) + + let continueEnable = self.selectedItems.count > 0 + self.continueEnableSubject.send(continueEnable) + } + .store(in: &disposeBag) + + input.comment.assign( + to: \.comment, + on: self + ) + .store(in: &disposeBag) + input.comment.sink { [weak self] (comment) in + guard let self = self else { return } + let sendEnable = (comment?.length ?? 0) > 0 + self.sendEnableSubject.send(sendEnable) + } + .store(in: &disposeBag) + } + + func bindForStep1(input: Input) { + let skip = input.step1Skip.map { [weak self] value -> Void in + guard let self = self else { return value } + self.selectedItems.removeAll() + return value + } + + Publishers.Merge(skip, input.step1Continue) + .sink { [weak self] _ in + self?.currentStep.value = .two + self?.sendEnableSubject.send(false) + } + .store(in: &disposeBag) + } + + func bindForStep2(input: Input, domain: String, activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox) { + let skip = input.step2Skip.map { [weak self] value -> Void in + guard let self = self else { return value } + self.comment = nil + return value + } + + Publishers.Merge(skip, input.step2Continue) + .sink { [weak self] _ in + guard let self = self else { return } + let managedObjectContext = self.statusFetchedResultsController.fetchedResultsController.managedObjectContext + + self.reportQuery.comment = self.comment + + var selectedStatusIds = [String]() + self.selectedItems.forEach { (item) in + guard case .status(let objectId, _) = item else { + return + } + guard let status = managedObjectContext.object(with: objectId) as? Status else { + return + } + selectedStatusIds.append(status.id) + } + self.reportQuery.statusIds = selectedStatusIds + + self.context.apiService.report( + domain: domain, + query: self.reportQuery, + mastodonAuthenticationBox: activeMastodonAuthenticationBox + ) + .sink { [weak self](data) in + switch data { + case .failure(let error): + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fail to file a report : %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + + let alertController = UIAlertController(for: error, title: nil, preferredStyle: .alert) + let okAction = UIAlertAction(title: "OK", style: .default, handler: nil) + alertController.addAction(okAction) + self?.coordinator.present( + scene: .alertController(alertController: alertController), + from: nil, + transition: .alertController(animated: true, completion: nil) + ) + case .finished: + self?.reportSuccess.send() + } + + } receiveValue: { (data) in + } + .store(in: &self.disposeBag) + } + .store(in: &disposeBag) + } +} + +extension ReportViewModel: ReportedStatusTableViewCellDelegate { + func reportedStatus(cell: ReportedStatusTableViewCell, isSelected indexPath: IndexPath) -> Bool { + guard let item = diffableDataSource?.itemIdentifier(for: indexPath) else { + return false + } + + return selectedItems.contains(item) + } +} diff --git a/Mastodon/Scene/Report/ReportedStatusTableviewCell.swift b/Mastodon/Scene/Report/ReportedStatusTableviewCell.swift new file mode 100644 index 000000000..8c7bd221c --- /dev/null +++ b/Mastodon/Scene/Report/ReportedStatusTableviewCell.swift @@ -0,0 +1,176 @@ +// +// ReportedStatusTableViewCell.swift +// Mastodon +// +// Created by ihugo on 2021/4/20. +// + +import os.log +import UIKit +import AVKit +import Combine +import CoreData +import CoreDataStack +import ActiveLabel + +protocol ReportedStatusTableViewCellDelegate: class { + func reportedStatus(cell: ReportedStatusTableViewCell, isSelected indexPath: IndexPath) -> Bool +} + +final class ReportedStatusTableViewCell: UITableViewCell, StatusCell { + + static let bottomPaddingHeight: CGFloat = 10 + + weak var delegate: ReportedStatusTableViewCellDelegate? + var disposeBag = Set() + var pollCountdownSubscription: AnyCancellable? + var observations = Set() + var checked: Bool = false + + let statusView = StatusView() + let separatorLine = UIView.separatorLine + + let checkbox: UIImageView = { + let imageView = UIImageView() + imageView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(textStyle: .body) + imageView.tintColor = Asset.Colors.Label.secondary.color + imageView.contentMode = .scaleAspectFill + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + var separatorLineToEdgeLeadingLayoutConstraint: NSLayoutConstraint! + var separatorLineToEdgeTrailingLayoutConstraint: NSLayoutConstraint! + + var separatorLineToMarginLeadingLayoutConstraint: NSLayoutConstraint! + var separatorLineToMarginTrailingLayoutConstraint: NSLayoutConstraint! + + override func prepareForReuse() { + super.prepareForReuse() + checked = false + statusView.isStatusTextSensitive = false + statusView.cleanUpContentWarning() + statusView.pollTableView.dataSource = nil + statusView.playerContainerView.reset() + statusView.playerContainerView.isHidden = true + disposeBag.removeAll() + observations.removeAll() + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + + override func layoutSubviews() { + super.layoutSubviews() + DispatchQueue.main.async { + self.statusView.drawContentWarningImageView() + } + } + + override func setHighlighted(_ highlighted: Bool, animated: Bool) { + super.setHighlighted(highlighted, animated: animated) + if highlighted { + checkbox.image = UIImage(systemName: "checkmark.circle.fill") + checkbox.tintColor = Asset.Colors.Label.highlight.color + } else if !checked { + checkbox.image = UIImage(systemName: "circle") + checkbox.tintColor = Asset.Colors.Label.secondary.color + } + } +} + +extension ReportedStatusTableViewCell { + + private func _init() { + backgroundColor = Asset.Colors.Background.systemBackground.color + statusView.contentWarningBlurContentImageView.backgroundColor = Asset.Colors.Background.systemBackground.color + + checkbox.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(checkbox) + NSLayoutConstraint.activate([ + checkbox.widthAnchor.constraint(equalToConstant: 23), + checkbox.heightAnchor.constraint(equalToConstant: 22), + checkbox.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor, constant: 12), + checkbox.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + ]) + + statusView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(statusView) + NSLayoutConstraint.activate([ + statusView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20), + statusView.leadingAnchor.constraint(equalTo: checkbox.trailingAnchor, constant: 20), + contentView.readableContentGuide.trailingAnchor.constraint(equalTo: statusView.trailingAnchor), + contentView.bottomAnchor.constraint(equalTo: statusView.bottomAnchor, constant: 20), + ]) + + separatorLine.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(separatorLine) + separatorLineToEdgeLeadingLayoutConstraint = separatorLine.leadingAnchor.constraint(equalTo: contentView.leadingAnchor) + separatorLineToEdgeTrailingLayoutConstraint = separatorLine.trailingAnchor.constraint(equalTo: contentView.trailingAnchor) + separatorLineToMarginLeadingLayoutConstraint = separatorLine.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor) + separatorLineToMarginTrailingLayoutConstraint = separatorLine.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor) + NSLayoutConstraint.activate([ + separatorLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)), + ]) + resetSeparatorLineLayout() + + selectionStyle = .none + statusView.actionToolbarContainer.isHidden = true + statusView.isUserInteractionEnabled = false + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + resetSeparatorLineLayout() + } + + func setupSelected(_ selected: Bool) { + checked = selected + if selected { + checkbox.image = UIImage(systemName: "checkmark.circle.fill") + } else { + checkbox.image = UIImage(systemName: "circle") + } + checkbox.tintColor = Asset.Colors.Label.secondary.color + } +} + +extension ReportedStatusTableViewCell { + private func resetSeparatorLineLayout() { + separatorLineToEdgeLeadingLayoutConstraint.isActive = false + separatorLineToEdgeTrailingLayoutConstraint.isActive = false + separatorLineToMarginLeadingLayoutConstraint.isActive = false + separatorLineToMarginTrailingLayoutConstraint.isActive = false + + if traitCollection.userInterfaceIdiom == .phone { + // to edge + NSLayoutConstraint.activate([ + separatorLineToEdgeLeadingLayoutConstraint, + separatorLineToEdgeTrailingLayoutConstraint, + ]) + } else { + if traitCollection.horizontalSizeClass == .compact { + // to edge + NSLayoutConstraint.activate([ + separatorLineToEdgeLeadingLayoutConstraint, + separatorLineToEdgeTrailingLayoutConstraint, + ]) + } else { + // to margin + NSLayoutConstraint.activate([ + separatorLineToMarginLeadingLayoutConstraint, + separatorLineToMarginTrailingLayoutConstraint, + ]) + } + } + } +} diff --git a/Mastodon/Scene/Settings/SettingsViewController.swift b/Mastodon/Scene/Settings/SettingsViewController.swift index 4615f92ab..156321d89 100644 --- a/Mastodon/Scene/Settings/SettingsViewController.swift +++ b/Mastodon/Scene/Settings/SettingsViewController.swift @@ -148,7 +148,7 @@ class SettingsViewController: UIViewController, NeedsDependency { } private func setupView() { - view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color + view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color setupNavigation() setupTableView() diff --git a/Mastodon/Service/APIService/APIService+Report.swift b/Mastodon/Service/APIService/APIService+Report.swift new file mode 100644 index 000000000..3c170c625 --- /dev/null +++ b/Mastodon/Service/APIService/APIService+Report.swift @@ -0,0 +1,23 @@ +// +// APIService+Report.swift +// Mastodon +// +// Created by ihugo on 2021/4/19. +// + +import Foundation +import MastodonSDK +import Combine + +extension APIService { + + func report( + domain: String, + query: Mastodon.API.Reports.FileReportQuery, + mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox + ) -> AnyPublisher, Error> { + let authorization = mastodonAuthenticationBox.userAuthorization + + return Mastodon.API.Reports.fileReport(session: session, domain: domain, query: query, authorization: authorization) + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Report.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Report.swift new file mode 100644 index 000000000..eac7f64f7 --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Report.swift @@ -0,0 +1,79 @@ +// +// File.swift +// +// +// Created by ihugo on 2021/4/19. +// + +import Combine +import Foundation + +extension Mastodon.API.Reports { + static func reportsEndpointURL(domain: String) -> URL { + Mastodon.API.endpointURL(domain: domain).appendingPathComponent("reports") + } + + /// File a report + /// + /// Version history: + /// 1.1 - added + /// 2.3.0 - add forward parameter + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/search/) + /// - Parameters: + /// - session: `URLSession` + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - query: fileReportQuery query + /// - authorization: User token + /// - Returns: `AnyPublisher` contains status indicate if report sucessfully. + public static func fileReport( + session: URLSession, + domain: String, + query: Mastodon.API.Reports.FileReportQuery, + authorization: Mastodon.API.OAuth.Authorization + ) -> AnyPublisher, Error> { + let request = Mastodon.API.post( + url: reportsEndpointURL(domain: domain), + query: query, + authorization: authorization + ) + return session.dataTaskPublisher(for: request) + .tryMap { data, response in + if let response = response as? HTTPURLResponse { + return Mastodon.Response.Content( + value: response.statusCode == 200, + response: response + ) + } + return Mastodon.Response.Content(value: false, response: response) + } + .eraseToAnyPublisher() + } +} + + +public extension Mastodon.API.Reports { + class FileReportQuery: Codable, PostQuery { + public let accountId: String + public var statusIds: [String]? + public var comment: String? + public let forward: Bool? + + enum CodingKeys: String, CodingKey { + case accountId = "account_id" + case statusIds = "status_ids" + case comment + case forward + } + + public init(accountId: String, + statusIds: [String]?, + comment: String?, + forward: Bool?) { + self.accountId = accountId + self.statusIds = statusIds + self.comment = comment + self.forward = forward + } + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift index 1a4496ed3..c85c2cd74 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift @@ -116,6 +116,7 @@ extension Mastodon.API { public enum Suggestions { } public enum Notifications { } public enum Subscriptions { } + public enum Reports { } } extension Mastodon.API { From d1e3039e386c5791b0f28feb344e78a7c2474710 Mon Sep 17 00:00:00 2001 From: ihugo Date: Thu, 22 Apr 2021 13:08:53 +0800 Subject: [PATCH 02/18] fix: compile issue --- Mastodon/Scene/Report/ReportedStatusTableviewCell.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Mastodon/Scene/Report/ReportedStatusTableviewCell.swift b/Mastodon/Scene/Report/ReportedStatusTableviewCell.swift index 8c7bd221c..a25c8b507 100644 --- a/Mastodon/Scene/Report/ReportedStatusTableviewCell.swift +++ b/Mastodon/Scene/Report/ReportedStatusTableviewCell.swift @@ -48,10 +48,11 @@ final class ReportedStatusTableViewCell: UITableViewCell, StatusCell { override func prepareForReuse() { super.prepareForReuse() checked = false - statusView.isStatusTextSensitive = false - statusView.cleanUpContentWarning() + statusView.updateContentWarningDisplay(isHidden: true, animated: false) + statusView.statusMosaicImageViewContainer.contentWarningOverlayView.isUserInteractionEnabled = true statusView.pollTableView.dataSource = nil statusView.playerContainerView.reset() + statusView.playerContainerView.contentWarningOverlayView.isUserInteractionEnabled = true statusView.playerContainerView.isHidden = true disposeBag.removeAll() observations.removeAll() @@ -90,7 +91,6 @@ extension ReportedStatusTableViewCell { private func _init() { backgroundColor = Asset.Colors.Background.systemBackground.color - statusView.contentWarningBlurContentImageView.backgroundColor = Asset.Colors.Background.systemBackground.color checkbox.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(checkbox) From 3810588abb421b88888b17b96eae9cb65282bc0e Mon Sep 17 00:00:00 2001 From: ihugo Date: Thu, 22 Apr 2021 22:58:21 +0800 Subject: [PATCH 03/18] chor: restyle localized string --- Localization/app.json | 4 ++-- Mastodon/Generated/Strings.swift | 6 +++--- Mastodon/Resources/en.lproj/Localizable.strings | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index ec908384b..fcd960293 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -399,8 +399,8 @@ "content1": "Are there any other posts you’d like to add to the report?", "content2": "Is there anything the moderators should know about this report?", "send": "Send Report", - "skipToSend": "Send without comment", - "textPlaceHolder": "|Type or paste additional comments" + "skip_to_send": "Send without comment", + "text_placeholder": "Type or paste additional comments" } } } diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 0972b5a20..d9baf8665 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -537,13 +537,13 @@ internal enum L10n { /// Send Report internal static let send = L10n.tr("Localizable", "Scene.Report.Send") /// Send without comment - internal static let skiptosend = L10n.tr("Localizable", "Scene.Report.Skiptosend") + internal static let skipToSend = L10n.tr("Localizable", "Scene.Report.SkipToSend") /// Step 1 of 2 internal static let step1 = L10n.tr("Localizable", "Scene.Report.Step1") /// Step 2 of 2 internal static let step2 = L10n.tr("Localizable", "Scene.Report.Step2") - /// |Type or paste additional comments - internal static let textplaceholder = L10n.tr("Localizable", "Scene.Report.Textplaceholder") + /// Type or paste additional comments + internal static let textPlaceholder = L10n.tr("Localizable", "Scene.Report.TextPlaceholder") /// Report %@ internal static func title(_ p1: Any) -> String { return L10n.tr("Localizable", "Scene.Report.Title", String(describing: p1)) diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 346734666..e16d3e886 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -173,10 +173,10 @@ tap the link to confirm your account."; "Scene.Report.Content1" = "Are there any other posts you’d like to add to the report?"; "Scene.Report.Content2" = "Is there anything the moderators should know about this report?"; "Scene.Report.Send" = "Send Report"; -"Scene.Report.Skiptosend" = "Send without comment"; +"Scene.Report.SkipToSend" = "Send without comment"; "Scene.Report.Step1" = "Step 1 of 2"; "Scene.Report.Step2" = "Step 2 of 2"; -"Scene.Report.Textplaceholder" = "|Type or paste additional comments"; +"Scene.Report.TextPlaceholder" = "Type or paste additional comments"; "Scene.Report.Title" = "Report %@"; "Scene.Search.Recommend.Accounts.Description" = "Except for Sam, you will not like his account."; "Scene.Search.Recommend.Accounts.Follow" = "Follow"; From 1411bcadf61245452d7d830508c8ea935e3808a1 Mon Sep 17 00:00:00 2001 From: ihugo Date: Thu, 22 Apr 2021 22:58:55 +0800 Subject: [PATCH 04/18] doc: update doc for Report API --- MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Report.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Report.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Report.swift index eac7f64f7..a0afabb2b 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Report.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Report.swift @@ -19,7 +19,7 @@ extension Mastodon.API.Reports { /// 1.1 - added /// 2.3.0 - add forward parameter /// # Reference - /// [Document](https://docs.joinmastodon.org/methods/search/) + /// [Document](https://docs.joinmastodon.org/methods/accounts/reports/) /// - Parameters: /// - session: `URLSession` /// - domain: Mastodon instance domain. e.g. "example.com" From 21264fead19a386ce9322c304360014b5ca1a7b5 Mon Sep 17 00:00:00 2001 From: ihugo Date: Thu, 22 Apr 2021 23:01:07 +0800 Subject: [PATCH 05/18] refactor: To pass the ViewModel to coordinator --- Mastodon/Coordinator/SceneCoordinator.swift | 21 ++++++------------- ...meTimelineViewController+DebugAction.swift | 17 +++++++++++++-- .../Scene/Settings/SettingsViewModel.swift | 6 ++---- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 93393d542..18d76e446 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -65,8 +65,8 @@ extension SceneCoordinator { case safari(url: URL) case alertController(alertController: UIAlertController) case activityViewController(activityViewController: UIActivityViewController, sourceView: UIView?, barButtonItem: UIBarButtonItem?) - case settings - case report(userId: String, statusId: String?) + case settings(viewModel: SettingsViewModel) + case report(viewModel: ReportViewModel) #if DEBUG case publicTimeline #endif @@ -265,22 +265,13 @@ private extension SceneCoordinator { activityViewController.popoverPresentationController?.sourceView = sourceView activityViewController.popoverPresentationController?.barButtonItem = barButtonItem viewController = activityViewController - case .settings: + case .settings(let viewModel): let _viewController = SettingsViewController() - _viewController.viewModel = SettingsViewModel(context: appContext, coordinator: self) + _viewController.viewModel = viewModel viewController = _viewController - case .report(let userId, let statusId): - guard let authenticationBox = appContext.authenticationService.activeMastodonAuthenticationBox.value else { - return nil - } + case .report(let viewModel): let _viewController = ReportViewController() - _viewController.viewModel = ReportViewModel( - context: appContext, - coordinator: self, - domain: authenticationBox.domain, - userId: userId, - statusId: statusId - ) + _viewController.viewModel = viewModel viewController = _viewController #if DEBUG case .publicTimeline: diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift index 459652008..fca20c92b 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift @@ -332,7 +332,12 @@ extension HomeTimelineViewController { } @objc private func showSettings(_ sender: UIAction) { - coordinator.present(scene: .settings, from: self, transition: .modal(animated: true, completion: nil)) + let viewModel = SettingsViewModel(context: context) + coordinator.present( + scene: .settings(viewModel: viewModel), + from: self, + transition: .modal(animated: true, completion: nil) + ) } @objc private func showReportAction(_ sender: UIAction) { @@ -350,12 +355,20 @@ extension HomeTimelineViewController { guard let userId = accountTextField.text else { return } guard let statusId = statusTextField.text else { return } + guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return } // itodo: delete them // 31803 // 106093402888557459 + let viewModel = ReportViewModel( + context: self.context, + coordinator: self.coordinator, + domain: authenticationBox.domain, + userId: userId, + statusId: statusId + ) self.coordinator.present( - scene: .report(userId: userId, statusId: statusId), + scene: .report(viewModel: viewModel), from: self, transition: .modal(animated: true, completion: nil)) } alertController.addAction(showAction) diff --git a/Mastodon/Scene/Settings/SettingsViewModel.swift b/Mastodon/Scene/Settings/SettingsViewModel.swift index 470617aeb..57371f92b 100644 --- a/Mastodon/Scene/Settings/SettingsViewModel.swift +++ b/Mastodon/Scene/Settings/SettingsViewModel.swift @@ -13,10 +13,9 @@ import MastodonSDK import UIKit import os.log -class SettingsViewModel: NSObject, NeedsDependency { +class SettingsViewModel: NSObject { // confirm set only once weak var context: AppContext! { willSet { precondition(context == nil) } } - weak var coordinator: SceneCoordinator! { willSet { precondition(coordinator == nil) } } var dataSource: UITableViewDiffableDataSource! var disposeBag = Set() @@ -87,9 +86,8 @@ class SettingsViewModel: NSObject, NeedsDependency { struct Output { } - init(context: AppContext, coordinator: SceneCoordinator) { + init(context: AppContext) { self.context = context - self.coordinator = coordinator super.init() } From ce3f4f5e96adbb1749214d3a62fd6e4e2ac2b65b Mon Sep 17 00:00:00 2001 From: ihugo Date: Thu, 22 Apr 2021 23:02:24 +0800 Subject: [PATCH 06/18] refactor: Use single file organization classes. --- Mastodon.xcodeproj/project.pbxproj | 12 +- Mastodon/Scene/Report/ReportFooterView.swift | 108 ++++++++++ Mastodon/Scene/Report/ReportHeaderView.swift | 115 ++++++++++ Mastodon/Scene/Report/ReportView.swift | 201 ------------------ .../Scene/Report/ReportViewController.swift | 12 +- Mastodon/Scene/Report/ReportViewModel.swift | 16 +- 6 files changed, 245 insertions(+), 219 deletions(-) create mode 100644 Mastodon/Scene/Report/ReportFooterView.swift create mode 100644 Mastodon/Scene/Report/ReportHeaderView.swift delete mode 100644 Mastodon/Scene/Report/ReportView.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index e3f553ba7..d53252220 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -137,6 +137,7 @@ 5B24BBDB262DB14800A9381B /* ReportViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B24BBD8262DB14800A9381B /* ReportViewModel+Diffable.swift */; }; 5B24BBDC262DB14800A9381B /* ReportViewModel+Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B24BBD9262DB14800A9381B /* ReportViewModel+Provider.swift */; }; 5B24BBE2262DB19100A9381B /* APIService+Report.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B24BBE1262DB19100A9381B /* APIService+Report.swift */; }; + 5B8E055826319E47006E3C53 /* ReportFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8E055726319E47006E3C53 /* ReportFooterView.swift */; }; 5B90C45E262599800002E742 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C456262599800002E742 /* SettingsViewModel.swift */; }; 5B90C45F262599800002E742 /* SettingsToggleTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C459262599800002E742 /* SettingsToggleTableViewCell.swift */; }; 5B90C460262599800002E742 /* SettingsAppearanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C45A262599800002E742 /* SettingsAppearanceTableViewCell.swift */; }; @@ -149,7 +150,7 @@ 5B90C48526259BF10002E742 /* APIService+Subscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C48426259BF10002E742 /* APIService+Subscriptions.swift */; }; 5B90C48B26259C120002E742 /* APIService+CoreData+Subscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C48A26259C120002E742 /* APIService+CoreData+Subscriptions.swift */; }; 5BB04FD5262E7AFF0043BFF6 /* ReportViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB04FD4262E7AFF0043BFF6 /* ReportViewController.swift */; }; - 5BB04FDB262EA3070043BFF6 /* ReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB04FDA262EA3070043BFF6 /* ReportView.swift */; }; + 5BB04FDB262EA3070043BFF6 /* ReportHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB04FDA262EA3070043BFF6 /* ReportHeaderView.swift */; }; 5BB04FE9262EFC300043BFF6 /* ReportedStatusTableviewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB04FE8262EFC300043BFF6 /* ReportedStatusTableviewCell.swift */; }; 5BB04FEF262F0DCB0043BFF6 /* ReportViewModel+Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB04FEE262F0DCB0043BFF6 /* ReportViewModel+Data.swift */; }; 5BB04FF5262F0E6D0043BFF6 /* ReportSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */; }; @@ -562,6 +563,7 @@ 5B24BBD8262DB14800A9381B /* ReportViewModel+Diffable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ReportViewModel+Diffable.swift"; sourceTree = ""; }; 5B24BBD9262DB14800A9381B /* ReportViewModel+Provider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ReportViewModel+Provider.swift"; sourceTree = ""; }; 5B24BBE1262DB19100A9381B /* APIService+Report.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+Report.swift"; sourceTree = ""; }; + 5B8E055726319E47006E3C53 /* ReportFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportFooterView.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 = ""; }; 5B90C45A262599800002E742 /* SettingsAppearanceTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsAppearanceTableViewCell.swift; sourceTree = ""; }; @@ -574,7 +576,7 @@ 5B90C48426259BF10002E742 /* APIService+Subscriptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+Subscriptions.swift"; sourceTree = ""; }; 5B90C48A26259C120002E742 /* APIService+CoreData+Subscriptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Subscriptions.swift"; sourceTree = ""; }; 5BB04FD4262E7AFF0043BFF6 /* ReportViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportViewController.swift; sourceTree = ""; }; - 5BB04FDA262EA3070043BFF6 /* ReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportView.swift; sourceTree = ""; }; + 5BB04FDA262EA3070043BFF6 /* ReportHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportHeaderView.swift; sourceTree = ""; }; 5BB04FE8262EFC300043BFF6 /* ReportedStatusTableviewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportedStatusTableviewCell.swift; sourceTree = ""; }; 5BB04FEE262F0DCB0043BFF6 /* ReportViewModel+Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReportViewModel+Data.swift"; sourceTree = ""; }; 5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportSection.swift; sourceTree = ""; }; @@ -1250,7 +1252,8 @@ 5B24BBD9262DB14800A9381B /* ReportViewModel+Provider.swift */, 5BB04FEE262F0DCB0043BFF6 /* ReportViewModel+Data.swift */, 5BB04FD4262E7AFF0043BFF6 /* ReportViewController.swift */, - 5BB04FDA262EA3070043BFF6 /* ReportView.swift */, + 5BB04FDA262EA3070043BFF6 /* ReportHeaderView.swift */, + 5B8E055726319E47006E3C53 /* ReportFooterView.swift */, 5BB04FE8262EFC300043BFF6 /* ReportedStatusTableviewCell.swift */, ); path = Report; @@ -2369,6 +2372,7 @@ 2DFAD5372617010500F9EE7C /* SearchingTableViewCell.swift in Sources */, DB447681260B3ED600B66B82 /* CustomEmojiPickerSection.swift in Sources */, 5BB04FEF262F0DCB0043BFF6 /* ReportViewModel+Data.swift in Sources */, + 5B8E055826319E47006E3C53 /* ReportFooterView.swift in Sources */, DBB525502611ED6D002F1F29 /* ProfileHeaderView.swift in Sources */, 0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */, DB71FD5225F8CCAA00512AE1 /* APIService+Status.swift in Sources */, @@ -2542,7 +2546,7 @@ DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */, DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */, 0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */, - 5BB04FDB262EA3070043BFF6 /* ReportView.swift in Sources */, + 5BB04FDB262EA3070043BFF6 /* ReportHeaderView.swift in Sources */, DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */, DB68A04A25E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift in Sources */, 0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */, diff --git a/Mastodon/Scene/Report/ReportFooterView.swift b/Mastodon/Scene/Report/ReportFooterView.swift new file mode 100644 index 000000000..16948f58e --- /dev/null +++ b/Mastodon/Scene/Report/ReportFooterView.swift @@ -0,0 +1,108 @@ +// +// ReportFooterView.swift +// Mastodon +// +// Created by ihugo on 2021/4/22. +// + +import UIKit + +final class ReportFooterView: UIView { + enum Step: Int { + case one + case two + } + + lazy var stackview: UIStackView = { + let view = UIStackView() + view.axis = .vertical + view.alignment = .fill + view.spacing = 8 + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + lazy var nextStepButton: PrimaryActionButton = { + let button = PrimaryActionButton() + button.setTitle(L10n.Common.Controls.Actions.continue, for: .normal) + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + lazy var skipButton: UIButton = { + let button = UIButton(type: .system) + button.tintColor = Asset.Colors.brandBlue.color + button.setTitle(L10n.Common.Controls.Actions.skip, for: .normal) + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + var step: Step = .one { + didSet { + switch step { + case .one: + nextStepButton.setTitle(L10n.Common.Controls.Actions.continue, for: .normal) + skipButton.setTitle(L10n.Common.Controls.Actions.skip, for: .normal) + case .two: + nextStepButton.setTitle(L10n.Scene.Report.send, for: .normal) + skipButton.setTitle(L10n.Scene.Report.skipToSend, for: .normal) + } + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + + self.backgroundColor = Asset.Colors.Background.elevatedPrimary.color + + stackview.addArrangedSubview(nextStepButton) + stackview.addArrangedSubview(skipButton) + addSubview(stackview) + + NSLayoutConstraint.activate([ + stackview.topAnchor.constraint( + equalTo: self.topAnchor, + constant: ReportView.continuTopMargin + ), + stackview.leadingAnchor.constraint( + equalTo: self.readableContentGuide.leadingAnchor, + constant: ReportView.horizontalMargin + ), + stackview.bottomAnchor.constraint( + equalTo: self.safeAreaLayoutGuide.bottomAnchor, + constant: -1 * ReportView.skipBottomMargin + ), + stackview.trailingAnchor.constraint( + equalTo: self.readableContentGuide.trailingAnchor, + constant: -1 * ReportView.horizontalMargin + ), + nextStepButton.heightAnchor.constraint( + equalToConstant: ReportView.buttonHeight + ), + skipButton.heightAnchor.constraint( + equalTo: nextStepButton.heightAnchor + ) + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +#if canImport(SwiftUI) && DEBUG +import SwiftUI + +struct ReportFooterView_Previews: PreviewProvider { + static var previews: some View { + Group { + UIViewPreview(width: 375) { () -> UIView in + return ReportFooterView(frame: CGRect(origin: .zero, size: CGSize(width: 375, height: 164))) + } + .previewLayout(.fixed(width: 375, height: 164)) + } + } + +} + +#endif diff --git a/Mastodon/Scene/Report/ReportHeaderView.swift b/Mastodon/Scene/Report/ReportHeaderView.swift new file mode 100644 index 000000000..bace3ada5 --- /dev/null +++ b/Mastodon/Scene/Report/ReportHeaderView.swift @@ -0,0 +1,115 @@ +// +// ReportView.swift +// Mastodon +// +// Created by ihugo on 2021/4/20. +// + +import UIKit + +struct ReportView { + static var horizontalMargin: CGFloat { return 12 } + static var verticalMargin: CGFloat { return 22 } + static var buttonHeight: CGFloat { return 46 } + static var skipBottomMargin: CGFloat { return 8 } + static var continuTopMargin: CGFloat { return 22 } +} + +final class ReportHeaderView: UIView { + enum Step: Int { + case one + case two + } + + lazy var titleLabel: UILabel = { + let label = UILabel() + label.textColor = Asset.Colors.Label.secondary.color + label.font = UIFontMetrics(forTextStyle: .subheadline) + .scaledFont(for: .systemFont(ofSize: 15, weight: .regular)) + label.numberOfLines = 0 + return label + }() + + lazy var contentLabel: UILabel = { + let label = UILabel() + label.textColor = Asset.Colors.Label.primary.color + label.font = UIFontMetrics(forTextStyle: .title3) + .scaledFont(for: .systemFont(ofSize: 20, weight: .semibold)) + label.numberOfLines = 0 + return label + }() + + lazy var stackview: UIStackView = { + let view = UIStackView() + view.axis = .vertical + view.alignment = .leading + view.spacing = 2 + return view + }() + + var step: Step = .one { + didSet { + switch step { + case .one: + titleLabel.text = L10n.Scene.Report.step1 + contentLabel.text = L10n.Scene.Report.content1 + case .two: + titleLabel.text = L10n.Scene.Report.step2 + contentLabel.text = L10n.Scene.Report.content2 + } + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + + self.backgroundColor = Asset.Colors.Background.elevatedPrimary.color + stackview.addArrangedSubview(titleLabel) + stackview.addArrangedSubview(contentLabel) + addSubview(stackview) + + stackview.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + stackview.safeAreaLayoutGuide.topAnchor.constraint( + equalTo: self.topAnchor, + constant: ReportView.verticalMargin + ), + stackview.leadingAnchor.constraint( + equalTo: self.readableContentGuide.leadingAnchor, + constant: ReportView.horizontalMargin + ), + stackview.bottomAnchor.constraint( + equalTo: self.bottomAnchor, + constant: -1 * ReportView.verticalMargin + ), + stackview.trailingAnchor.constraint( + equalTo: self.readableContentGuide.trailingAnchor, + constant: -1 * ReportView.horizontalMargin + ) + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +#if canImport(SwiftUI) && DEBUG +import SwiftUI + +struct ReportHeaderView_Previews: PreviewProvider { + static var previews: some View { + Group { + UIViewPreview { () -> UIView in + let view = ReportHeaderView() + view.step = .one + view.contentLabel.preferredMaxLayoutWidth = 335 + return view + } + .previewLayout(.fixed(width: 375, height: 110)) + } + } + +} + +#endif diff --git a/Mastodon/Scene/Report/ReportView.swift b/Mastodon/Scene/Report/ReportView.swift deleted file mode 100644 index 9166259fe..000000000 --- a/Mastodon/Scene/Report/ReportView.swift +++ /dev/null @@ -1,201 +0,0 @@ -// -// ReportView.swift -// Mastodon -// -// Created by ihugo on 2021/4/20. -// - -import UIKit - -struct ReportView { - static var horizontalMargin: CGFloat { return 12 } - static var verticalMargin: CGFloat { return 22 } - static var buttonHeight: CGFloat { return 46 } - static var skipBottomMargin: CGFloat { return 8 } - static var continuTopMargin: CGFloat { return 22 } -} - -final class ReportViewHeader: UIView { - enum Step: Int { - case one - case two - } - - lazy var titleLabel: UILabel = { - let label = UILabel() - label.textColor = Asset.Colors.Label.secondary.color - label.font = UIFont.preferredFont(forTextStyle: .subheadline) - label.numberOfLines = 0 - return label - }() - - lazy var contentLabel: UILabel = { - let label = UILabel() - label.textColor = Asset.Colors.Label.primary.color - label.font = UIFont.preferredFont(forTextStyle: .title3) - label.numberOfLines = 0 - return label - }() - - lazy var stackview: UIStackView = { - let view = UIStackView() - view.axis = .vertical - view.alignment = .leading - view.spacing = 2 - return view - }() - - var step: Step = .one { - didSet { - switch step { - case .one: - titleLabel.text = L10n.Scene.Report.step1 - contentLabel.text = L10n.Scene.Report.content1 - case .two: - titleLabel.text = L10n.Scene.Report.step2 - contentLabel.text = L10n.Scene.Report.content2 - } - } - } - - override init(frame: CGRect) { - super.init(frame: frame) - - self.backgroundColor = Asset.Colors.Background.elevatedPrimary.color - stackview.addArrangedSubview(titleLabel) - stackview.addArrangedSubview(contentLabel) - addSubview(stackview) - - stackview.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - stackview.safeAreaLayoutGuide.topAnchor.constraint( - equalTo: self.topAnchor, - constant: ReportView.verticalMargin - ), - stackview.leadingAnchor.constraint( - equalTo: self.readableContentGuide.leadingAnchor, - constant: ReportView.horizontalMargin - ), - stackview.bottomAnchor.constraint( - equalTo: self.bottomAnchor, - constant: -1 * ReportView.verticalMargin - ), - stackview.trailingAnchor.constraint( - equalTo: self.readableContentGuide.trailingAnchor, - constant: -1 * ReportView.horizontalMargin - ) - ]) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -final class ReportViewFooter: UIView { - enum Step: Int { - case one - case two - } - - lazy var stackview: UIStackView = { - let view = UIStackView() - view.axis = .vertical - view.alignment = .fill - view.spacing = 8 - view.translatesAutoresizingMaskIntoConstraints = false - return view - }() - - lazy var nextStepButton: PrimaryActionButton = { - let button = PrimaryActionButton() - button.setTitle(L10n.Common.Controls.Actions.continue, for: .normal) - button.translatesAutoresizingMaskIntoConstraints = false - return button - }() - - lazy var skipButton: UIButton = { - let button = UIButton(type: .system) - button.tintColor = Asset.Colors.brandBlue.color - button.setTitle(L10n.Common.Controls.Actions.skip, for: .normal) - button.translatesAutoresizingMaskIntoConstraints = false - return button - }() - - var step: Step = .one { - didSet { - switch step { - case .one: - nextStepButton.setTitle(L10n.Common.Controls.Actions.continue, for: .normal) - skipButton.setTitle(L10n.Common.Controls.Actions.skip, for: .normal) - case .two: - nextStepButton.setTitle(L10n.Scene.Report.send, for: .normal) - skipButton.setTitle(L10n.Scene.Report.skiptosend, for: .normal) - } - } - } - - override init(frame: CGRect) { - super.init(frame: frame) - - self.backgroundColor = Asset.Colors.Background.elevatedPrimary.color - - stackview.addArrangedSubview(nextStepButton) - stackview.addArrangedSubview(skipButton) - addSubview(stackview) - - NSLayoutConstraint.activate([ - stackview.topAnchor.constraint( - equalTo: self.topAnchor, - constant: ReportView.continuTopMargin - ), - stackview.leadingAnchor.constraint( - equalTo: self.readableContentGuide.leadingAnchor, - constant: ReportView.horizontalMargin - ), - stackview.bottomAnchor.constraint( - equalTo: self.safeAreaLayoutGuide.bottomAnchor, - constant: -1 * ReportView.skipBottomMargin - ), - stackview.trailingAnchor.constraint( - equalTo: self.readableContentGuide.trailingAnchor, - constant: -1 * ReportView.horizontalMargin - ), - nextStepButton.heightAnchor.constraint( - equalToConstant: ReportView.buttonHeight - ), - skipButton.heightAnchor.constraint( - equalTo: nextStepButton.heightAnchor - ) - ]) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -#if canImport(SwiftUI) && DEBUG -import SwiftUI - -struct ReportView_Previews: PreviewProvider { - static var previews: some View { - Group { - UIViewPreview { () -> UIView in - let view = ReportViewHeader() - view.step = .one - view.contentLabel.preferredMaxLayoutWidth = 335 - return view - } - .previewLayout(.fixed(width: 375, height: 110)) - - UIViewPreview(width: 375) { () -> UIView in - return ReportViewFooter(frame: CGRect(origin: .zero, size: CGSize(width: 375, height: 164))) - } - .previewLayout(.fixed(width: 375, height: 164)) - } - } - -} - -#endif diff --git a/Mastodon/Scene/Report/ReportViewController.swift b/Mastodon/Scene/Report/ReportViewController.swift index 7d46ae6b9..b00e4a3ee 100644 --- a/Mastodon/Scene/Report/ReportViewController.swift +++ b/Mastodon/Scene/Report/ReportViewController.swift @@ -30,14 +30,14 @@ class ReportViewController: UIViewController, NeedsDependency { let cancel = PassthroughSubject() // MAKK: - UI - lazy var header: ReportViewHeader = { - let view = ReportViewHeader() + lazy var header: ReportHeaderView = { + let view = ReportHeaderView() view.translatesAutoresizingMaskIntoConstraints = false return view }() - lazy var footer: ReportViewFooter = { - let view = ReportViewFooter() + lazy var footer: ReportFooterView = { + let view = ReportFooterView() view.translatesAutoresizingMaskIntoConstraints = false return view }() @@ -74,7 +74,7 @@ class ReportViewController: UIViewController, NeedsDependency { let textView = UITextView() textView.font = .preferredFont(forTextStyle: .body) textView.isScrollEnabled = false - textView.placeholder = L10n.Scene.Report.textplaceholder + textView.placeholder = L10n.Scene.Report.textPlaceholder textView.backgroundColor = .clear textView.delegate = self return textView @@ -194,7 +194,7 @@ class ReportViewController: UIViewController, NeedsDependency { }() navigationItem.title = L10n.Scene.Report.title( - "\(beReportedUser?.displayName ?? "@\(beReportedUser?.acct ?? "")")" + beReportedUser?.displayNameWithFallback ?? "" ) } diff --git a/Mastodon/Scene/Report/ReportViewModel.swift b/Mastodon/Scene/Report/ReportViewModel.swift index 94404af10..6feedc0b8 100644 --- a/Mastodon/Scene/Report/ReportViewModel.swift +++ b/Mastodon/Scene/Report/ReportViewModel.swift @@ -29,14 +29,14 @@ class ReportViewModel: NSObject, NeedsDependency { var selectedItems = [Item]() var comment: String? - internal var reportQuery: FileReportQuery - internal var disposeBag = Set() - internal let currentStep = CurrentValueSubject(.one) - internal let statusFetchedResultsController: StatusFetchedResultsController - internal var diffableDataSource: UITableViewDiffableDataSource? - internal let continueEnableSubject = CurrentValueSubject(false) - internal let sendEnableSubject = CurrentValueSubject(false) - internal let reportSuccess = PassthroughSubject() + var reportQuery: FileReportQuery + var disposeBag = Set() + let currentStep = CurrentValueSubject(.one) + let statusFetchedResultsController: StatusFetchedResultsController + var diffableDataSource: UITableViewDiffableDataSource? + let continueEnableSubject = CurrentValueSubject(false) + let sendEnableSubject = CurrentValueSubject(false) + let reportSuccess = PassthroughSubject() struct Input { let didToggleSelected: AnyPublisher From 008bb49d2d0679a9bd059d7d5ce6e9166c4701cf Mon Sep 17 00:00:00 2001 From: ihugo Date: Fri, 23 Apr 2021 09:37:18 +0800 Subject: [PATCH 07/18] fix: add selection state of report status --- Mastodon.xcodeproj/project.pbxproj | 4 - Mastodon/Diffiable/Item/Item.swift | 16 ++++ .../Diffiable/Section/ReportSection.swift | 8 +- .../Diffiable/Section/StatusSection.swift | 2 + .../Scene/Report/ReportViewModel+Data.swift | 11 +-- .../Report/ReportViewModel+Diffable.swift | 6 +- .../Report/ReportViewModel+Provider.swift | 86 ------------------- Mastodon/Scene/Report/ReportViewModel.swift | 58 +++---------- .../Report/ReportedStatusTableviewCell.swift | 5 -- .../MastodonSDK/API/Mastodon+API+Report.swift | 14 +++ 10 files changed, 54 insertions(+), 156 deletions(-) delete mode 100644 Mastodon/Scene/Report/ReportViewModel+Provider.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index d53252220..f84e8384d 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -135,7 +135,6 @@ 2DFAD5372617010500F9EE7C /* SearchingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFAD5362617010500F9EE7C /* SearchingTableViewCell.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 */; }; - 5B24BBDC262DB14800A9381B /* ReportViewModel+Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B24BBD9262DB14800A9381B /* ReportViewModel+Provider.swift */; }; 5B24BBE2262DB19100A9381B /* APIService+Report.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B24BBE1262DB19100A9381B /* APIService+Report.swift */; }; 5B8E055826319E47006E3C53 /* ReportFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8E055726319E47006E3C53 /* ReportFooterView.swift */; }; 5B90C45E262599800002E742 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C456262599800002E742 /* SettingsViewModel.swift */; }; @@ -561,7 +560,6 @@ 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 = ""; }; 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 = ""; }; - 5B24BBD9262DB14800A9381B /* ReportViewModel+Provider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ReportViewModel+Provider.swift"; sourceTree = ""; }; 5B24BBE1262DB19100A9381B /* APIService+Report.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+Report.swift"; sourceTree = ""; }; 5B8E055726319E47006E3C53 /* ReportFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportFooterView.swift; sourceTree = ""; }; 5B90C456262599800002E742 /* SettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; @@ -1249,7 +1247,6 @@ children = ( 5B24BBD7262DB14800A9381B /* ReportViewModel.swift */, 5B24BBD8262DB14800A9381B /* ReportViewModel+Diffable.swift */, - 5B24BBD9262DB14800A9381B /* ReportViewModel+Provider.swift */, 5BB04FEE262F0DCB0043BFF6 /* ReportViewModel+Data.swift */, 5BB04FD4262E7AFF0043BFF6 /* ReportViewController.swift */, 5BB04FDA262EA3070043BFF6 /* ReportHeaderView.swift */, @@ -2569,7 +2566,6 @@ 2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */, DB35FC252612FD7A006193C9 /* ProfileFieldView.swift in Sources */, DB938F0326240EA300E5B6C1 /* CachedThreadViewModel.swift in Sources */, - 5B24BBDC262DB14800A9381B /* ReportViewModel+Provider.swift in Sources */, 2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */, 2D24E12D2626FD2E00A59D4F /* NotificationViewModel+LoadOldestState.swift in Sources */, 2DB72C8C262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift in Sources */, diff --git a/Mastodon/Diffiable/Item/Item.swift b/Mastodon/Diffiable/Item/Item.swift index e169be66f..04a1262d5 100644 --- a/Mastodon/Diffiable/Item/Item.swift +++ b/Mastodon/Diffiable/Item/Item.swift @@ -32,6 +32,9 @@ enum Item { case bottomLoader case emptyStateHeader(attribute: EmptyStateHeaderAttribute) + + // reports + case reportStatus(objectID: NSManagedObjectID, attribute: ReportStatusAttribute) } extension Item { @@ -79,6 +82,15 @@ extension Item { hasher.combine(id) } } + + class ReportStatusAttribute: StatusAttribute { + var isSelected: Bool + + init(isSeparatorLineHidden: Bool = false, isSelected: Bool = false) { + self.isSelected = isSelected + super.init(isSeparatorLineHidden: isSeparatorLineHidden) + } + } } extension Item: Equatable { @@ -106,6 +118,8 @@ extension Item: Equatable { return true case (.emptyStateHeader(let attributeLeft), .emptyStateHeader(let attributeRight)): return attributeLeft == attributeRight + case (.reportStatus(let objectIDLeft, _), .reportStatus(let objectIDRight, _)): + return objectIDLeft == objectIDRight default: return false } @@ -139,6 +153,8 @@ extension Item: Hashable { hasher.combine(String(describing: Item.bottomLoader.self)) case .emptyStateHeader(let attribute): hasher.combine(attribute) + case .reportStatus(let objectID, _): + hasher.combine(objectID) } } } diff --git a/Mastodon/Diffiable/Section/ReportSection.swift b/Mastodon/Diffiable/Section/ReportSection.swift index 7567488c5..86a12a9a3 100644 --- a/Mastodon/Diffiable/Section/ReportSection.swift +++ b/Mastodon/Diffiable/Section/ReportSection.swift @@ -23,8 +23,7 @@ extension ReportSection { for tableView: UITableView, dependency: NeedsDependency, managedObjectContext: NSManagedObjectContext, - timestampUpdatePublisher: AnyPublisher, - reportdStatusDelegate: ReportedStatusTableViewCellDelegate + timestampUpdatePublisher: AnyPublisher ) -> UITableViewDiffableDataSource { UITableViewDiffableDataSource(tableView: tableView) {[ weak dependency @@ -32,7 +31,7 @@ extension ReportSection { guard let dependency = dependency else { return UITableViewCell() } switch item { - case .status(let objectID, let attribute): + case .reportStatus(let objectID, let attribute): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ReportedStatusTableViewCell.self), for: indexPath) as! ReportedStatusTableViewCell let activeMastodonAuthenticationBox = dependency.context.authenticationService.activeMastodonAuthenticationBox.value let requestUserID = activeMastodonAuthenticationBox?.userID ?? "" @@ -49,8 +48,7 @@ extension ReportSection { ) } - let isSelected = reportdStatusDelegate.reportedStatus(cell: cell, isSelected: indexPath) - cell.setupSelected(isSelected) + cell.setupSelected(attribute.isSelected) return cell default: return nil diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 4f09142a7..97e4cdf9c 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -125,6 +125,8 @@ extension StatusSection { let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineHeaderTableViewCell.self), for: indexPath) as! TimelineHeaderTableViewCell StatusSection.configureEmptyStateHeader(cell: cell, attribute: attribute) return cell + case .reportStatus: + return UITableViewCell() } } } diff --git a/Mastodon/Scene/Report/ReportViewModel+Data.swift b/Mastodon/Scene/Report/ReportViewModel+Data.swift index 4df2ccb20..005098b47 100644 --- a/Mastodon/Scene/Report/ReportViewModel+Data.swift +++ b/Mastodon/Scene/Report/ReportViewModel+Data.swift @@ -71,23 +71,24 @@ extension ReportViewModel { diffableDataSource.apply(snapshot, animatingDifferences: !items.isEmpty) } - var oldSnapshotAttributeDict: [NSManagedObjectID : Item.StatusAttribute] = [:] + var oldSnapshotAttributeDict: [NSManagedObjectID : Item.ReportStatusAttribute] = [:] let oldSnapshot = diffableDataSource.snapshot() for item in oldSnapshot.itemIdentifiers { - guard case let .status(objectID, attribute) = item else { continue } + guard case let .reportStatus(objectID, attribute) = item else { continue } oldSnapshotAttributeDict[objectID] = attribute } for objectID in objectIDs { - let attribute = oldSnapshotAttributeDict[objectID] ?? Item.StatusAttribute() - let item = Item.status(objectID: objectID, attribute: attribute) + let attribute = oldSnapshotAttributeDict[objectID] ?? Item.ReportStatusAttribute() + let item = Item.reportStatus(objectID: objectID, attribute: attribute) items.append(item) guard let status = managedObjectContext.object(with: objectID) as? Status else { continue } if status.id == self.statusId { - self.selectedItems.append(item) + attribute.isSelected = true + self.reportQuery.append(statusId: status.id) self.continueEnableSubject.send(true) } } diff --git a/Mastodon/Scene/Report/ReportViewModel+Diffable.swift b/Mastodon/Scene/Report/ReportViewModel+Diffable.swift index 38f2edb19..f737381b1 100644 --- a/Mastodon/Scene/Report/ReportViewModel+Diffable.swift +++ b/Mastodon/Scene/Report/ReportViewModel+Diffable.swift @@ -13,8 +13,7 @@ import CoreDataStack extension ReportViewModel { func setupDiffableDataSource( for tableView: UITableView, - dependency: NeedsDependency, - reportdStatusDelegate: ReportedStatusTableViewCellDelegate + dependency: NeedsDependency ) { let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common) .autoconnect() @@ -25,8 +24,7 @@ extension ReportViewModel { for: tableView, dependency: dependency, managedObjectContext: statusFetchedResultsController.fetchedResultsController.managedObjectContext, - timestampUpdatePublisher: timestampUpdatePublisher, - reportdStatusDelegate: reportdStatusDelegate + timestampUpdatePublisher: timestampUpdatePublisher ) // set empty section to make update animation top-to-bottom style diff --git a/Mastodon/Scene/Report/ReportViewModel+Provider.swift b/Mastodon/Scene/Report/ReportViewModel+Provider.swift deleted file mode 100644 index 052fc2ba1..000000000 --- a/Mastodon/Scene/Report/ReportViewModel+Provider.swift +++ /dev/null @@ -1,86 +0,0 @@ -//// -//// ReportViewModel+Provider.swift -//// Mastodon -//// -//// Created by ihugo on 2021/4/19. -//// -// -//import Combine -//import CoreData -//import CoreDataStack -//import Foundation -//import MastodonSDK -//import UIKit -//import os.log -// -//extension ReportViewController: StatusProvider { -// func status() -> Future { -// return Future { promise in promise(.success(nil)) } -// } -// -// func status(for cell: UITableViewCell?, indexPath: IndexPath?) -> Future { -// return Future { promise in -// guard let diffableDataSource = self.viewModel.diffableDataSource else { -// assertionFailure() -// promise(.success(nil)) -// return -// } -// guard let indexPath = indexPath ?? cell.flatMap({ self.tableView.indexPath(for: $0) }), -// let item = diffableDataSource.itemIdentifier(for: indexPath) else { -// promise(.success(nil)) -// return -// } -// -// switch item { -// case .status(let objectID, _): -// let managedObjectContext = self.viewModel.statusFetchedResultsController.fetchedResultsController.managedObjectContext -// managedObjectContext.perform { -// let status = managedObjectContext.object(with: objectID) as? Status -// promise(.success(status)) -// } -// default: -// promise(.success(nil)) -// } -// } -// } -// -// func status(for cell: UICollectionViewCell) -> Future { -// return Future { promise in promise(.success(nil)) } -// } -// -// var managedObjectContext: NSManagedObjectContext { -// return viewModel.statusFetchedResultsController.fetchedResultsController.managedObjectContext -// } -// -// var tableViewDiffableDataSource: UITableViewDiffableDataSource? { -// return viewModel.diffableDataSource -// } -// -// func item(for cell: UITableViewCell?, indexPath: IndexPath?) -> Item? { -// guard let diffableDataSource = self.viewModel.diffableDataSource else { -// assertionFailure() -// return nil -// } -// -// guard let indexPath = indexPath ?? cell.flatMap({ self.tableView.indexPath(for: $0) }), -// let item = diffableDataSource.itemIdentifier(for: indexPath) else { -// return nil -// } -// -// return item -// } -// -// func items(indexPaths: [IndexPath]) -> [Item] { -// guard let diffableDataSource = self.viewModel.diffableDataSource else { -// assertionFailure() -// return [] -// } -// -// var items: [Item] = [] -// for indexPath in indexPaths { -// guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { continue } -// items.append(item) -// } -// return items -// } -//} diff --git a/Mastodon/Scene/Report/ReportViewModel.swift b/Mastodon/Scene/Report/ReportViewModel.swift index 6feedc0b8..a7bba0a7e 100644 --- a/Mastodon/Scene/Report/ReportViewModel.swift +++ b/Mastodon/Scene/Report/ReportViewModel.swift @@ -26,8 +26,6 @@ class ReportViewModel: NSObject, NeedsDependency { weak var coordinator: SceneCoordinator! { willSet { precondition(coordinator == nil) } } var userId: String var statusId: String? - var selectedItems = [Item]() - var comment: String? var reportQuery: FileReportQuery var disposeBag = Set() @@ -74,7 +72,7 @@ class ReportViewModel: NSObject, NeedsDependency { self.reportQuery = FileReportQuery( accountId: userId, - statusIds: nil, + statusIds: [], comment: nil, forward: nil ) @@ -90,8 +88,7 @@ class ReportViewModel: NSObject, NeedsDependency { setupDiffableDataSource( for: input.tableView, - dependency: self, - reportdStatusDelegate: self + dependency: self ) // data binding @@ -125,38 +122,31 @@ class ReportViewModel: NSObject, NeedsDependency { func bindData(input: Input) { input.didToggleSelected.sink { [weak self] (item) in guard let self = self else { return } - guard case let .status(objectID, attribute) = item else { return } + guard case let .reportStatus(objectID, attribute) = item else { return } guard var snapshot = self.diffableDataSource?.snapshot() else { return } let managedObjectContext = self.statusFetchedResultsController.fetchedResultsController.managedObjectContext guard let status = managedObjectContext.object(with: objectID) as? Status else { return } - var items = [Item]() - if let index = self.selectedItems.firstIndex(of: item) { - self.selectedItems.remove(at: index) - items.append(.status(objectID: objectID, attribute: attribute)) - - if let index = self.reportQuery.statusIds?.firstIndex(of: status.id) { - self.reportQuery.statusIds?.remove(at: index) - } + attribute.isSelected = !attribute.isSelected + if attribute.isSelected { + self.reportQuery.append(statusId: status.id) } else { - self.selectedItems.append(item) - items.append(.status(objectID: objectID, attribute: attribute)) - self.reportQuery.statusIds?.append(status.id) + self.reportQuery.remove(statusId: status.id) } snapshot.reloadItems([item]) self.diffableDataSource?.apply(snapshot, animatingDifferences: false) - let continueEnable = self.selectedItems.count > 0 + let continueEnable = (self.reportQuery.statusIds?.count ?? 0) > 0 self.continueEnableSubject.send(continueEnable) } .store(in: &disposeBag) input.comment.assign( to: \.comment, - on: self + on: self.reportQuery ) .store(in: &disposeBag) input.comment.sink { [weak self] (comment) in @@ -170,7 +160,7 @@ class ReportViewModel: NSObject, NeedsDependency { func bindForStep1(input: Input) { let skip = input.step1Skip.map { [weak self] value -> Void in guard let self = self else { return value } - self.selectedItems.removeAll() + self.reportQuery.statusIds?.removeAll() return value } @@ -185,29 +175,13 @@ class ReportViewModel: NSObject, NeedsDependency { func bindForStep2(input: Input, domain: String, activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox) { let skip = input.step2Skip.map { [weak self] value -> Void in guard let self = self else { return value } - self.comment = nil + self.reportQuery.comment = nil return value } Publishers.Merge(skip, input.step2Continue) .sink { [weak self] _ in guard let self = self else { return } - let managedObjectContext = self.statusFetchedResultsController.fetchedResultsController.managedObjectContext - - self.reportQuery.comment = self.comment - - var selectedStatusIds = [String]() - self.selectedItems.forEach { (item) in - guard case .status(let objectId, _) = item else { - return - } - guard let status = managedObjectContext.object(with: objectId) as? Status else { - return - } - selectedStatusIds.append(status.id) - } - self.reportQuery.statusIds = selectedStatusIds - self.context.apiService.report( domain: domain, query: self.reportQuery, @@ -237,13 +211,3 @@ class ReportViewModel: NSObject, NeedsDependency { .store(in: &disposeBag) } } - -extension ReportViewModel: ReportedStatusTableViewCellDelegate { - func reportedStatus(cell: ReportedStatusTableViewCell, isSelected indexPath: IndexPath) -> Bool { - guard let item = diffableDataSource?.itemIdentifier(for: indexPath) else { - return false - } - - return selectedItems.contains(item) - } -} diff --git a/Mastodon/Scene/Report/ReportedStatusTableviewCell.swift b/Mastodon/Scene/Report/ReportedStatusTableviewCell.swift index a25c8b507..8329124cc 100644 --- a/Mastodon/Scene/Report/ReportedStatusTableviewCell.swift +++ b/Mastodon/Scene/Report/ReportedStatusTableviewCell.swift @@ -13,15 +13,10 @@ import CoreData import CoreDataStack import ActiveLabel -protocol ReportedStatusTableViewCellDelegate: class { - func reportedStatus(cell: ReportedStatusTableViewCell, isSelected indexPath: IndexPath) -> Bool -} - final class ReportedStatusTableViewCell: UITableViewCell, StatusCell { static let bottomPaddingHeight: CGFloat = 10 - weak var delegate: ReportedStatusTableViewCellDelegate? var disposeBag = Set() var pollCountdownSubscription: AnyCancellable? var observations = Set() diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Report.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Report.swift index a0afabb2b..17bcd5331 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Report.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Report.swift @@ -75,5 +75,19 @@ public extension Mastodon.API.Reports { self.comment = comment self.forward = forward } + + public func append(statusId: String) { + guard self.statusIds?.contains(statusId) != true else { return } + if self.statusIds == nil { + self.statusIds = [] + } + + self.statusIds?.append(statusId) + } + + public func remove(statusId: String) { + guard let index = self.statusIds?.firstIndex(of: statusId) else { return } + self.statusIds?.remove(at: index) + } } } From 479f21fd9f3ae77f1763266e712a1195c4ef6c16 Mon Sep 17 00:00:00 2001 From: ihugo Date: Fri, 23 Apr 2021 09:52:22 +0800 Subject: [PATCH 08/18] chore: remove secondary and rename elevated color --- Mastodon/Generated/Assets.swift | 3 +- .../secondary.colorset/Contents.json | 38 ------------------- .../Contents.json | 12 +++--- .../Contents.json | 12 +++--- .../Contents.json | 6 +-- Mastodon/Scene/Report/ReportFooterView.swift | 2 +- Mastodon/Scene/Report/ReportHeaderView.swift | 2 +- .../Scene/Report/ReportViewController.swift | 2 +- 8 files changed, 19 insertions(+), 58 deletions(-) delete mode 100644 Mastodon/Resources/Assets.xcassets/Colors/Background/secondary.colorset/Contents.json rename Mastodon/Resources/Assets.xcassets/Colors/Background/{elevatedPrimary.colorset => system.elevated.background.colorset}/Contents.json (88%) diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift index aba5e4f6b..911544485 100644 --- a/Mastodon/Generated/Assets.swift +++ b/Mastodon/Generated/Assets.swift @@ -41,14 +41,13 @@ internal enum Asset { internal static let alertYellow = ColorAsset(name: "Colors/Background/alert.yellow") internal static let dangerBorder = ColorAsset(name: "Colors/Background/danger.border") internal static let danger = ColorAsset(name: "Colors/Background/danger") - internal static let elevatedPrimary = ColorAsset(name: "Colors/Background/elevatedPrimary") internal static let mediaTypeIndicotor = ColorAsset(name: "Colors/Background/media.type.indicotor") internal static let navigationBar = ColorAsset(name: "Colors/Background/navigationBar") internal static let onboardingBackground = ColorAsset(name: "Colors/Background/onboarding.background") - internal static let secondary = ColorAsset(name: "Colors/Background/secondary") internal static let secondaryGroupedSystemBackground = ColorAsset(name: "Colors/Background/secondary.grouped.system.background") internal static let secondarySystemBackground = ColorAsset(name: "Colors/Background/secondary.system.background") internal static let systemBackground = ColorAsset(name: "Colors/Background/system.background") + internal static let systemElevatedBackground = ColorAsset(name: "Colors/Background/system.elevated.background") internal static let systemGroupedBackground = ColorAsset(name: "Colors/Background/system.grouped.background") internal static let tertiarySystemBackground = ColorAsset(name: "Colors/Background/tertiary.system.background") internal static let tertiarySystemGroupedBackground = ColorAsset(name: "Colors/Background/tertiary.system.grouped.background") diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/secondary.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/secondary.colorset/Contents.json deleted file mode 100644 index 5e7067405..000000000 --- a/Mastodon/Resources/Assets.xcassets/Colors/Background/secondary.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "254", - "green" : "255", - "red" : "254" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "46", - "green" : "44", - "red" : "44" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/secondary.grouped.system.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/secondary.grouped.system.background.colorset/Contents.json index 55f84c267..5e7067405 100644 --- a/Mastodon/Resources/Assets.xcassets/Colors/Background/secondary.grouped.system.background.colorset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Colors/Background/secondary.grouped.system.background.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0xFE", - "green" : "0xFF", - "red" : "0xFE" + "blue" : "254", + "green" : "255", + "red" : "254" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0x2E", - "green" : "0x2C", - "red" : "0x2C" + "blue" : "46", + "green" : "44", + "red" : "44" } }, "idiom" : "universal" diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/secondary.system.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/secondary.system.background.colorset/Contents.json index bd6f07f25..82abbf254 100644 --- a/Mastodon/Resources/Assets.xcassets/Colors/Background/secondary.system.background.colorset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Colors/Background/secondary.system.background.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0xE8", - "green" : "0xE1", - "red" : "0xD9" + "blue" : "232", + "green" : "225", + "red" : "217" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0x2E", - "green" : "0x2C", - "red" : "0x2C" + "blue" : "46", + "green" : "44", + "red" : "44" } }, "idiom" : "universal" diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/elevatedPrimary.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/system.elevated.background.colorset/Contents.json similarity index 88% rename from Mastodon/Resources/Assets.xcassets/Colors/Background/elevatedPrimary.colorset/Contents.json rename to Mastodon/Resources/Assets.xcassets/Colors/Background/system.elevated.background.colorset/Contents.json index 82edd034b..e13fb4690 100644 --- a/Mastodon/Resources/Assets.xcassets/Colors/Background/elevatedPrimary.colorset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Colors/Background/system.elevated.background.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "30", - "green" : "28", - "red" : "28" + "blue" : "0x1E", + "green" : "0x1C", + "red" : "0x1C" } }, "idiom" : "universal" diff --git a/Mastodon/Scene/Report/ReportFooterView.swift b/Mastodon/Scene/Report/ReportFooterView.swift index 16948f58e..a64a556d3 100644 --- a/Mastodon/Scene/Report/ReportFooterView.swift +++ b/Mastodon/Scene/Report/ReportFooterView.swift @@ -53,7 +53,7 @@ final class ReportFooterView: UIView { override init(frame: CGRect) { super.init(frame: frame) - self.backgroundColor = Asset.Colors.Background.elevatedPrimary.color + self.backgroundColor = Asset.Colors.Background.systemElevatedBackground.color stackview.addArrangedSubview(nextStepButton) stackview.addArrangedSubview(skipButton) diff --git a/Mastodon/Scene/Report/ReportHeaderView.swift b/Mastodon/Scene/Report/ReportHeaderView.swift index bace3ada5..414196e86 100644 --- a/Mastodon/Scene/Report/ReportHeaderView.swift +++ b/Mastodon/Scene/Report/ReportHeaderView.swift @@ -63,7 +63,7 @@ final class ReportHeaderView: UIView { override init(frame: CGRect) { super.init(frame: frame) - self.backgroundColor = Asset.Colors.Background.elevatedPrimary.color + self.backgroundColor = Asset.Colors.Background.systemElevatedBackground.color stackview.addArrangedSubview(titleLabel) stackview.addArrangedSubview(contentLabel) addSubview(stackview) diff --git a/Mastodon/Scene/Report/ReportViewController.swift b/Mastodon/Scene/Report/ReportViewController.swift index b00e4a3ee..14eed14ca 100644 --- a/Mastodon/Scene/Report/ReportViewController.swift +++ b/Mastodon/Scene/Report/ReportViewController.swift @@ -46,7 +46,7 @@ class ReportViewController: UIViewController, NeedsDependency { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false view.setContentHuggingPriority(.defaultLow, for: .vertical) - view.backgroundColor = Asset.Colors.Background.elevatedPrimary.color + view.backgroundColor = Asset.Colors.Background.systemElevatedBackground.color return view }() From cbc828eec2a5ae13797f7daf9fe17684fc1e71c7 Mon Sep 17 00:00:00 2001 From: ihugo Date: Sun, 25 Apr 2021 15:36:40 +0800 Subject: [PATCH 09/18] refactor: remove UI part from ReportViewmodel --- ...meTimelineViewController+DebugAction.swift | 1 - .../Scene/Report/ReportViewController.swift | 36 ++++++++-- Mastodon/Scene/Report/ReportViewModel.swift | 65 +++++++------------ .../MastodonSDK/API/Mastodon+API+Report.swift | 17 ++++- 4 files changed, 67 insertions(+), 52 deletions(-) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift index fca20c92b..eaad67b7c 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift @@ -362,7 +362,6 @@ extension HomeTimelineViewController { // 106093402888557459 let viewModel = ReportViewModel( context: self.context, - coordinator: self.coordinator, domain: authenticationBox.domain, userId: userId, statusId: statusId diff --git a/Mastodon/Scene/Report/ReportViewController.swift b/Mastodon/Scene/Report/ReportViewController.swift index 14eed14ca..0bdd72bc9 100644 --- a/Mastodon/Scene/Report/ReportViewController.swift +++ b/Mastodon/Scene/Report/ReportViewController.swift @@ -12,6 +12,7 @@ import CoreDataStack import os.log import UIKit import TwitterTextEditor +import MastodonSDK class ReportViewController: UIViewController, NeedsDependency { static let kAnimationDuration: TimeInterval = 0.33 @@ -84,6 +85,12 @@ class ReportViewController: UIViewController, NeedsDependency { super.viewDidLoad() setupView() + + viewModel.setupDiffableDataSource( + for: tableView, + dependency: self + ) + bindViewModel() bindActions() } @@ -127,8 +134,7 @@ class ReportViewController: UIViewController, NeedsDependency { step1Skip: step1Skip.eraseToAnyPublisher(), step2Continue: step2Continue.eraseToAnyPublisher(), step2Skip: step2Skip.eraseToAnyPublisher(), - cancel: cancel.eraseToAnyPublisher(), - tableView: tableView + cancel: cancel.eraseToAnyPublisher() ) let output = viewModel.transform(input: input) output?.currentStep @@ -161,12 +167,28 @@ class ReportViewController: UIViewController, NeedsDependency { .assign(to: \.nextStepButton.isEnabled, on: footer) .store(in: &disposeBag) - output?.reportSuccess + output?.reportResult + .print() .receive(on: DispatchQueue.main) - .sink(receiveValue: { [weak self] (_) in - self?.dismiss(animated: true, completion: nil) - }) - .store(in: &disposeBag) + .sink(receiveCompletion: { _ in + }, receiveValue: { [weak self] data in + let (success, error) = data + if success { + self?.dismiss(animated: true, completion: nil) + } else if let error = error { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fail to file a report : %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + + let alertController = UIAlertController(for: error, title: nil, preferredStyle: .alert) + let okAction = UIAlertAction(title: "OK", style: .default, handler: nil) + alertController.addAction(okAction) + self?.coordinator.present( + scene: .alertController(alertController: alertController), + from: nil, + transition: .alertController(animated: true, completion: nil) + ) + } + }) + .store(in: &disposeBag) } private func setupNavigation() { diff --git a/Mastodon/Scene/Report/ReportViewModel.swift b/Mastodon/Scene/Report/ReportViewModel.swift index a7bba0a7e..8ee5a44a9 100644 --- a/Mastodon/Scene/Report/ReportViewModel.swift +++ b/Mastodon/Scene/Report/ReportViewModel.swift @@ -13,7 +13,7 @@ import MastodonSDK import UIKit import os.log -class ReportViewModel: NSObject, NeedsDependency { +class ReportViewModel: NSObject { typealias FileReportQuery = Mastodon.API.Reports.FileReportQuery enum Step: Int { @@ -23,7 +23,6 @@ class ReportViewModel: NSObject, NeedsDependency { // confirm set only once weak var context: AppContext! { willSet { precondition(context == nil) } } - weak var coordinator: SceneCoordinator! { willSet { precondition(coordinator == nil) } } var userId: String var statusId: String? @@ -34,7 +33,6 @@ class ReportViewModel: NSObject, NeedsDependency { var diffableDataSource: UITableViewDiffableDataSource? let continueEnableSubject = CurrentValueSubject(false) let sendEnableSubject = CurrentValueSubject(false) - let reportSuccess = PassthroughSubject() struct Input { let didToggleSelected: AnyPublisher @@ -44,24 +42,21 @@ class ReportViewModel: NSObject, NeedsDependency { let step2Continue: AnyPublisher let step2Skip: AnyPublisher let cancel: AnyPublisher - let tableView: UITableView } struct Output { let currentStep: AnyPublisher let continueEnableSubject: AnyPublisher let sendEnableSubject: AnyPublisher - let reportSuccess: AnyPublisher + let reportResult: AnyPublisher<(Bool, Error?), Never> } init(context: AppContext, - coordinator: SceneCoordinator, domain: String, userId: String, statusId: String? ) { self.context = context - self.coordinator = coordinator self.userId = userId self.statusId = statusId self.statusFetchedResultsController = StatusFetchedResultsController( @@ -86,17 +81,12 @@ class ReportViewModel: NSObject, NeedsDependency { } let domain = activeMastodonAuthenticationBox.domain - setupDiffableDataSource( - for: input.tableView, - dependency: self - ) - // data binding bindData(input: input) // step1 and step2 binding bindForStep1(input: input) - bindForStep2( + let reportResult = bindForStep2( input: input, domain: domain, activeMastodonAuthenticationBox: activeMastodonAuthenticationBox @@ -114,7 +104,7 @@ class ReportViewModel: NSObject, NeedsDependency { currentStep: currentStep.eraseToAnyPublisher(), continueEnableSubject: continueEnableSubject.eraseToAnyPublisher(), sendEnableSubject: sendEnableSubject.eraseToAnyPublisher(), - reportSuccess: reportSuccess.eraseToAnyPublisher() + reportResult: reportResult ) } @@ -172,42 +162,35 @@ class ReportViewModel: NSObject, NeedsDependency { .store(in: &disposeBag) } - func bindForStep2(input: Input, domain: String, activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox) { + func bindForStep2(input: Input, domain: String, activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox) -> AnyPublisher<(Bool, Error?), Never> { let skip = input.step2Skip.map { [weak self] value -> Void in guard let self = self else { return value } self.reportQuery.comment = nil return value } - - Publishers.Merge(skip, input.step2Continue) - .sink { [weak self] _ in - guard let self = self else { return } - self.context.apiService.report( + + return Publishers.Merge(skip, input.step2Continue) + .flatMap { [weak self] (_) -> AnyPublisher<(Bool, Error?), Never> in + guard let self = self else { + return Empty(completeImmediately: true).eraseToAnyPublisher() + } + + return self.context.apiService.report( domain: domain, query: self.reportQuery, mastodonAuthenticationBox: activeMastodonAuthenticationBox ) - .sink { [weak self](data) in - switch data { - case .failure(let error): - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fail to file a report : %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) - - let alertController = UIAlertController(for: error, title: nil, preferredStyle: .alert) - let okAction = UIAlertAction(title: "OK", style: .default, handler: nil) - alertController.addAction(okAction) - self?.coordinator.present( - scene: .alertController(alertController: alertController), - from: nil, - transition: .alertController(animated: true, completion: nil) - ) - case .finished: - self?.reportSuccess.send() - } - - } receiveValue: { (data) in - } - .store(in: &self.disposeBag) + .map({ (content) -> (Bool, Error?) in + return (true, nil) + }) + .eraseToAnyPublisher() + .tryCatch({ (error) -> AnyPublisher<(Bool, Error?), Never> in + return Just((false, error)).eraseToAnyPublisher() + }) + // to covert to AnyPublisher<(Bool, Error?), Never> + .replaceError(with: (false, nil)) + .eraseToAnyPublisher() } - .store(in: &disposeBag) + .eraseToAnyPublisher() } } diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Report.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Report.swift index 17bcd5331..1c63f744f 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Report.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Report.swift @@ -7,6 +7,7 @@ import Combine import Foundation +import enum NIOHTTP1.HTTPResponseStatus extension Mastodon.API.Reports { static func reportsEndpointURL(domain: String) -> URL { @@ -39,13 +40,23 @@ extension Mastodon.API.Reports { ) return session.dataTaskPublisher(for: request) .tryMap { data, response in - if let response = response as? HTTPURLResponse { + guard let response = response as? HTTPURLResponse else { + assertionFailure() + throw NSError() + } + + if response.statusCode == 200 { return Mastodon.Response.Content( - value: response.statusCode == 200, + value: true, response: response ) + } else { + let httpResponseStatus = HTTPResponseStatus(statusCode: response.statusCode) + throw Mastodon.API.Error( + httpResponseStatus: httpResponseStatus, + mastodonError: nil + ) } - return Mastodon.Response.Content(value: false, response: response) } .eraseToAnyPublisher() } From 85014802c441ef88645a3633a21f75d9c07b3748 Mon Sep 17 00:00:00 2001 From: ihugo Date: Sun, 25 Apr 2021 15:48:37 +0800 Subject: [PATCH 10/18] style: rename `Id` to `ID` --- .../Scene/Report/ReportViewModel+Data.swift | 2 +- Mastodon/Scene/Report/ReportViewModel.swift | 12 +++--- .../MastodonSDK/API/Mastodon+API+Report.swift | 37 ++++++++++--------- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/Mastodon/Scene/Report/ReportViewModel+Data.swift b/Mastodon/Scene/Report/ReportViewModel+Data.swift index 005098b47..4dde72528 100644 --- a/Mastodon/Scene/Report/ReportViewModel+Data.swift +++ b/Mastodon/Scene/Report/ReportViewModel+Data.swift @@ -88,7 +88,7 @@ extension ReportViewModel { } if status.id == self.statusId { attribute.isSelected = true - self.reportQuery.append(statusId: status.id) + self.reportQuery.append(statusID: status.id) self.continueEnableSubject.send(true) } } diff --git a/Mastodon/Scene/Report/ReportViewModel.swift b/Mastodon/Scene/Report/ReportViewModel.swift index 8ee5a44a9..e1de26039 100644 --- a/Mastodon/Scene/Report/ReportViewModel.swift +++ b/Mastodon/Scene/Report/ReportViewModel.swift @@ -66,8 +66,8 @@ class ReportViewModel: NSObject { ) self.reportQuery = FileReportQuery( - accountId: userId, - statusIds: [], + accountID: userId, + statusIDs: [], comment: nil, forward: nil ) @@ -121,15 +121,15 @@ class ReportViewModel: NSObject { attribute.isSelected = !attribute.isSelected if attribute.isSelected { - self.reportQuery.append(statusId: status.id) + self.reportQuery.append(statusID: status.id) } else { - self.reportQuery.remove(statusId: status.id) + self.reportQuery.remove(statusID: status.id) } snapshot.reloadItems([item]) self.diffableDataSource?.apply(snapshot, animatingDifferences: false) - let continueEnable = (self.reportQuery.statusIds?.count ?? 0) > 0 + let continueEnable = (self.reportQuery.statusIDs?.count ?? 0) > 0 self.continueEnableSubject.send(continueEnable) } .store(in: &disposeBag) @@ -150,7 +150,7 @@ class ReportViewModel: NSObject { func bindForStep1(input: Input) { let skip = input.step1Skip.map { [weak self] value -> Void in guard let self = self else { return value } - self.reportQuery.statusIds?.removeAll() + self.reportQuery.statusIDs?.removeAll() return value } diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Report.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Report.swift index 1c63f744f..fff746174 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Report.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Report.swift @@ -65,40 +65,41 @@ extension Mastodon.API.Reports { public extension Mastodon.API.Reports { class FileReportQuery: Codable, PostQuery { - public let accountId: String - public var statusIds: [String]? + public let accountID: Mastodon.Entity.Account.ID + public var statusIDs: [Mastodon.Entity.Status.ID]? public var comment: String? public let forward: Bool? enum CodingKeys: String, CodingKey { - case accountId = "account_id" - case statusIds = "status_ids" + case accountID = "account_id" + case statusIDs = "status_ids" case comment case forward } - public init(accountId: String, - statusIds: [String]?, - comment: String?, - forward: Bool?) { - self.accountId = accountId - self.statusIds = statusIds + public init( + accountID: Mastodon.Entity.Account.ID, + statusIDs: [Mastodon.Entity.Status.ID]?, + comment: String?, + forward: Bool?) { + self.accountID = accountID + self.statusIDs = statusIDs self.comment = comment self.forward = forward } - public func append(statusId: String) { - guard self.statusIds?.contains(statusId) != true else { return } - if self.statusIds == nil { - self.statusIds = [] + public func append(statusID: Mastodon.Entity.Status.ID) { + guard self.statusIDs?.contains(statusID) != true else { return } + if self.statusIDs == nil { + self.statusIDs = [] } - self.statusIds?.append(statusId) + self.statusIDs?.append(statusID) } - public func remove(statusId: String) { - guard let index = self.statusIds?.firstIndex(of: statusId) else { return } - self.statusIds?.remove(at: index) + public func remove(statusID: String) { + guard let index = self.statusIDs?.firstIndex(of: statusID) else { return } + self.statusIDs?.remove(at: index) } } } From db48e56dd9a07ee287ee499e665a9a22b35a19f2 Mon Sep 17 00:00:00 2001 From: ihugo Date: Sun, 25 Apr 2021 15:51:45 +0800 Subject: [PATCH 11/18] refactor: remove negative constraints --- Mastodon/Scene/Report/ReportHeaderView.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Mastodon/Scene/Report/ReportHeaderView.swift b/Mastodon/Scene/Report/ReportHeaderView.swift index 414196e86..8a6d957c8 100644 --- a/Mastodon/Scene/Report/ReportHeaderView.swift +++ b/Mastodon/Scene/Report/ReportHeaderView.swift @@ -70,7 +70,7 @@ final class ReportHeaderView: UIView { stackview.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - stackview.safeAreaLayoutGuide.topAnchor.constraint( + stackview.topAnchor.constraint( equalTo: self.topAnchor, constant: ReportView.verticalMargin ), @@ -78,13 +78,13 @@ final class ReportHeaderView: UIView { equalTo: self.readableContentGuide.leadingAnchor, constant: ReportView.horizontalMargin ), - stackview.bottomAnchor.constraint( - equalTo: self.bottomAnchor, - constant: -1 * ReportView.verticalMargin + self.bottomAnchor.constraint( + equalTo: stackview.bottomAnchor, + constant: ReportView.verticalMargin ), - stackview.trailingAnchor.constraint( - equalTo: self.readableContentGuide.trailingAnchor, - constant: -1 * ReportView.horizontalMargin + self.readableContentGuide.trailingAnchor.constraint( + equalTo: stackview.trailingAnchor, + constant: ReportView.horizontalMargin ) ]) } From e9d015720b62b64794b64c6b33531b785f013356 Mon Sep 17 00:00:00 2001 From: ihugo Date: Sun, 25 Apr 2021 18:28:47 +0800 Subject: [PATCH 12/18] refactor: use `tableView.allowsMultipleSelection` --- .../Diffiable/Section/ReportSection.swift | 8 +++++- .../Scene/Report/ReportViewController.swift | 9 ++++++- Mastodon/Scene/Report/ReportViewModel.swift | 4 --- .../Report/ReportedStatusTableviewCell.swift | 25 +++++++++---------- 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/Mastodon/Diffiable/Section/ReportSection.swift b/Mastodon/Diffiable/Section/ReportSection.swift index 86a12a9a3..eccf09d26 100644 --- a/Mastodon/Diffiable/Section/ReportSection.swift +++ b/Mastodon/Diffiable/Section/ReportSection.swift @@ -48,7 +48,13 @@ extension ReportSection { ) } - cell.setupSelected(attribute.isSelected) + // defalut to select the report status + if attribute.isSelected { + tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none) + } else { + tableView.deselectRow(at: indexPath, animated: false) + } + return cell default: return nil diff --git a/Mastodon/Scene/Report/ReportViewController.swift b/Mastodon/Scene/Report/ReportViewController.swift index 0bdd72bc9..487efa124 100644 --- a/Mastodon/Scene/Report/ReportViewController.swift +++ b/Mastodon/Scene/Report/ReportViewController.swift @@ -68,6 +68,7 @@ class ReportViewController: UIViewController, NeedsDependency { tableView.backgroundColor = .clear tableView.translatesAutoresizingMaskIntoConstraints = false tableView.delegate = self + tableView.allowsMultipleSelection = true return tableView }() @@ -277,7 +278,13 @@ extension ReportViewController: UITableViewDelegate { guard let item = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { return } - + didToggleSelected.send(item) + } + + func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) { + guard let item = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { + return + } didToggleSelected.send(item) } } diff --git a/Mastodon/Scene/Report/ReportViewModel.swift b/Mastodon/Scene/Report/ReportViewModel.swift index e1de26039..26c0f2f2a 100644 --- a/Mastodon/Scene/Report/ReportViewModel.swift +++ b/Mastodon/Scene/Report/ReportViewModel.swift @@ -113,7 +113,6 @@ class ReportViewModel: NSObject { input.didToggleSelected.sink { [weak self] (item) in guard let self = self else { return } guard case let .reportStatus(objectID, attribute) = item else { return } - guard var snapshot = self.diffableDataSource?.snapshot() else { return } let managedObjectContext = self.statusFetchedResultsController.fetchedResultsController.managedObjectContext guard let status = managedObjectContext.object(with: objectID) as? Status else { return @@ -126,9 +125,6 @@ class ReportViewModel: NSObject { self.reportQuery.remove(statusID: status.id) } - snapshot.reloadItems([item]) - self.diffableDataSource?.apply(snapshot, animatingDifferences: false) - let continueEnable = (self.reportQuery.statusIDs?.count ?? 0) > 0 self.continueEnableSubject.send(continueEnable) } diff --git a/Mastodon/Scene/Report/ReportedStatusTableviewCell.swift b/Mastodon/Scene/Report/ReportedStatusTableviewCell.swift index 8329124cc..0c63398d1 100644 --- a/Mastodon/Scene/Report/ReportedStatusTableviewCell.swift +++ b/Mastodon/Scene/Report/ReportedStatusTableviewCell.swift @@ -20,7 +20,6 @@ final class ReportedStatusTableViewCell: UITableViewCell, StatusCell { var disposeBag = Set() var pollCountdownSubscription: AnyCancellable? var observations = Set() - var checked: Bool = false let statusView = StatusView() let separatorLine = UIView.separatorLine @@ -42,7 +41,6 @@ final class ReportedStatusTableViewCell: UITableViewCell, StatusCell { override func prepareForReuse() { super.prepareForReuse() - checked = false statusView.updateContentWarningDisplay(isHidden: true, animated: false) statusView.statusMosaicImageViewContainer.contentWarningOverlayView.isUserInteractionEnabled = true statusView.pollTableView.dataSource = nil @@ -75,11 +73,22 @@ final class ReportedStatusTableViewCell: UITableViewCell, StatusCell { if highlighted { checkbox.image = UIImage(systemName: "checkmark.circle.fill") checkbox.tintColor = Asset.Colors.Label.highlight.color - } else if !checked { + } else if !isSelected { checkbox.image = UIImage(systemName: "circle") checkbox.tintColor = Asset.Colors.Label.secondary.color } } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + if isSelected { + checkbox.image = UIImage(systemName: "checkmark.circle.fill") + } else { + checkbox.image = UIImage(systemName: "circle") + } + checkbox.tintColor = Asset.Colors.Label.secondary.color + } } extension ReportedStatusTableViewCell { @@ -127,16 +136,6 @@ extension ReportedStatusTableViewCell { resetSeparatorLineLayout() } - - func setupSelected(_ selected: Bool) { - checked = selected - if selected { - checkbox.image = UIImage(systemName: "checkmark.circle.fill") - } else { - checkbox.image = UIImage(systemName: "circle") - } - checkbox.tintColor = Asset.Colors.Label.secondary.color - } } extension ReportedStatusTableViewCell { From 3f62272162e3b5ac28511b0f1556a92609d861d1 Mon Sep 17 00:00:00 2001 From: ihugo Date: Sun, 25 Apr 2021 18:45:17 +0800 Subject: [PATCH 13/18] fix: lost comment if send without comment first --- .../Scene/Report/ReportViewModel+Data.swift | 2 +- Mastodon/Scene/Report/ReportViewModel.swift | 37 ++++++++++++++++--- .../MastodonSDK/API/Mastodon+API+Report.swift | 14 ------- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/Mastodon/Scene/Report/ReportViewModel+Data.swift b/Mastodon/Scene/Report/ReportViewModel+Data.swift index 4dde72528..dcb715fba 100644 --- a/Mastodon/Scene/Report/ReportViewModel+Data.swift +++ b/Mastodon/Scene/Report/ReportViewModel+Data.swift @@ -88,7 +88,7 @@ extension ReportViewModel { } if status.id == self.statusId { attribute.isSelected = true - self.reportQuery.append(statusID: status.id) + self.append(statusID: status.id) self.continueEnableSubject.send(true) } } diff --git a/Mastodon/Scene/Report/ReportViewModel.swift b/Mastodon/Scene/Report/ReportViewModel.swift index 26c0f2f2a..4864145f5 100644 --- a/Mastodon/Scene/Report/ReportViewModel.swift +++ b/Mastodon/Scene/Report/ReportViewModel.swift @@ -26,6 +26,9 @@ class ReportViewModel: NSObject { var userId: String var statusId: String? + var statusIDs = [Mastodon.Entity.Status.ID]() + var comment: String? + var reportQuery: FileReportQuery var disposeBag = Set() let currentStep = CurrentValueSubject(.one) @@ -120,19 +123,19 @@ class ReportViewModel: NSObject { attribute.isSelected = !attribute.isSelected if attribute.isSelected { - self.reportQuery.append(statusID: status.id) + self.append(statusID: status.id) } else { - self.reportQuery.remove(statusID: status.id) + self.remove(statusID: status.id) } - let continueEnable = (self.reportQuery.statusIDs?.count ?? 0) > 0 + let continueEnable = self.statusIDs.count > 0 self.continueEnableSubject.send(continueEnable) } .store(in: &disposeBag) input.comment.assign( to: \.comment, - on: self.reportQuery + on: self ) .store(in: &disposeBag) input.comment.sink { [weak self] (comment) in @@ -150,7 +153,13 @@ class ReportViewModel: NSObject { return value } - Publishers.Merge(skip, input.step1Continue) + let step1Continue = input.step1Continue.map { [weak self] value -> Void in + guard let self = self else { return value } + self.reportQuery.statusIDs = self.statusIDs + return value + } + + Publishers.Merge(skip, step1Continue) .sink { [weak self] _ in self?.currentStep.value = .two self?.sendEnableSubject.send(false) @@ -164,8 +173,14 @@ class ReportViewModel: NSObject { self.reportQuery.comment = nil return value } + + let step2Continue = input.step2Continue.map { [weak self] value -> Void in + guard let self = self else { return value } + self.reportQuery.comment = self.comment + return value + } - return Publishers.Merge(skip, input.step2Continue) + return Publishers.Merge(skip, step2Continue) .flatMap { [weak self] (_) -> AnyPublisher<(Bool, Error?), Never> in guard let self = self else { return Empty(completeImmediately: true).eraseToAnyPublisher() @@ -189,4 +204,14 @@ class ReportViewModel: NSObject { } .eraseToAnyPublisher() } + + func append(statusID: Mastodon.Entity.Status.ID) { + guard self.statusIDs.contains(statusID) != true else { return } + self.statusIDs.append(statusID) + } + + func remove(statusID: String) { + guard let index = self.statusIDs.firstIndex(of: statusID) else { return } + self.statusIDs.remove(at: index) + } } diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Report.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Report.swift index fff746174..6ba8c3cf5 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Report.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Report.swift @@ -87,19 +87,5 @@ public extension Mastodon.API.Reports { self.comment = comment self.forward = forward } - - public func append(statusID: Mastodon.Entity.Status.ID) { - guard self.statusIDs?.contains(statusID) != true else { return } - if self.statusIDs == nil { - self.statusIDs = [] - } - - self.statusIDs?.append(statusID) - } - - public func remove(statusID: String) { - guard let index = self.statusIDs?.firstIndex(of: statusID) else { return } - self.statusIDs?.remove(at: index) - } } } From 1356e3755f486aba5fb912a15bc1b863dd3c3b46 Mon Sep 17 00:00:00 2001 From: ihugo Date: Sun, 25 Apr 2021 19:49:49 +0800 Subject: [PATCH 14/18] fix: make the keyboard to be laid out properly --- .../Scene/Report/ReportViewController.swift | 45 +++++++++++++++++-- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/Mastodon/Scene/Report/ReportViewController.swift b/Mastodon/Scene/Report/ReportViewController.swift index 487efa124..9cf506a30 100644 --- a/Mastodon/Scene/Report/ReportViewController.swift +++ b/Mastodon/Scene/Report/ReportViewController.swift @@ -79,9 +79,19 @@ class ReportViewController: UIViewController, NeedsDependency { textView.placeholder = L10n.Scene.Report.textPlaceholder textView.backgroundColor = .clear textView.delegate = self + textView.isScrollEnabled = true + textView.keyboardDismissMode = .onDrag return textView }() + lazy var bottomSpacing: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + var bottomConstraint: NSLayoutConstraint! + override func viewDidLoad() { super.viewDidLoad() @@ -104,6 +114,7 @@ class ReportViewController: UIViewController, NeedsDependency { stackview.addArrangedSubview(header) stackview.addArrangedSubview(contentView) stackview.addArrangedSubview(footer) + stackview.addArrangedSubview(bottomSpacing) contentView.addSubview(tableView) @@ -116,9 +127,12 @@ class ReportViewController: UIViewController, NeedsDependency { tableView.topAnchor.constraint(equalTo: contentView.topAnchor), tableView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), tableView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - tableView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor) + tableView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), ]) + self.bottomConstraint = bottomSpacing.heightAnchor.constraint(equalToConstant: 0) + bottomConstraint.isActive = true + header.step = .one } @@ -190,6 +204,29 @@ class ReportViewController: UIViewController, NeedsDependency { } }) .store(in: &disposeBag) + + Publishers.CombineLatest( + KeyboardResponderService.shared.state.eraseToAnyPublisher(), + KeyboardResponderService.shared.endFrame.eraseToAnyPublisher() + ) + .sink(receiveValue: { [weak self] state, endFrame in + guard let self = self else { return } + + guard state == .dock else { + self.bottomConstraint.constant = 0.0 + return + } + + let contentFrame = self.view.convert(self.view.frame, to: nil) + let padding = contentFrame.maxY - endFrame.minY + guard padding > 0 else { + self.bottomConstraint.constant = 0.0 + return + } + + self.bottomConstraint.constant = padding + }) + .store(in: &disposeBag) } private func setupNavigation() { @@ -231,9 +268,9 @@ class ReportViewController: UIViewController, NeedsDependency { constant: ReportView.horizontalMargin ), self.textView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor), - self.textView.trailingAnchor.constraint( - equalTo: self.contentView.safeAreaLayoutGuide.trailingAnchor, - constant: -1 * ReportView.horizontalMargin + self.contentView.trailingAnchor.constraint( + equalTo: self.textView.trailingAnchor, + constant: ReportView.horizontalMargin ), ]) self.textView.layoutIfNeeded() From 1b05d787df2f88d7ec42aea0ff351236fb30b8b1 Mon Sep 17 00:00:00 2001 From: ihugo Date: Sun, 25 Apr 2021 23:49:19 +0800 Subject: [PATCH 15/18] fix: make report-status can be revealed --- .../Diffiable/Section/ReportSection.swift | 3 +- .../StatusProvider/StatusProviderFacade.swift | 28 +++++++++++ .../Report/ReportViewModel+Diffable.swift | 2 +- .../Report/ReportedStatusTableviewCell.swift | 49 ++++++++++++++++++- 4 files changed, 79 insertions(+), 3 deletions(-) diff --git a/Mastodon/Diffiable/Section/ReportSection.swift b/Mastodon/Diffiable/Section/ReportSection.swift index eccf09d26..e14a959f7 100644 --- a/Mastodon/Diffiable/Section/ReportSection.swift +++ b/Mastodon/Diffiable/Section/ReportSection.swift @@ -21,7 +21,7 @@ enum ReportSection: Equatable, Hashable { extension ReportSection { static func tableViewDiffableDataSource( for tableView: UITableView, - dependency: NeedsDependency, + dependency: ReportViewController, managedObjectContext: NSManagedObjectContext, timestampUpdatePublisher: AnyPublisher ) -> UITableViewDiffableDataSource { @@ -33,6 +33,7 @@ extension ReportSection { switch item { case .reportStatus(let objectID, let attribute): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ReportedStatusTableViewCell.self), for: indexPath) as! ReportedStatusTableViewCell + cell.dependency = dependency let activeMastodonAuthenticationBox = dependency.context.authenticationService.activeMastodonAuthenticationBox.value let requestUserID = activeMastodonAuthenticationBox?.userID ?? "" managedObjectContext.performAndWait { diff --git a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift index 2e6102227..03f84216a 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift @@ -498,6 +498,34 @@ extension StatusProviderFacade { .store(in: &dependency.context.disposeBag) } + static func responseToStatusContentWarningRevealAction(dependency: ReportViewController, cell: UITableViewCell) { + let status = Future { promise in + guard let diffableDataSource = dependency.viewModel.diffableDataSource, + let indexPath = dependency.tableView.indexPath(for: cell), + let item = diffableDataSource.itemIdentifier(for: indexPath) else { + promise(.success(nil)) + return + } + let managedObjectContext = dependency.viewModel.statusFetchedResultsController + .fetchedResultsController + .managedObjectContext + + switch item { + case .reportStatus(let objectID, _): + managedObjectContext.perform { + let status = managedObjectContext.object(with: objectID) as! Status + promise(.success(status)) + } + default: + promise(.success(nil)) + } + } + + _responseToStatusContentWarningRevealAction( + dependency: dependency, + status: status + ) + } } extension StatusProviderFacade { diff --git a/Mastodon/Scene/Report/ReportViewModel+Diffable.swift b/Mastodon/Scene/Report/ReportViewModel+Diffable.swift index f737381b1..73d6ffa0d 100644 --- a/Mastodon/Scene/Report/ReportViewModel+Diffable.swift +++ b/Mastodon/Scene/Report/ReportViewModel+Diffable.swift @@ -13,7 +13,7 @@ import CoreDataStack extension ReportViewModel { func setupDiffableDataSource( for tableView: UITableView, - dependency: NeedsDependency + dependency: ReportViewController ) { let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common) .autoconnect() diff --git a/Mastodon/Scene/Report/ReportedStatusTableviewCell.swift b/Mastodon/Scene/Report/ReportedStatusTableviewCell.swift index 0c63398d1..3cbfface5 100644 --- a/Mastodon/Scene/Report/ReportedStatusTableviewCell.swift +++ b/Mastodon/Scene/Report/ReportedStatusTableviewCell.swift @@ -17,6 +17,7 @@ final class ReportedStatusTableViewCell: UITableViewCell, StatusCell { static let bottomPaddingHeight: CGFloat = 10 + var dependency: ReportViewController? var disposeBag = Set() var pollCountdownSubscription: AnyCancellable? var observations = Set() @@ -63,6 +64,9 @@ final class ReportedStatusTableViewCell: UITableViewCell, StatusCell { override func layoutSubviews() { super.layoutSubviews() + + // precondition: app is active + guard UIApplication.shared.applicationState == .active else { return } DispatchQueue.main.async { self.statusView.drawContentWarningImageView() } @@ -127,8 +131,10 @@ extension ReportedStatusTableViewCell { resetSeparatorLineLayout() selectionStyle = .none + statusView.delegate = self + statusView.statusMosaicImageViewContainer.delegate = self statusView.actionToolbarContainer.isHidden = true - statusView.isUserInteractionEnabled = false + statusView.contentWarningOverlayView.blurContentImageView.backgroundColor = backgroundColor } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { @@ -168,3 +174,44 @@ extension ReportedStatusTableViewCell { } } } + +extension ReportedStatusTableViewCell: MosaicImageViewContainerDelegate { + func mosaicImageViewContainer(_ mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) { + + } + + func mosaicImageViewContainer(_ mosaicImageViewContainer: MosaicImageViewContainer, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) { + + guard let dependency = self.dependency else { return } + StatusProviderFacade.responseToStatusContentWarningRevealAction(dependency: dependency, cell: self) + } +} + +extension ReportedStatusTableViewCell: StatusViewDelegate { + func statusView(_ statusView: StatusView, headerInfoLabelDidPressed label: UILabel) { + } + + func statusView(_ statusView: StatusView, avatarButtonDidPressed button: UIButton) { + } + + func statusView(_ statusView: StatusView, revealContentWarningButtonDidPressed button: UIButton) { + guard let dependency = self.dependency else { return } + StatusProviderFacade.responseToStatusContentWarningRevealAction(dependency: dependency, cell: self) + } + + func statusView(_ statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) { + guard let dependency = self.dependency else { return } + StatusProviderFacade.responseToStatusContentWarningRevealAction(dependency: dependency, cell: self) + } + + func statusView(_ statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) { + guard let dependency = self.dependency else { return } + StatusProviderFacade.responseToStatusContentWarningRevealAction(dependency: dependency, cell: self) + } + + func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton) { + } + + func statusView(_ statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) { + } +} From ad8df6813f141795e2d7b451dbb7d89a00199875 Mon Sep 17 00:00:00 2001 From: ihugo Date: Mon, 26 Apr 2021 15:58:49 +0800 Subject: [PATCH 16/18] fix: add entries for the reporting --- Localization/app.json | 3 +- .../Diffiable/Section/StatusSection.swift | 53 ++++++++++++++++++- Mastodon/Generated/Strings.swift | 4 ++ .../UserProvider/UserProviderFacade.swift | 18 +++++++ .../Resources/en.lproj/Localizable.strings | 1 + ...meTimelineViewController+DebugAction.swift | 41 -------------- .../Scene/Report/ReportViewController.swift | 2 +- .../Scene/Report/ReportViewModel+Data.swift | 7 +-- Mastodon/Scene/Report/ReportViewModel.swift | 16 +++--- 9 files changed, 89 insertions(+), 56 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index fcd960293..45ec698ad 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -52,7 +52,8 @@ "share": "Share", "share_user": "Share %s", "open_in_safari": "Open in Safari", - "skip": "Skip" + "skip": "Skip", + "report_user": "Report %s" }, "status": { "user_reblogged": "%s reblogged", diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 97e4cdf9c..ea3332f14 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -301,6 +301,9 @@ extension StatusSection { case is NotificationStatusTableViewCell: let notificationTableViewCell = cell as! NotificationStatusTableViewCell parent = notificationTableViewCell.delegate?.parent() + case is ReportedStatusTableViewCell: + let reportTableViewCell = cell as! ReportedStatusTableViewCell + parent = reportTableViewCell.dependency default: parent = nil assertionFailure("unknown cell") @@ -394,7 +397,12 @@ extension StatusSection { } // toolbar - StatusSection.configureActionToolBar(cell: cell, status: status, requestUserID: requestUserID) + StatusSection.configureActionToolBar( + cell: cell, + dependency: dependency, + status: status, + requestUserID: requestUserID + ) // separator line if let statusTableViewCell = cell as? StatusTableViewCell { @@ -418,7 +426,12 @@ extension StatusSection { } receiveValue: { change in guard case .update(let object) = change.changeType, let status = object as? Status else { return } - StatusSection.configureActionToolBar(cell: cell, status: status, requestUserID: requestUserID) + StatusSection.configureActionToolBar( + cell: cell, + dependency: dependency, + status: status, + requestUserID: requestUserID + ) os_log("%{public}s[%{public}ld], %{public}s: reblog count label for status %s did update: %ld", (#file as NSString).lastPathComponent, #line, #function, status.id, status.reblogsCount.intValue) os_log("%{public}s[%{public}ld], %{public}s: like count label for status %s did update: %ld", (#file as NSString).lastPathComponent, #line, #function, status.id, status.favouritesCount.intValue) @@ -573,6 +586,7 @@ extension StatusSection { static func configureActionToolBar( cell: StatusCell, + dependency: NeedsDependency, status: Status, requestUserID: String ) { @@ -600,6 +614,8 @@ extension StatusSection { }() cell.statusView.actionToolbarContainer.favoriteButton.setTitle(favoriteCountTitle, for: .normal) cell.statusView.actionToolbarContainer.isFavoriteButtonHighlight = isLike + + self.setupStatusMoreButtonMenu(cell: cell, dependency: dependency, status: status) } static func configurePoll( @@ -726,4 +742,37 @@ extension StatusSection { guard let number = number, number > 0 else { return "" } return String(number) } + + private static func setupStatusMoreButtonMenu( + cell: StatusCell, + dependency: NeedsDependency, + status: Status) { + + cell.statusView.actionToolbarContainer.moreButton.menu = nil + + guard let authenticationBox = dependency.context.authenticationService.activeMastodonAuthenticationBox.value else { + return + } + let author = (status.reblog ?? status).author + guard authenticationBox.userID != author.id else { + return + } + var children: [UIMenuElement] = [] + let name = author.displayNameWithFallback + let reportAction = UIAction(title: L10n.Common.Controls.Actions.reportUser(name), image: UIImage(systemName: "exclamationmark.bubble"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { _ in + let viewModel = ReportViewModel( + context: dependency.context, + domain: authenticationBox.domain, + user: status.author, + status: status) + dependency.coordinator.present( + scene: .report(viewModel: viewModel), + from: nil, + transition: .modal(animated: true, completion: nil) + ) + } + children.append(reportAction) + cell.statusView.actionToolbarContainer.moreButton.menu = UIMenu(title: "", options: [], children: children) + cell.statusView.actionToolbarContainer.moreButton.showsMenuAsPrimaryAction = true + } } diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index d9baf8665..7fb024417 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -80,6 +80,10 @@ internal enum L10n { internal static let preview = L10n.tr("Localizable", "Common.Controls.Actions.Preview") /// Remove internal static let remove = L10n.tr("Localizable", "Common.Controls.Actions.Remove") + /// Report %@ + internal static func reportUser(_ p1: Any) -> String { + return L10n.tr("Localizable", "Common.Controls.Actions.ReportUser", String(describing: p1)) + } /// Save internal static let save = L10n.tr("Localizable", "Common.Controls.Actions.Save") /// Save photo diff --git a/Mastodon/Protocol/UserProvider/UserProviderFacade.swift b/Mastodon/Protocol/UserProvider/UserProviderFacade.swift index b5f4dd32f..4f0a2bfee 100644 --- a/Mastodon/Protocol/UserProvider/UserProviderFacade.swift +++ b/Mastodon/Protocol/UserProvider/UserProviderFacade.swift @@ -218,6 +218,24 @@ extension UserProviderFacade { children.append(shareAction) } + let reportAction = UIAction(title: L10n.Common.Controls.Actions.reportUser(name), image: UIImage(systemName: "exclamationmark.bubble"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak provider] _ in + guard let provider = provider else { return } + guard let authenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else { + return + } + let viewModel = ReportViewModel( + context: provider.context, + domain: authenticationBox.domain, + user: mastodonUser, + status: nil) + provider.coordinator.present( + scene: .report(viewModel: viewModel), + from: provider, + transition: .modal(animated: true, completion: nil) + ) + } + children.append(reportAction) + return UIMenu(title: "", options: [], children: children) } diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index e16d3e886..4ce29afd8 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -24,6 +24,7 @@ Please check your internet connection."; "Common.Controls.Actions.OpenInSafari" = "Open in Safari"; "Common.Controls.Actions.Preview" = "Preview"; "Common.Controls.Actions.Remove" = "Remove"; +"Common.Controls.Actions.ReportUser" = "Report %@"; "Common.Controls.Actions.Save" = "Save"; "Common.Controls.Actions.SavePhoto" = "Save photo"; "Common.Controls.Actions.SeeMore" = "See More"; diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift index eaad67b7c..b1dd19447 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift @@ -41,10 +41,6 @@ extension HomeTimelineViewController { guard let self = self else { return } self.showSettings(action) }, - UIAction(title: "Report", image: UIImage(systemName: "exclamationmark.bubble"), attributes: []) { [weak self] action in - guard let self = self else { return } - self.showReportAction(action) - }, UIAction(title: "Sign Out", image: UIImage(systemName: "escape"), attributes: .destructive) { [weak self] action in guard let self = self else { return } self.signOutAction(action) @@ -339,42 +335,5 @@ extension HomeTimelineViewController { transition: .modal(animated: true, completion: nil) ) } - - @objc private func showReportAction(_ sender: UIAction) { - let alertController = UIAlertController(title: "Enter User ID", message: nil, preferredStyle: .alert) - alertController.addTextField() - alertController.addTextField() - guard let accountTextField = alertController.textFields?.first else { return } - guard let statusTextField = alertController.textFields?.last else { return } - accountTextField.placeholder = "User ID" - statusTextField.placeholder = "Status ID" - accountTextField.text = "212477" - statusTextField.text = "106103767536113615" - let showAction = UIAlertAction(title: "Show", style: .default) { [weak self] _ in - guard let self = self else { return } - - guard let userId = accountTextField.text else { return } - guard let statusId = statusTextField.text else { return } - guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return } - - // itodo: delete them - // 31803 - // 106093402888557459 - let viewModel = ReportViewModel( - context: self.context, - domain: authenticationBox.domain, - userId: userId, - statusId: statusId - ) - self.coordinator.present( - scene: .report(viewModel: viewModel), - from: self, transition: .modal(animated: true, completion: nil)) - } - alertController.addAction(showAction) - let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) - alertController.addAction(cancelAction) - coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil)) - } - } #endif diff --git a/Mastodon/Scene/Report/ReportViewController.swift b/Mastodon/Scene/Report/ReportViewController.swift index 9cf506a30..dea962dca 100644 --- a/Mastodon/Scene/Report/ReportViewController.swift +++ b/Mastodon/Scene/Report/ReportViewController.swift @@ -242,7 +242,7 @@ class ReportViewController: UIViewController, NeedsDependency { return nil } let request = MastodonUser.sortedFetchRequest - request.predicate = MastodonUser.predicate(domain: domain, id: viewModel.userId) + request.predicate = MastodonUser.predicate(domain: domain, id: viewModel.user.id) request.fetchLimit = 1 request.returnsObjectsAsFaults = false do { diff --git a/Mastodon/Scene/Report/ReportViewModel+Data.swift b/Mastodon/Scene/Report/ReportViewModel+Data.swift index dcb715fba..da22a4b95 100644 --- a/Mastodon/Scene/Report/ReportViewModel+Data.swift +++ b/Mastodon/Scene/Report/ReportViewModel+Data.swift @@ -22,6 +22,7 @@ extension ReportViewModel { context.apiService.userTimeline( domain: domain, accountID: accountId, + excludeReblogs: true, authorizationBox: authorizationBox ) .receive(on: DispatchQueue.main) @@ -30,7 +31,7 @@ extension ReportViewModel { case .failure(let error): os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch user timeline fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) guard let self = self else { return } - guard let reportStatusId = self.statusId else { return } + guard let reportStatusId = self.status?.id else { return } var statusIDs = self.statusFetchedResultsController.statusIDs.value guard statusIDs.contains(reportStatusId) else { return } @@ -44,7 +45,7 @@ extension ReportViewModel { guard let self = self else { return } var statusIDs = response.value.map { $0.id } - if let reportStatusId = self.statusId, !statusIDs.contains(reportStatusId) { + if let reportStatusId = self.status?.id, !statusIDs.contains(reportStatusId) { statusIDs.append(reportStatusId) } @@ -86,7 +87,7 @@ extension ReportViewModel { guard let status = managedObjectContext.object(with: objectID) as? Status else { continue } - if status.id == self.statusId { + if status.id == self.status?.id { attribute.isSelected = true self.append(statusID: status.id) self.continueEnableSubject.send(true) diff --git a/Mastodon/Scene/Report/ReportViewModel.swift b/Mastodon/Scene/Report/ReportViewModel.swift index 4864145f5..b787cf6c7 100644 --- a/Mastodon/Scene/Report/ReportViewModel.swift +++ b/Mastodon/Scene/Report/ReportViewModel.swift @@ -23,8 +23,8 @@ class ReportViewModel: NSObject { // confirm set only once weak var context: AppContext! { willSet { precondition(context == nil) } } - var userId: String - var statusId: String? + var user: MastodonUser + var status: Status? var statusIDs = [Mastodon.Entity.Status.ID]() var comment: String? @@ -56,12 +56,12 @@ class ReportViewModel: NSObject { init(context: AppContext, domain: String, - userId: String, - statusId: String? + user: MastodonUser, + status: Status? ) { self.context = context - self.userId = userId - self.statusId = statusId + self.user = user + self.status = status self.statusFetchedResultsController = StatusFetchedResultsController( managedObjectContext: context.managedObjectContext, domain: domain, @@ -69,7 +69,7 @@ class ReportViewModel: NSObject { ) self.reportQuery = FileReportQuery( - accountID: userId, + accountID: user.id, statusIDs: [], comment: nil, forward: nil @@ -97,7 +97,7 @@ class ReportViewModel: NSObject { requestRecentStatus( domain: domain, - accountId: self.userId, + accountId: self.user.id, authorizationBox: activeMastodonAuthenticationBox ) From 7eb79414a45607e0fe16fecc6167c83071f03895 Mon Sep 17 00:00:00 2001 From: ihugo Date: Mon, 26 Apr 2021 17:41:24 +0800 Subject: [PATCH 17/18] fix: memery leak --- .../Diffiable/Section/ReportSection.swift | 3 ++- .../Diffiable/Section/StatusSection.swift | 26 +++++++++++++------ Mastodon/Scene/Report/ReportViewModel.swift | 8 +++--- .../Report/ReportedStatusTableviewCell.swift | 2 +- .../View/Content/TimelineHeaderView.swift | 2 ++ 5 files changed, 26 insertions(+), 15 deletions(-) diff --git a/Mastodon/Diffiable/Section/ReportSection.swift b/Mastodon/Diffiable/Section/ReportSection.swift index e14a959f7..6faaae6c2 100644 --- a/Mastodon/Diffiable/Section/ReportSection.swift +++ b/Mastodon/Diffiable/Section/ReportSection.swift @@ -36,7 +36,8 @@ extension ReportSection { cell.dependency = dependency let activeMastodonAuthenticationBox = dependency.context.authenticationService.activeMastodonAuthenticationBox.value let requestUserID = activeMastodonAuthenticationBox?.userID ?? "" - managedObjectContext.performAndWait { + managedObjectContext.performAndWait { [weak dependency] in + guard let dependency = dependency else { return } let status = managedObjectContext.object(with: objectID) as! Status StatusSection.configure( cell: cell, diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index ea3332f14..b897de47f 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -149,7 +149,8 @@ extension StatusSection { .receive(on: DispatchQueue.main) .sink { _ in // do nothing - } receiveValue: { change in + } receiveValue: { [weak cell] change in + guard let cell = cell else { return } guard case .update(let object) = change.changeType, let newStatus = object as? Status else { return } StatusSection.configureHeader(cell: cell, status: newStatus) @@ -221,7 +222,8 @@ extension StatusSection { } else { meta.blurhashImagePublisher() .receive(on: DispatchQueue.main) - .sink { image in + .sink { [weak cell] image in + guard let cell = cell else { return } blurhashOverlayImageView.image = image image?.pngData().flatMap { blurhashImageCache.setObject($0 as NSData, forKey: blurhashImageDataKey) @@ -246,7 +248,8 @@ extension StatusSection { statusItemAttribute.isRevealing ) .receive(on: DispatchQueue.main) - .sink { isImageLoaded, isMediaRevealing in + .sink { [weak cell] isImageLoaded, isMediaRevealing in + guard let cell = cell else { return } guard isImageLoaded else { blurhashOverlayImageView.alpha = 1 blurhashOverlayImageView.isHidden = false @@ -355,7 +358,8 @@ extension StatusSection { .receive(on: DispatchQueue.main) .sink { _ in // do nothing - } receiveValue: { [weak dependency] change in + } receiveValue: { [weak dependency, weak cell] change in + guard let cell = cell else { return } guard let dependency = dependency else { return } guard case .update(let object) = change.changeType, let status = object as? Status else { return } @@ -382,7 +386,8 @@ extension StatusSection { ManagedObjectObserver.observe(object: poll) .sink { _ in // do nothing - } receiveValue: { change in + } receiveValue: { [weak cell] change in + guard let cell = cell else { return } guard case .update(let object) = change.changeType, let newPoll = object as? Poll else { return } StatusSection.configurePoll( @@ -413,7 +418,8 @@ extension StatusSection { let createdAt = (status.reblog ?? status).createdAt cell.statusView.dateLabel.text = createdAt.shortTimeAgoSinceNow timestampUpdatePublisher - .sink { _ in + .sink { [weak cell] _ in + guard let cell = cell else { return } cell.statusView.dateLabel.text = createdAt.shortTimeAgoSinceNow } .store(in: &cell.disposeBag) @@ -423,7 +429,9 @@ extension StatusSection { .receive(on: DispatchQueue.main) .sink { _ in // do nothing - } receiveValue: { change in + } receiveValue: { [weak dependency, weak cell] change in + guard let cell = cell else { return } + guard let dependency = dependency else { return } guard case .update(let object) = change.changeType, let status = object as? Status else { return } StatusSection.configureActionToolBar( @@ -759,7 +767,9 @@ extension StatusSection { } var children: [UIMenuElement] = [] let name = author.displayNameWithFallback - let reportAction = UIAction(title: L10n.Common.Controls.Actions.reportUser(name), image: UIImage(systemName: "exclamationmark.bubble"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { _ in + let reportAction = UIAction(title: L10n.Common.Controls.Actions.reportUser(name), image: UIImage(systemName: "exclamationmark.bubble"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { + [weak dependency] _ in + guard let dependency = dependency else { return } let viewModel = ReportViewModel( context: dependency.context, domain: authenticationBox.domain, diff --git a/Mastodon/Scene/Report/ReportViewModel.swift b/Mastodon/Scene/Report/ReportViewModel.swift index b787cf6c7..8631963c5 100644 --- a/Mastodon/Scene/Report/ReportViewModel.swift +++ b/Mastodon/Scene/Report/ReportViewModel.swift @@ -133,13 +133,11 @@ class ReportViewModel: NSObject { } .store(in: &disposeBag) - input.comment.assign( - to: \.comment, - on: self - ) - .store(in: &disposeBag) input.comment.sink { [weak self] (comment) in guard let self = self else { return } + + self.comment = comment + let sendEnable = (comment?.length ?? 0) > 0 self.sendEnableSubject.send(sendEnable) } diff --git a/Mastodon/Scene/Report/ReportedStatusTableviewCell.swift b/Mastodon/Scene/Report/ReportedStatusTableviewCell.swift index 3cbfface5..b1d0af6b0 100644 --- a/Mastodon/Scene/Report/ReportedStatusTableviewCell.swift +++ b/Mastodon/Scene/Report/ReportedStatusTableviewCell.swift @@ -17,7 +17,7 @@ final class ReportedStatusTableViewCell: UITableViewCell, StatusCell { static let bottomPaddingHeight: CGFloat = 10 - var dependency: ReportViewController? + weak var dependency: ReportViewController? var disposeBag = Set() var pollCountdownSubscription: AnyCancellable? var observations = Set() diff --git a/Mastodon/Scene/Share/View/Content/TimelineHeaderView.swift b/Mastodon/Scene/Share/View/Content/TimelineHeaderView.swift index b5e4c5bde..f095f6f44 100644 --- a/Mastodon/Scene/Share/View/Content/TimelineHeaderView.swift +++ b/Mastodon/Scene/Share/View/Content/TimelineHeaderView.swift @@ -5,6 +5,8 @@ // Created by MainasuK Cirno on 2021-4-6. // +import UIKit + final class TimelineHeaderView: UIView { let iconImageView: UIImageView = { From d66491b7264542a840741a68d3cacfaaaa2d55f0 Mon Sep 17 00:00:00 2001 From: ihugo Date: Tue, 27 Apr 2021 17:44:01 +0800 Subject: [PATCH 18/18] feat: add prefetching feaature for reporting --- .../Scene/Report/ReportViewController.swift | 10 +++++ .../Scene/Report/ReportViewModel+Data.swift | 38 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/Mastodon/Scene/Report/ReportViewController.swift b/Mastodon/Scene/Report/ReportViewController.swift index dea962dca..b0c6ddcc9 100644 --- a/Mastodon/Scene/Report/ReportViewController.swift +++ b/Mastodon/Scene/Report/ReportViewController.swift @@ -68,6 +68,7 @@ class ReportViewController: UIViewController, NeedsDependency { tableView.backgroundColor = .clear tableView.translatesAutoresizingMaskIntoConstraints = false tableView.delegate = self + tableView.prefetchDataSource = self tableView.allowsMultipleSelection = true return tableView }() @@ -310,6 +311,7 @@ class ReportViewController: UIViewController, NeedsDependency { } } +// MARK: - UITableViewDelegate extension ReportViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let item = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { @@ -326,6 +328,14 @@ extension ReportViewController: UITableViewDelegate { } } +// MARK: - UITableViewDataSourcePrefetching +extension ReportViewController: UITableViewDataSourcePrefetching { + func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { + viewModel.prefetchData(prefetchRowsAt: indexPaths) + } +} + +// MARK: - UITextViewDelegate extension ReportViewController: UITextViewDelegate { func textViewDidChange(_ textView: UITextView) { self.comment.send(textView.text) diff --git a/Mastodon/Scene/Report/ReportViewModel+Data.swift b/Mastodon/Scene/Report/ReportViewModel+Data.swift index da22a4b95..df95cb002 100644 --- a/Mastodon/Scene/Report/ReportViewModel+Data.swift +++ b/Mastodon/Scene/Report/ReportViewModel+Data.swift @@ -97,4 +97,42 @@ extension ReportViewModel { } .store(in: &disposeBag) } + + func prefetchData(prefetchRowsAt indexPaths: [IndexPath]) { + guard let diffableDataSource = diffableDataSource else { return } + + // prefetch reply status + guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } + let domain = activeMastodonAuthenticationBox.domain + + var statusObjectIDs: [NSManagedObjectID] = [] + for indexPath in indexPaths { + let item = diffableDataSource.itemIdentifier(for: indexPath) + switch item { + case .reportStatus(let objectID, _): + statusObjectIDs.append(objectID) + default: + continue + } + } + + let backgroundManagedObjectContext = context.backgroundManagedObjectContext + backgroundManagedObjectContext.perform { [weak self] in + guard let self = self else { return } + for objectID in statusObjectIDs { + let status = backgroundManagedObjectContext.object(with: objectID) as! Status + guard let replyToID = status.inReplyToID, status.replyTo == nil else { + // skip + continue + } + self.context.statusPrefetchingService.prefetchReplyTo( + domain: domain, + statusObjectID: status.objectID, + statusID: status.id, + replyToStatusID: replyToID, + authorizationBox: activeMastodonAuthenticationBox + ) + } + } + } }