From b06f4b1b4d4228a7423709e6e1cd54af7efb0490 Mon Sep 17 00:00:00 2001 From: Ingerid Fosli Date: Thu, 5 Oct 2023 14:45:22 -0600 Subject: [PATCH] [MBL-855] Remove creator dashboard (#1853) * Remove flag from remoteConfig * Delete dashboard from root view model * Delete dashboard from navigation * Delete goToDashboard signals * Delete creator message thread and project activity signals * Delete all the files * Delete view models and clean up * Delete project activity files * Fix format * Fix tests * Delete creator-specific notification enums for notifications that would currently do nothing anyways * Update screenshots after merge * Delete redundant test * Add tabbaritemsstyles test back in and fix format * Delete unused isMember * Re-add accidentally deleted goToDiscovery Co-authored-by: Scott Clampet <110618242+scottkicks@users.noreply.github.com> * Readd goToReportProject methods that got deleted in a bad merge * Fix format * Add rosetta to build-and-cache --------- Co-authored-by: Scott Clampet <110618242+scottkicks@users.noreply.github.com> --- .circleci/config.yml | 1 + Kickstarter-iOS/AppDelegate.swift | 25 - Kickstarter-iOS/AppDelegateViewModel.swift | 76 +- .../AppDelegateViewModelTests.swift | 88 +- .../Contents.json | 15 - .../icon--graph-line-selected-tab-bar.pdf | Bin 4225 -> 0 bytes .../Contents.json | 15 - .../icon--graph-line-unselected-tab-bar.pdf | Bin 4220 -> 0 bytes .../Controller/DashboardViewController.swift | 405 ------- .../DashboardViewControllerTests.swift | 134 --- .../UpdateDraftViewController.swift | 365 ------ .../UpdatePreviewViewController.swift | 100 -- .../Datasource/DashboardDataSource.swift | 84 -- .../Datasource/DashboardDataSourceTests.swift | 41 - .../Dashboard/Storyboard/Dashboard.storyboard | 1035 ----------------- .../Storyboard/UpdateDraft.storyboard | 214 ---- .../ViewModel/UpdatePreviewViewModel.swift | 138 --- .../UpdatePreviewViewModelTests.swift | 156 --- .../Views/Cells/DashboardActionCell.swift | 104 -- .../Views/Cells/DashboardContextCell.swift | 42 - .../Views/Cells/DashboardFundingCell.swift | 103 -- .../Views/Cells/DashboardReferrersCell.swift | 231 ---- .../Views/Cells/DashboardRewardsCell.swift | 150 --- .../Views/Cells/DashboardVideoCell.swift | 117 -- .../Views/DashboardReferrerRowStackView.swift | 43 - .../Views/DashboardRewardRowStackView.swift | 47 - .../Dashboard/Views/DashboardTitleView.swift | 96 -- .../Dashboard/Views/ReferralChartView.swift | 85 -- .../DashboardDeprecationView.swift | 52 - ...ashboardProjectsDrawerViewController.swift | 154 --- .../DashboardProjectsDrawerDataSource.swift | 28 - ...shboardProjectsDrawerDataSourceTests.swift | 31 - .../DashboardProjectsDrawer.storyboard | 106 -- .../Cells/DashboardProjectsDrawerCell.swift | 45 - .../Views/FundingGraphView.swift | 276 ----- .../Views/FundingGraphViewTests.swift | 138 --- .../ProjectActivitiesViewController.swift | 197 ---- .../ProjectActivityViewControllerTests.swift | 126 -- .../ProjectActivitiesDataSource.swift | 124 -- .../ProjectActivitiesDataSourceTests.swift | 97 -- .../Storyboard/ProjectActivity.storyboard | 738 ------------ .../Cells/ProjectActivityBackingCell.swift | 202 ---- .../Cells/ProjectActivityCommentCell.swift | 166 --- .../Views/Cells/ProjectActivityDateCell.swift | 38 - .../Cells/ProjectActivityEmptyStateCell.swift | 6 - .../Cells/ProjectActivityLaunchCell.swift | 56 - ...ojectActivityNegativeStateChangeCell.swift | 57 - .../Cells/ProjectActivitySuccessCell.swift | 75 -- .../Cells/ProjectActivityUpdateCell.swift | 122 -- .../ProjectPageViewController.swift | 22 - ...gFeatureFlagToolsViewControllerTests.swift | 4 +- ...ConfigFeatureFlagToolsViewController.1.png | Bin 56298 -> 50082 bytes .../RootTabBar/RootTabBarViewController.swift | 68 +- Kickstarter-iOS/Library/Storyboard.swift | 2 - Kickstarter.xcodeproj/project.pbxproj | 459 +------- KsApi/models/Activity.swift | 4 - Library/Navigation.swift | 14 - Library/NavigationTests.swift | 5 - Library/RefTag.swift | 4 - Library/RefTagTests.swift | 3 - .../RemoteConfigFeature+Helpers.swift | 7 - .../RemoteConfigFeature+HelpersTests.swift | 18 - .../RemoteConfig/RemoteConfigFeature.swift | 2 - Library/ShareContext.swift | 7 +- Library/ShortcutItem.swift | 10 +- Library/Styles/TabBarItemStyles.swift | 26 +- Library/Tracking/KSRAnalytics.swift | 62 - Library/Tracking/KSRAnalyticsTests.swift | 48 - .../DashboardActionCellViewModel.swift | 160 --- .../DashboardActionCellViewModelTests.swift | 171 --- .../DashboardFundingCellViewModel.swift | 194 --- .../DashboardFundingCellViewModelTests.swift | 148 --- ...DashboardProjectsDrawerCellViewModel.swift | 66 -- ...oardProjectsDrawerCellViewModelTests.swift | 43 - .../DashboardProjectsDrawerViewModel.swift | 102 -- ...ashboardProjectsDrawerViewModelTests.swift | 102 -- ...shboardReferrerRowStackViewViewModel.swift | 71 -- ...rdReferrerRowStackViewViewModelTests.swift | 39 - .../DashboardReferrersCellViewModel.swift | 249 ---- ...DashboardReferrersCellViewModelTests.swift | 219 ---- ...DashboardRewardRowStackViewViewModel.swift | 76 -- ...oardRewardRowStackViewViewModelTests.swift | 99 -- .../DashboardRewardsCellViewModel.swift | 162 --- .../DashboardRewardsCellViewModelTests.swift | 195 ---- .../DashboardTitleViewViewModel.swift | 97 -- .../DashboardTitleViewViewModelTests.swift | 157 --- .../DashboardVideoCellViewModel.swift | 128 -- .../DashboardVideoCellViewModelTests.swift | 91 -- Library/ViewModels/DashboardViewModel.swift | 430 ------- .../ViewModels/DashboardViewModelTests.swift | 402 ------- .../ProjectActivitiesViewModel.swift | 231 ---- .../ProjectActivitiesViewModelTests.swift | 235 ---- .../ProjectActivityBackingCellViewModel.swift | 253 ---- ...ectActivityBackingCellViewModelTests.swift | 494 -------- .../ProjectActivityCommentCellViewModel.swift | 156 --- ...ectActivityCommentCellViewModelTests.swift | 262 ----- .../ProjectActivityLaunchCellViewModel.swift | 61 - ...jectActivityLaunchCellViewModelTests.swift | 45 - ...vityNegativeStateChangeCellViewModel.swift | 69 -- ...egativeStateChangeCellViewModelTests.swift | 81 -- .../ProjectActivitySuccessCellViewModel.swift | 62 - ...ectActivitySuccessCellViewModelTests.swift | 62 - .../ProjectActivityUpdateCellViewModel.swift | 102 -- ...jectActivityUpdateCellViewModelTests.swift | 144 --- Library/ViewModels/ProjectPageViewModel.swift | 8 - .../ProjectPageViewModelTests.swift | 14 - .../ProjectPamphletContentViewModel.swift | 6 - ...ProjectPamphletContentViewModelTests.swift | 11 - ...emoteConfigFeatureFlagToolsViewModel.swift | 8 - ...ConfigFeatureFlagToolsViewModelTests.swift | 2 - Library/ViewModels/RootViewModel.swift | 103 +- Library/ViewModels/RootViewModelTests.swift | 277 +---- Library/ViewModels/ShareViewModel.swift | 4 - Library/ViewModels/ShareViewModelTests.swift | 7 - 114 files changed, 89 insertions(+), 13518 deletions(-) delete mode 100644 Kickstarter-iOS/Assets.xcassets/icons/tabbar-icon-dashboard-selected.imageset/Contents.json delete mode 100644 Kickstarter-iOS/Assets.xcassets/icons/tabbar-icon-dashboard-selected.imageset/icon--graph-line-selected-tab-bar.pdf delete mode 100644 Kickstarter-iOS/Assets.xcassets/icons/tabbar-icon-dashboard.imageset/Contents.json delete mode 100644 Kickstarter-iOS/Assets.xcassets/icons/tabbar-icon-dashboard.imageset/icon--graph-line-unselected-tab-bar.pdf delete mode 100644 Kickstarter-iOS/Features/Dashboard/Controller/DashboardViewController.swift delete mode 100644 Kickstarter-iOS/Features/Dashboard/Controller/DashboardViewControllerTests.swift delete mode 100644 Kickstarter-iOS/Features/Dashboard/Controller/UpdateDraftViewController.swift delete mode 100644 Kickstarter-iOS/Features/Dashboard/Controller/UpdatePreviewViewController.swift delete mode 100644 Kickstarter-iOS/Features/Dashboard/Datasource/DashboardDataSource.swift delete mode 100644 Kickstarter-iOS/Features/Dashboard/Datasource/DashboardDataSourceTests.swift delete mode 100644 Kickstarter-iOS/Features/Dashboard/Storyboard/Dashboard.storyboard delete mode 100644 Kickstarter-iOS/Features/Dashboard/Storyboard/UpdateDraft.storyboard delete mode 100644 Kickstarter-iOS/Features/Dashboard/ViewModel/UpdatePreviewViewModel.swift delete mode 100644 Kickstarter-iOS/Features/Dashboard/ViewModel/UpdatePreviewViewModelTests.swift delete mode 100644 Kickstarter-iOS/Features/Dashboard/Views/Cells/DashboardActionCell.swift delete mode 100644 Kickstarter-iOS/Features/Dashboard/Views/Cells/DashboardContextCell.swift delete mode 100644 Kickstarter-iOS/Features/Dashboard/Views/Cells/DashboardFundingCell.swift delete mode 100644 Kickstarter-iOS/Features/Dashboard/Views/Cells/DashboardReferrersCell.swift delete mode 100644 Kickstarter-iOS/Features/Dashboard/Views/Cells/DashboardRewardsCell.swift delete mode 100644 Kickstarter-iOS/Features/Dashboard/Views/Cells/DashboardVideoCell.swift delete mode 100644 Kickstarter-iOS/Features/Dashboard/Views/DashboardReferrerRowStackView.swift delete mode 100644 Kickstarter-iOS/Features/Dashboard/Views/DashboardRewardRowStackView.swift delete mode 100644 Kickstarter-iOS/Features/Dashboard/Views/DashboardTitleView.swift delete mode 100644 Kickstarter-iOS/Features/Dashboard/Views/ReferralChartView.swift delete mode 100644 Kickstarter-iOS/Features/DashboardDeprecationBanner/DashboardDeprecationView.swift delete mode 100644 Kickstarter-iOS/Features/DashboardProjects/Controller/DashboardProjectsDrawerViewController.swift delete mode 100644 Kickstarter-iOS/Features/DashboardProjects/Datasource/DashboardProjectsDrawerDataSource.swift delete mode 100644 Kickstarter-iOS/Features/DashboardProjects/Datasource/DashboardProjectsDrawerDataSourceTests.swift delete mode 100644 Kickstarter-iOS/Features/DashboardProjects/Storyboard/DashboardProjectsDrawer.storyboard delete mode 100644 Kickstarter-iOS/Features/DashboardProjects/Views/Cells/DashboardProjectsDrawerCell.swift delete mode 100644 Kickstarter-iOS/Features/DashboardProjects/Views/FundingGraphView.swift delete mode 100644 Kickstarter-iOS/Features/DashboardProjects/Views/FundingGraphViewTests.swift delete mode 100644 Kickstarter-iOS/Features/ProjectActivities/Controller/ProjectActivitiesViewController.swift delete mode 100644 Kickstarter-iOS/Features/ProjectActivities/Controller/ProjectActivityViewControllerTests.swift delete mode 100644 Kickstarter-iOS/Features/ProjectActivities/Datasource/ProjectActivitiesDataSource.swift delete mode 100644 Kickstarter-iOS/Features/ProjectActivities/Datasource/ProjectActivitiesDataSourceTests.swift delete mode 100644 Kickstarter-iOS/Features/ProjectActivities/Storyboard/ProjectActivity.storyboard delete mode 100644 Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivityBackingCell.swift delete mode 100644 Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivityCommentCell.swift delete mode 100644 Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivityDateCell.swift delete mode 100644 Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivityEmptyStateCell.swift delete mode 100644 Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivityLaunchCell.swift delete mode 100644 Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivityNegativeStateChangeCell.swift delete mode 100644 Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivitySuccessCell.swift delete mode 100644 Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivityUpdateCell.swift delete mode 100644 Library/ViewModels/DashboardActionCellViewModel.swift delete mode 100644 Library/ViewModels/DashboardActionCellViewModelTests.swift delete mode 100644 Library/ViewModels/DashboardFundingCellViewModel.swift delete mode 100644 Library/ViewModels/DashboardFundingCellViewModelTests.swift delete mode 100644 Library/ViewModels/DashboardProjectsDrawerCellViewModel.swift delete mode 100644 Library/ViewModels/DashboardProjectsDrawerCellViewModelTests.swift delete mode 100644 Library/ViewModels/DashboardProjectsDrawerViewModel.swift delete mode 100644 Library/ViewModels/DashboardProjectsDrawerViewModelTests.swift delete mode 100644 Library/ViewModels/DashboardReferrerRowStackViewViewModel.swift delete mode 100644 Library/ViewModels/DashboardReferrerRowStackViewViewModelTests.swift delete mode 100644 Library/ViewModels/DashboardReferrersCellViewModel.swift delete mode 100644 Library/ViewModels/DashboardReferrersCellViewModelTests.swift delete mode 100644 Library/ViewModels/DashboardRewardRowStackViewViewModel.swift delete mode 100644 Library/ViewModels/DashboardRewardRowStackViewViewModelTests.swift delete mode 100644 Library/ViewModels/DashboardRewardsCellViewModel.swift delete mode 100644 Library/ViewModels/DashboardRewardsCellViewModelTests.swift delete mode 100644 Library/ViewModels/DashboardTitleViewViewModel.swift delete mode 100644 Library/ViewModels/DashboardTitleViewViewModelTests.swift delete mode 100644 Library/ViewModels/DashboardVideoCellViewModel.swift delete mode 100644 Library/ViewModels/DashboardVideoCellViewModelTests.swift delete mode 100644 Library/ViewModels/DashboardViewModel.swift delete mode 100644 Library/ViewModels/DashboardViewModelTests.swift delete mode 100644 Library/ViewModels/ProjectActivitiesViewModel.swift delete mode 100644 Library/ViewModels/ProjectActivitiesViewModelTests.swift delete mode 100644 Library/ViewModels/ProjectActivityBackingCellViewModel.swift delete mode 100644 Library/ViewModels/ProjectActivityBackingCellViewModelTests.swift delete mode 100644 Library/ViewModels/ProjectActivityCommentCellViewModel.swift delete mode 100644 Library/ViewModels/ProjectActivityCommentCellViewModelTests.swift delete mode 100644 Library/ViewModels/ProjectActivityLaunchCellViewModel.swift delete mode 100644 Library/ViewModels/ProjectActivityLaunchCellViewModelTests.swift delete mode 100644 Library/ViewModels/ProjectActivityNegativeStateChangeCellViewModel.swift delete mode 100644 Library/ViewModels/ProjectActivityNegativeStateChangeCellViewModelTests.swift delete mode 100644 Library/ViewModels/ProjectActivitySuccessCellViewModel.swift delete mode 100644 Library/ViewModels/ProjectActivitySuccessCellViewModelTests.swift delete mode 100644 Library/ViewModels/ProjectActivityUpdateCellViewModel.swift delete mode 100644 Library/ViewModels/ProjectActivityUpdateCellViewModelTests.swift diff --git a/.circleci/config.yml b/.circleci/config.yml index 9470ead0ad..bcafdeb192 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -95,6 +95,7 @@ jobs: environment: - *default_environment steps: + - macos/install-rosetta - checkout - run: name: SPM SSH Workaround diff --git a/Kickstarter-iOS/AppDelegate.swift b/Kickstarter-iOS/AppDelegate.swift index 7784dff2b6..6af9a1d3cf 100644 --- a/Kickstarter-iOS/AppDelegate.swift +++ b/Kickstarter-iOS/AppDelegate.swift @@ -97,16 +97,6 @@ internal final class AppDelegate: UIResponder, UIApplicationDelegate { .observeForUI() .observeValues { [weak self] in self?.rootTabBarController?.switchToActivities() } - self.viewModel.outputs.goToDashboard - .observeForUI() - .observeValues { [weak self] in self?.rootTabBarController?.switchToDashboard(project: $0) } - - self.viewModel.outputs.goToCreatorMessageThread - .observeForUI() - .observeValues { [weak self] in - self?.goToCreatorMessageThread($0, $1) - } - self.viewModel.outputs.goToLoginWithIntent .observeForControllerAction() .observeValues { [weak self] intent in @@ -121,12 +111,6 @@ internal final class AppDelegate: UIResponder, UIApplicationDelegate { .observeForUI() .observeValues { [weak self] in self?.goToMessageThread($0) } - self.viewModel.outputs.goToProjectActivities - .observeForUI() - .observeValues { [weak self] in - self?.goToProjectActivities($0) - } - self.viewModel.outputs.goToSearch .observeForUI() .observeValues { [weak self] in self?.rootTabBarController?.switchToSearch() } @@ -398,15 +382,6 @@ internal final class AppDelegate: UIResponder, UIApplicationDelegate { self.rootTabBarController?.switchToMessageThread(messageThread) } - private func goToCreatorMessageThread(_ projectId: Param, _ messageThread: MessageThread) { - self.rootTabBarController? - .switchToCreatorMessageThread(projectId: projectId, messageThread: messageThread) - } - - private func goToProjectActivities(_ projectId: Param) { - self.rootTabBarController?.switchToProjectActivities(projectId: projectId) - } - private func findRedirectUrl(_ url: URL) { let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil) let task = session.dataTask(with: url) diff --git a/Kickstarter-iOS/AppDelegateViewModel.swift b/Kickstarter-iOS/AppDelegateViewModel.swift index 26d75a4885..e59cd8165e 100644 --- a/Kickstarter-iOS/AppDelegateViewModel.swift +++ b/Kickstarter-iOS/AppDelegateViewModel.swift @@ -126,13 +126,7 @@ public protocol AppDelegateViewModelOutputs { /// Emits when the root view controller should navigate to activity. var goToActivity: Signal<(), Never> { get } - /// Emits when application should navigate to the creator's message thread - var goToCreatorMessageThread: Signal<(Param, MessageThread), Never> { get } - - /// Emits when the root view controller should navigate to the creator dashboard. - var goToDashboard: Signal { get } - - /// Emits when the root view controller should navigate to the creator dashboard. + /// Emits when the root view controller should navigate to the discovery screen. var goToDiscovery: Signal { get } /// Emits when the root view controller should present the login modal. @@ -144,9 +138,6 @@ public protocol AppDelegateViewModelOutputs { /// Emits when the root view controller should navigate to the user's profile. var goToProfile: Signal<(), Never> { get } - /// Emits when should navigate to the project activities view - var goToProjectActivities: Signal { get } - /// Emits a URL when we should open it in the safari browser. var goToMobileSafari: Signal { get } @@ -522,18 +513,6 @@ public final class AppDelegateViewModel: AppDelegateViewModelType, AppDelegateVi goToLogin.mapConst(.generic) ) - self.goToCreatorMessageThread = deepLink - .map { navigation -> (Param, Int)? in - guard case let .creatorMessages(projectId, messageThreadId) = navigation else { return nil } - return .some((projectId, messageThreadId: messageThreadId)) - } - .skipNil() - .switchMap { projectId, messageThreadId in - AppEnvironment.current.apiService.fetchMessageThread(messageThreadId: messageThreadId) - .demoteErrors() - .map { (projectId, $0.messageThread) } - } - self.goToMessageThread = deepLink .map { navigation -> Int? in guard case let .messages(messageThreadId) = navigation else { return nil } @@ -546,13 +525,6 @@ public final class AppDelegateViewModel: AppDelegateViewModelType, AppDelegateVi .map { $0.messageThread } } - self.goToProjectActivities = deepLink - .map { navigation -> Param? in - guard case let .projectActivity(projectId) = navigation else { return nil } - return .some(projectId) - } - .skipNil() - self.goToProfile = deepLink .filter { $0 == .tab(.me) } .ignoreValues() @@ -565,13 +537,6 @@ public final class AppDelegateViewModel: AppDelegateViewModelType, AppDelegateVi self.goToMobileSafari = resolvedRedirectUrl - self.goToDashboard = deepLink - .map { link -> Param?? in - guard case let .tab(.dashboard(param)) = link else { return nil } - return .some(param) - } - .skipNil() - let projectRootLink = Signal.merge(projectLink, projectPreviewLink) .filter { _, subpage, _, _ in subpage == .root } .map { _, _, vcs, _ in vcs } @@ -951,13 +916,10 @@ public final class AppDelegateViewModel: AppDelegateViewModelType, AppDelegateVi public let findRedirectUrl: Signal public let forceLogout: Signal<(), Never> public let goToActivity: Signal<(), Never> - public let goToCreatorMessageThread: Signal<(Param, MessageThread), Never> - public let goToDashboard: Signal public let goToDiscovery: Signal public let goToLoginWithIntent: Signal public let goToMessageThread: Signal public let goToProfile: Signal<(), Never> - public let goToProjectActivities: Signal public let goToMobileSafari: Signal public let goToSearch: Signal<(), Never> public let postNotification: Signal @@ -1052,15 +1014,9 @@ private func navigation(fromPushEnvelope envelope: PushEnvelope) -> Navigation? switch activity.category { case .backing: guard let projectId = activity.projectId else { return nil } - if envelope.forCreator == true { - return .projectActivity(.id(projectId)) - } return .project(.id(projectId), .root, refTag: .push) case .failure, .launch, .success, .cancellation, .suspension: guard let projectId = activity.projectId else { return nil } - if envelope.forCreator == .some(true) { - return .tab(.dashboard(project: .id(projectId))) - } return .project(.id(projectId), .root, refTag: .push) case .update: @@ -1087,10 +1043,6 @@ private func navigation(fromPushEnvelope envelope: PushEnvelope) -> Navigation? } return .project(.id(projectId), .comments, refTag: .push) - case .backingAmount, .backingCanceled, .backingDropped, .backingReward: - guard let projectId = activity.projectId else { return nil } - return .tab(.dashboard(project: .id(projectId))) - case .follow: return .tab(.activity) @@ -1100,18 +1052,11 @@ private func navigation(fromPushEnvelope envelope: PushEnvelope) -> Navigation? } if let project = envelope.project { - if envelope.forCreator == .some(true) { - return .tab(.dashboard(project: .id(project.id))) - } return .project(.id(project.id), .root, refTag: .push) } if let message = envelope.message { - if envelope.forCreator == .some(true) { - return .creatorMessages(.id(message.projectId), messageThreadId: message.messageThreadId) - } else { - return .messages(messageThreadId: message.messageThreadId) - } + return .messages(messageThreadId: message.messageThreadId) } if let survey = envelope.survey { @@ -1132,9 +1077,6 @@ private func navigation(fromPushEnvelope envelope: PushEnvelope) -> Navigation? // Figures out a `Navigation` to route the user to from a shortcut item. private func navigation(fromShortcutItem shortcutItem: ShortcutItem) -> SignalProducer { switch shortcutItem { - case .creatorDashboard: - return SignalProducer(value: .tab(.dashboard(project: nil))) - case .recommendedForYou: let params = .defaults |> DiscoveryParams.lens.recommended .~ true @@ -1179,14 +1121,10 @@ private func shortcutItems(forUser user: User?) -> SignalProducer<[ShortcutItem] // Figures out which shortcut items to show to a user based on whether they are a project member and/or // has recommendations. -private func shortcutItems(isProjectMember: Bool, hasRecommendations: Bool) +private func shortcutItems(isProjectMember _: Bool, hasRecommendations: Bool) -> [ShortcutItem] { var items: [ShortcutItem] = [] - if isProjectMember { - items.append(.creatorDashboard) - } - if hasRecommendations { items.append(.recommendedForYou) } @@ -1208,14 +1146,6 @@ private func dictionary(fromUrlComponents urlComponents: URLComponents) -> [Stri extension ShortcutItem { public var applicationShortcutItem: UIApplicationShortcutItem { switch self { - case .creatorDashboard: - return .init( - type: self.typeString, - localizedTitle: Strings.accessibility_discovery_buttons_creator_dashboard(), - localizedSubtitle: nil, - icon: UIApplicationShortcutIcon(templateImageName: "shortcut-icon-bars"), - userInfo: nil - ) case .projectsWeLove: return .init( type: self.typeString, diff --git a/Kickstarter-iOS/AppDelegateViewModelTests.swift b/Kickstarter-iOS/AppDelegateViewModelTests.swift index 232e36d7af..2141491825 100644 --- a/Kickstarter-iOS/AppDelegateViewModelTests.swift +++ b/Kickstarter-iOS/AppDelegateViewModelTests.swift @@ -22,9 +22,7 @@ final class AppDelegateViewModelTests: TestCase { private let findRedirectUrl = TestObserver() private let forceLogout = TestObserver<(), Never>() private let goToActivity = TestObserver<(), Never>() - private let goToDashboard = TestObserver() private let goToDiscovery = TestObserver() - private let goToProjectActivities = TestObserver() private let goToLoginWithIntent = TestObserver() private let goToProfile = TestObserver<(), Never>() private let goToMobileSafari = TestObserver() @@ -71,12 +69,10 @@ final class AppDelegateViewModelTests: TestCase { self.vm.outputs.findRedirectUrl.observe(self.findRedirectUrl.observer) self.vm.outputs.forceLogout.observe(self.forceLogout.observer) self.vm.outputs.goToActivity.observe(self.goToActivity.observer) - self.vm.outputs.goToDashboard.observe(self.goToDashboard.observer) self.vm.outputs.goToDiscovery.observe(self.goToDiscovery.observer) self.vm.outputs.goToLoginWithIntent.observe(self.goToLoginWithIntent.observer) self.vm.outputs.goToProfile.observe(self.goToProfile.observer) self.vm.outputs.goToMobileSafari.observe(self.goToMobileSafari.observer) - self.vm.outputs.goToProjectActivities.observe(self.goToProjectActivities.observer) self.vm.outputs.goToSearch.observe(self.goToSearch.observer) self.vm.outputs.postNotification.map { $0.name }.observe(self.postNotificationName.observer) self.vm.outputs.presentViewController.map { ($0 as! UINavigationController).viewControllers.count } @@ -719,25 +715,6 @@ final class AppDelegateViewModelTests: TestCase { self.goToActivity.assertValueCount(1) } - func testGoToDashboard() { - self.vm.inputs.applicationDidFinishLaunching( - application: UIApplication.shared, - launchOptions: [:] - ) - - self.goToDashboard.assertValueCount(0) - - let url = "https://www.kickstarter.com/projects/tequila/help-me-transform-this-pile-of-wood/dashboard" - let result = self.vm.inputs.applicationOpenUrl( - application: UIApplication.shared, - url: URL(string: url)!, - options: [:] - ) - XCTAssertTrue(result) - - self.goToDashboard.assertValueCount(1) - } - func testGoToDiscovery() { self.vm.inputs.applicationDidFinishLaunching( application: UIApplication.shared, @@ -1195,16 +1172,6 @@ final class AppDelegateViewModelTests: TestCase { } } - func testOpenNotification_NewBacking_ForCreator() { - let projectId = (backingForCreatorPushData["activity"] as? [String: AnyObject]) - .flatMap { $0["project_id"] as? Int } - let param = Param.id(projectId ?? -1) - - self.vm.inputs.didReceive(remoteNotification: backingForCreatorPushData) - - self.goToProjectActivities.assertValues([param]) - } - func testOpenNotification_NewBacking_ForCreator_WithBadData() { var badPushData = backingForCreatorPushData var badActivityData = badPushData["activity"] as? [String: AnyObject] @@ -1212,8 +1179,6 @@ final class AppDelegateViewModelTests: TestCase { badPushData["activity"] = badActivityData self.vm.inputs.didReceive(remoteNotification: badPushData) - - self.goToDashboard.assertValueCount(0) } func testOpenNotification_ProjectUpdate() { @@ -1314,29 +1279,6 @@ final class AppDelegateViewModelTests: TestCase { } } - func testOpenNotification_CreatorActivity() { - let categories: [Activity.Category] = [.backingAmount, .backingCanceled, .backingDropped, .backingReward] - - let projectId = (backingForCreatorPushData["activity"] as? [String: AnyObject]) - .flatMap { $0["project_id"] as? Int } - let param = Param.id(projectId ?? -1) - - self.vm.inputs.applicationDidFinishLaunching( - application: UIApplication.shared, - launchOptions: [:] - ) - - categories.enumerated().forEach { idx, state in - var pushData = genericActivityPushData - pushData["activity"]?["category"] = state.rawValue - - self.vm.inputs.didReceive(remoteNotification: pushData) - - self.goToDashboard.assertValueCount(idx + 1) - self.goToDashboard.assertLastValue(param) - } - } - func testOpenNotification_PostLike() { withEnvironment(apiService: MockService(fetchProjectResult: .success(.template))) { let pushData: [String: Any] = [ @@ -1369,7 +1311,6 @@ final class AppDelegateViewModelTests: TestCase { self.vm.inputs.didReceive(remoteNotification: pushData) - self.goToDashboard.assertValueCount(0) self.goToDiscovery.assertValueCount(0) self.presentViewController.assertValueCount(0) } @@ -1475,38 +1416,11 @@ final class AppDelegateViewModelTests: TestCase { self.scheduler.advance(by: .seconds(5)) self.setApplicationShortcutItems.assertValues([ - [.creatorDashboard, .recommendedForYou, .projectsWeLove, .search] + [.recommendedForYou, .projectsWeLove, .search] ]) } } - func testPerformShortcutItem_CreatorDashboard() { - self.vm.inputs.applicationDidFinishLaunching( - application: UIApplication.shared, - launchOptions: [:] - ) - - self.goToDashboard.assertValueCount(0) - - self.vm.inputs.applicationPerformActionForShortcutItem( - ShortcutItem.creatorDashboard.applicationShortcutItem - ) - - self.goToDashboard.assertValueCount(1) - } - - func testLaunchShortcutItem_CreatorDashboard() { - self.vm.inputs.applicationDidFinishLaunching( - application: UIApplication.shared, - launchOptions: [ - UIApplication.LaunchOptionsKey.shortcutItem: ShortcutItem.creatorDashboard.applicationShortcutItem - ] - ) - - self.goToDashboard.assertValueCount(1) - XCTAssertFalse(self.vm.outputs.applicationDidFinishLaunchingReturnValue) - } - func testPerformShortcutItem_ProjectsWeLove() { self.vm.inputs.applicationDidFinishLaunching( application: UIApplication.shared, diff --git a/Kickstarter-iOS/Assets.xcassets/icons/tabbar-icon-dashboard-selected.imageset/Contents.json b/Kickstarter-iOS/Assets.xcassets/icons/tabbar-icon-dashboard-selected.imageset/Contents.json deleted file mode 100644 index 3e999ecaba..0000000000 --- a/Kickstarter-iOS/Assets.xcassets/icons/tabbar-icon-dashboard-selected.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "icon--graph-line-selected-tab-bar.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - }, - "properties" : { - "template-rendering-intent" : "template" - } -} \ No newline at end of file diff --git a/Kickstarter-iOS/Assets.xcassets/icons/tabbar-icon-dashboard-selected.imageset/icon--graph-line-selected-tab-bar.pdf b/Kickstarter-iOS/Assets.xcassets/icons/tabbar-icon-dashboard-selected.imageset/icon--graph-line-selected-tab-bar.pdf deleted file mode 100644 index 4a6bc77938234fe1a452b64a71985729ede5aaee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4225 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 - } - } - - internal static func instantiate() -> DashboardViewController { - return Storyboard.Dashboard.instantiate(DashboardViewController.self) - } - - internal func `switch`(toProject param: Param) { - self.viewModel.inputs.switch(toProject: param) - } - - internal override func viewDidLoad() { - super.viewDidLoad() - - self.tableView.backgroundView = self.backgroundView - - self.tableView.dataSource = self.dataSource - - let shareButton = UIBarButtonItem() - |> shareBarButtonItemStyle - |> UIBarButtonItem.lens.targetAction .~ (self, #selector(DashboardViewController.shareButtonTapped)) - - self.navigationItem.rightBarButtonItem = shareButton - - self.titleView.delegate = self - - self.viewModel.inputs.viewDidLoad() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - self.viewModel.inputs.viewWillAppear(animated: animated) - - self.showDeprecationWarning() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - self.updateTableViewBottomContentInset() - } - - override func bindStyles() { - _ = self - |> baseTableControllerStyle(estimatedRowHeight: 200.0) - |> UITableViewController.lens.view.backgroundColor .~ .ksr_white - - _ = self.loadingIndicatorView - |> UIActivityIndicatorView.lens.hidesWhenStopped .~ true - |> UIActivityIndicatorView.lens.style .~ .medium - |> UIActivityIndicatorView.lens.color .~ .ksr_support_700 - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - - self.viewModel.inputs.viewWillDisappear() - - self.removeDeprecationWarning() - } - - internal override func bindViewModel() { - super.bindViewModel() - - self.loadingIndicatorView.rac.animating = self.viewModel.outputs.loaderIsAnimating - - self.viewModel.outputs.loaderIsAnimating - .observeForUI() - .observeValues { [weak self] isAnimating in - guard let _self = self else { return } - _self.tableView.tableHeaderView = isAnimating ? _self.loadingIndicatorView : nil - if let headerView = _self.tableView.tableHeaderView { - headerView.frame = CGRect( - x: headerView.frame.origin.x, - y: headerView.frame.origin.y, - width: headerView.frame.size.width, - height: Styles.grid(15) - ) - } - } - - self.viewModel.outputs.fundingData - .observeForUI() - .observeValues { [weak self] stats, project in - self?.dataSource.load(fundingDateStats: stats, project: project) - self?.tableView.reloadData() - } - - self.viewModel.outputs.project - .observeForUI() - .observeValues { [weak self] project in - self?.dataSource.load(project: project) - self?.tableView.reloadData() - - // NB: this is just temporary for now - self?.shareViewModel.inputs.configureWith( - shareContext: .creatorDashboard(project), - shareContextView: nil - ) - } - - self.viewModel.outputs.referrerData - .observeForUI() - .observeValues { [weak self] cumulative, project, aggregates, referrers in - self?.dataSource.load( - cumulative: cumulative, project: project, - aggregate: aggregates, referrers: referrers - ) - self?.tableView.reloadData() - } - - self.viewModel.outputs.rewardData - .observeForUI() - .observeValues { [weak self] stats, project in - self?.dataSource.load(rewardStats: stats, project: project) - self?.tableView.reloadData() - } - - self.viewModel.outputs.videoStats - .observeForUI() - .observeValues { [weak self] videoStats in - self?.dataSource.load(videoStats: videoStats) - self?.tableView.reloadData() - } - - self.viewModel.outputs.presentProjectsDrawer - .observeForControllerAction() - .observeValues { [weak self] data in - self?.presentProjectsDrawer(data: data) - } - - self.viewModel.outputs.animateOutProjectsDrawer - .observeForControllerAction() - .observeValues { [weak self] in - if let drawerVC = self?.presentedViewController as? DashboardProjectsDrawerViewController { - drawerVC.animateOut() - } - } - - self.viewModel.outputs.dismissProjectsDrawer - .observeForControllerAction() - .observeValues { [weak self] in - self?.dismiss(animated: false, completion: nil) - } - - self.viewModel.outputs.updateTitleViewData - .observeForControllerAction() - .observeValues { [weak element = self.titleView] data in - element?.updateData(data) - } - - self.viewModel.outputs.goToMessages - .observeForControllerAction() - .observeValues { [weak self] project in - self?.goToMessages(project: project) - } - - self.viewModel.outputs.goToProject - .observeForControllerAction() - .observeValues { [weak self] project, reftag in - self?.goToProject(project, refTag: reftag) - } - - self.viewModel.outputs.focusScreenReaderOnTitleView - .observeForControllerAction() - .observeValues { [weak self] in - self?.accessibilityFocusOnTitleView() - } - - self.shareViewModel.outputs.showShareSheet - .observeForControllerAction() - .observeValues { [weak self] controller, _ in self?.showShareSheet(controller) } - - self.viewModel.outputs.goToMessageThread - .observeForControllerAction() - .observeValues { [weak self] project, messageThread in - self?.goToMessageThread(project: project, messageThread: messageThread) - } - - self.viewModel.outputs.goToActivities - .observeForControllerAction() - .observeValues { [weak self] project in - self?.goToActivity(project) - } - } - - internal override func tableView(_: UITableView, willDisplay cell: UITableViewCell, forRowAt _: IndexPath) { - if let actionCell = cell as? DashboardActionCell { - actionCell.delegate = self - } else if let referrersCell = cell as? DashboardReferrersCell { - referrersCell.delegate = self - } else if let rewardsCell = cell as? DashboardRewardsCell { - rewardsCell.delegate = self - } - } - - private func updateTableViewBottomContentInset() { - if let deprecationWarningHostingController = deprecationWarningHostingController { - self.tableView.contentInset - .bottom = deprecationWarningHostingController.view.bounds - .height - } - } - - private func removeDeprecationWarning() { - self.deprecationWarningHostingController?.removeFromParent() - - let deprecationWarningView = self.tabBarController?.view.subviews.first(where: { view in - view.viewWithTag(-99) != nil - }) - - deprecationWarningView?.removeFromSuperview() - } - - private func showDeprecationWarning() { - let deprecationWarningHostingController = UIHostingController(rootView: DashboardDeprecationView()) - - guard let tabController = self.tabBarController as? RootTabBarViewController, - let deprecationWarningView = deprecationWarningHostingController.view else { return } - - deprecationWarningView.tag = -99 - tabController.addChild(deprecationWarningHostingController) - tabController.view.addSubview(deprecationWarningView) - - deprecationWarningHostingController.didMove(toParent: tabController) - - deprecationWarningView.translatesAutoresizingMaskIntoConstraints = false - - NSLayoutConstraint - .activate([ - deprecationWarningView.leftAnchor.constraint(equalTo: tabController.view.leftAnchor), - deprecationWarningView.rightAnchor - .constraint(equalTo: tabController.view.rightAnchor), - deprecationWarningView.bottomAnchor.constraint( - equalTo: tabController.tabBar.safeAreaLayoutGuide.topAnchor) - ]) - } - - fileprivate func goToActivity(_ project: Project) { - let vc = ProjectActivitiesViewController.configuredWith(project: project) - self.navigationController?.pushViewController(vc, animated: true) - - self.navigationItem.backBarButtonItem = UIBarButtonItem.back(nil, selector: nil) - } - - internal override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let cell = tableView.cellForRow(at: indexPath) - if cell as? DashboardContextCell != nil { - self.viewModel.inputs.projectContextCellTapped() - } - } - - private func goToMessages(project: Project) { - let vc = MessageThreadsViewController.configuredWith(project: project, refTag: .dashboard) - self.navigationController?.pushViewController(vc, animated: true) - } - - fileprivate func goToPostUpdate(_ project: Project) { - self.viewModel.inputs.trackPostUpdateClicked() - - let vc = UpdateDraftViewController.configuredWith(project: project) - vc.delegate = self - - let nav = UINavigationController(rootViewController: vc) - nav.modalPresentationStyle = .formSheet - - self.present(nav, animated: true, completion: nil) - } - - private func goToProject(_ project: Project, refTag: RefTag) { - let projectParam = Either(left: project) - let vc = ProjectPageViewController.configuredWith( - projectOrParam: projectParam, - refTag: refTag - ) - - let nav = NavigationController(rootViewController: vc) - nav.modalPresentationStyle = self.traitCollection.userInterfaceIdiom == .pad ? .fullScreen : .formSheet - - self.present(nav, animated: true, completion: nil) - } - - private func presentProjectsDrawer(data: [ProjectsDrawerData]) { - let vc = DashboardProjectsDrawerViewController.configuredWith(data: data) - vc.delegate = self - self.modalPresentationStyle = .overCurrentContext - self.present(vc, animated: false, completion: nil) - } - - private func showShareSheet(_ controller: UIActivityViewController) { - if UIDevice.current.userInterfaceIdiom == .pad { - controller.modalPresentationStyle = .popover - controller.popoverPresentationController?.barButtonItem = self.navigationItem.rightBarButtonItem - self.present(controller, animated: true, completion: nil) - - } else { - self.present(controller, animated: true, completion: nil) - } - } - - private func accessibilityFocusOnTitleView() { - UIAccessibility.post(notification: UIAccessibility.Notification.screenChanged, argument: self.titleView) - } - - @objc fileprivate func shareButtonTapped() { - self.shareViewModel.inputs.shareButtonTapped() - } - - private func goToMessageThread(project: Project, messageThread: MessageThread) { - let threadsVC = MessageThreadsViewController.configuredWith(project: project, refTag: .dashboard) - let messageThreadVC = MessagesViewController.configuredWith(messageThread: messageThread) - - self.navigationController?.setViewControllers([self, threadsVC, messageThreadVC], animated: true) - } - - public func navigateToProjectMessageThread(projectId: Param, messageThread: MessageThread) { - self.viewModel.inputs.messageThreadNavigated(projectId: projectId, messageThread: messageThread) - } - - public func navigateToProjectActivities(projectId: Param) { - self.viewModel.inputs.activitiesNavigated(projectId: projectId) - } -} - -extension DashboardViewController: DashboardActionCellDelegate { - internal func goToActivity(_: DashboardActionCell?, project: Project) { - self.goToActivity(project) - } - - internal func goToMessages(_: DashboardActionCell?) { - self.viewModel.inputs.messagesCellTapped() - } - - internal func goToPostUpdate(_: DashboardActionCell?, project: Project) { - self.goToPostUpdate(project) - } -} - -extension DashboardViewController: UpdateDraftViewControllerDelegate { - func updateDraftViewControllerWantsDismissal(_: UpdateDraftViewController) { - self.dismiss(animated: true, completion: nil) - } -} - -extension DashboardViewController: DashboardReferrersCellDelegate { - func dashboardReferrersCellDidAddReferrerRows(_: DashboardReferrersCell?) { - let inset = self.tableView.contentInset - self.tableView.contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 1_000, right: 0.0) - - self.tableView.beginUpdates() - self.tableView.endUpdates() - - self.tableView.contentInset = inset - } -} - -extension DashboardViewController: DashboardRewardsCellDelegate { - func dashboardRewardsCellDidAddRewardRows(_: DashboardRewardsCell?) { - self.tableView.beginUpdates() - self.tableView.endUpdates() - } -} - -extension DashboardViewController: DashboardProjectsDrawerViewControllerDelegate { - func dashboardProjectsDrawerCellDidTapProject(_ project: Project) { - self.viewModel.inputs.switch(toProject: .id(project.id)) - } - - func dashboardProjectsDrawerDidAnimateOut() { - self.viewModel.inputs.dashboardProjectsDrawerDidAnimateOut() - } - - func dashboardProjectsDrawerHideDrawer() { - self.viewModel.inputs.showHideProjectsDrawer() - } -} - -extension DashboardViewController: DashboardTitleViewDelegate { - func dashboardTitleViewShowHideProjectsDrawer() { - self.viewModel.inputs.showHideProjectsDrawer() - } -} - -extension DashboardViewController: TabBarControllerScrollable {} diff --git a/Kickstarter-iOS/Features/Dashboard/Controller/DashboardViewControllerTests.swift b/Kickstarter-iOS/Features/Dashboard/Controller/DashboardViewControllerTests.swift deleted file mode 100644 index 2a51b6bb13..0000000000 --- a/Kickstarter-iOS/Features/Dashboard/Controller/DashboardViewControllerTests.swift +++ /dev/null @@ -1,134 +0,0 @@ -@testable import Kickstarter_Framework -@testable import KsApi -@testable import Library -import Prelude -import SnapshotTesting -import XCTest - -internal final class DashboardViewControllerTests: TestCase { - override func setUp() { - super.setUp() - let project = cosmicSurgery - |> Project.lens.dates.launchedAt .~ (self.dateType.init().timeIntervalSince1970 - 60 * 60 * 24 * 14) - |> Project.lens.dates.deadline .~ (self.dateType.init().timeIntervalSince1970 + 60 * 60 * 24 * 14) - - AppEnvironment.pushEnvironment( - apiService: MockService( - fetchProjectsResponse: [project], - - fetchProjectStatsResponse: .template - |> ProjectStatsEnvelope.lens.cumulativeStats .~ cumulativeStats - |> ProjectStatsEnvelope.lens.referralDistribution .~ referrerStats - |> ProjectStatsEnvelope.lens.referralAggregateStats .~ referralAggregateStats - |> ProjectStatsEnvelope.lens.rewardDistribution .~ rewardStats - |> ProjectStatsEnvelope.lens.videoStats .~ videoStats - |> ProjectStatsEnvelope.lens.fundingDistribution .~ fundingStats - ), - currentUser: project.creator, - mainBundle: Bundle.framework - ) - - UIView.setAnimationsEnabled(false) - } - - override func tearDown() { - AppEnvironment.popEnvironment() - UIView.setAnimationsEnabled(true) - super.tearDown() - } - - func testView() { - combos(Language.allLanguages, [Device.phone4_7inch, Device.phone5_8inch, Device.pad]).forEach { - language, device in - withEnvironment(language: language) { - let controller = DashboardViewController.instantiate() - let (parent, _) = traitControllers(device: device, orientation: .portrait, child: controller) - - parent.view.frame.size.height = device == .pad ? 2_150 : 2_000 - - self.scheduler.run() - - assertSnapshot(matching: parent.view, as: .image, named: "lang_\(language)_device_\(device)") - } - } - } - - func testScrollToTop() { - let controller = ActivitiesViewController.instantiate() - - XCTAssertNotNil(controller.view as? UIScrollView) - } -} - -private let rewards = (1...6).map { - .template - |> Reward.lens.backersCount .~ ($0 * 5) - |> Reward.lens.id .~ $0 - |> Reward.lens.minimum .~ Double($0 * 4) -} - -private let externalReferrerStats = .template - |> ProjectStatsEnvelope.ReferrerStats.lens.backersCount .~ 5 - |> ProjectStatsEnvelope.ReferrerStats.lens.code .~ "direct" - |> ProjectStatsEnvelope.ReferrerStats.lens.percentageOfDollars .~ 0.25 - |> ProjectStatsEnvelope.ReferrerStats.lens.pledged .~ 25.0 - |> ProjectStatsEnvelope.ReferrerStats.lens.referrerName .~ "Direct traffic" - |> ProjectStatsEnvelope.ReferrerStats.lens.referrerType .~ .external - -private let internalReferrerStats = .template - |> ProjectStatsEnvelope.ReferrerStats.lens.backersCount .~ 10 - |> ProjectStatsEnvelope.ReferrerStats.lens.code .~ "search" - |> ProjectStatsEnvelope.ReferrerStats.lens.percentageOfDollars .~ 0.4 - |> ProjectStatsEnvelope.ReferrerStats.lens.pledged .~ 40.0 - |> ProjectStatsEnvelope.ReferrerStats.lens.referrerName .~ "Search" - |> ProjectStatsEnvelope.ReferrerStats.lens.referrerType .~ .internal - -private let customReferrerStats = .template - |> ProjectStatsEnvelope.ReferrerStats.lens.backersCount .~ 25 - |> ProjectStatsEnvelope.ReferrerStats.lens.code .~ "dfg" - |> ProjectStatsEnvelope.ReferrerStats.lens.percentageOfDollars .~ 0.35 - |> ProjectStatsEnvelope.ReferrerStats.lens.pledged .~ 35.0 - |> ProjectStatsEnvelope.ReferrerStats.lens.referrerName .~ "Dfg" - |> ProjectStatsEnvelope.ReferrerStats.lens.referrerType .~ .custom - -private let referrerStats = [externalReferrerStats, internalReferrerStats, customReferrerStats] - -private let rewardStats = (1...6).map { - .template - |> ProjectStatsEnvelope.RewardStats.lens.backersCount .~ ($0 * 5) - |> ProjectStatsEnvelope.RewardStats.lens.id .~ $0 - |> ProjectStatsEnvelope.RewardStats.lens.minimum .~ Double($0 * 4) - |> ProjectStatsEnvelope.RewardStats.lens.pledged .~ ($0 * $0 * 4 * 5) -} - -private let videoStats = .template - |> ProjectStatsEnvelope.VideoStats.lens.externalCompletions .~ 51 - |> ProjectStatsEnvelope.VideoStats.lens.externalStarts .~ 212 - |> ProjectStatsEnvelope.VideoStats.lens.internalCompletions .~ 751 - |> ProjectStatsEnvelope.VideoStats.lens.internalStarts .~ 1_000 - -private let referralAggregateStats = .template - |> ProjectStatsEnvelope.ReferralAggregateStats.lens.external .~ 455.00 - |> ProjectStatsEnvelope.ReferralAggregateStats.lens.kickstarter .~ 728.00 - |> ProjectStatsEnvelope.ReferralAggregateStats.lens.custom .~ 637.00 - -private let cumulativeStats = .template - |> ProjectStatsEnvelope.CumulativeStats.lens.pledged .~ rewardStats.reduce(0) { $0 + $1.pledged } - |> ProjectStatsEnvelope.CumulativeStats.lens.averagePledge .~ 5 - -private let cosmicSurgery = .cosmicSurgery - |> Project.lens.stats.pledged .~ cumulativeStats.pledged - |> Project.lens.memberData.lastUpdatePublishedAt .~ 1_477_581_146 - |> Project.lens.memberData.unreadMessagesCount .~ .some(42) - |> Project.lens.memberData.unseenActivityCount .~ .some(1_299) - |> Project.lens.memberData.permissions .~ [.post, .viewPledges] - |> Project.lens.rewardData.rewards .~ rewards - -private let stats = [3_000, 4_000, 5_000, 7_000, 8_000, 13_000, 14_000, 15_000, 17_000, 18_000] - -private let fundingStats = stats.enumerated().map { idx, pledged in - .template - |> ProjectStatsEnvelope.FundingDateStats.lens.cumulativePledged .~ pledged - |> ProjectStatsEnvelope.FundingDateStats.lens.date - .~ ((cosmicSurgery.dates.launchedAt ?? 0) + TimeInterval(idx * 86_400)) -} diff --git a/Kickstarter-iOS/Features/Dashboard/Controller/UpdateDraftViewController.swift b/Kickstarter-iOS/Features/Dashboard/Controller/UpdateDraftViewController.swift deleted file mode 100644 index c84ef66ab4..0000000000 --- a/Kickstarter-iOS/Features/Dashboard/Controller/UpdateDraftViewController.swift +++ /dev/null @@ -1,365 +0,0 @@ -import AlamofireImage -import KsApi -import Library -import Prelude -import Prelude_UIKit -import SafariServices -import UIKit - -internal protocol UpdateDraftViewControllerDelegate: AnyObject { - func updateDraftViewControllerWantsDismissal(_ updateDraftViewController: UpdateDraftViewController) -} - -internal final class UpdateDraftViewController: UIViewController { - fileprivate let viewModel: UpdateDraftViewModelType = UpdateDraftViewModel() - internal weak var delegate: UpdateDraftViewControllerDelegate? - - @IBOutlet fileprivate var addAttachmentButton: UIButton! - @IBOutlet fileprivate var addAttachmentExpandedButton: UIButton! - @IBOutlet fileprivate var attachmentsSeparatorView: UIView! - @IBOutlet fileprivate var attachmentsScrollView: UIScrollView! - @IBOutlet fileprivate var attachmentsStackView: UIStackView! - @IBOutlet fileprivate var bodyPlaceholderTextView: UITextView! - @IBOutlet fileprivate var bodyTextView: UITextView! - @IBOutlet fileprivate var bottomConstraint: NSLayoutConstraint! - @IBOutlet fileprivate var closeBarButtonItem: UIBarButtonItem! - @IBOutlet fileprivate var isBackersOnlyButton: UIButton! - @IBOutlet fileprivate var previewBarButtonItem: UIBarButtonItem! - @IBOutlet fileprivate var titleTextField: UITextField! - - @IBOutlet fileprivate var separatorViews: [UIView]! - - internal static func configuredWith(project: Project) -> UpdateDraftViewController { - let vc = Storyboard.UpdateDraft.instantiate(UpdateDraftViewController.self) - vc.viewModel.inputs.configureWith(project: project) - return vc - } - - internal override func bindStyles() { - super.bindStyles() - - _ = self - |> baseControllerStyle() - - self.navigationItem.backBarButtonItem = UIBarButtonItem.back(nil, selector: nil) - - _ = self.closeBarButtonItem |> updateDraftCloseBarButtonItemStyle - _ = self.previewBarButtonItem |> updateDraftPreviewBarButtonItemStyle - - _ = self.addAttachmentExpandedButton |> updateAddAttachmentExpandedButtonStyle - _ = self.attachmentsScrollView |> updateAttachmentsScrollViewStyle - _ = self.attachmentsStackView |> updateAttachmentsStackViewStyle - _ = self.addAttachmentButton |> updateAddAttachmentButtonStyle - _ = self.bodyPlaceholderTextView |> updateBodyPlaceholderTextViewStyle - _ = self.bodyTextView |> updateBodyTextViewStyle - _ = self.isBackersOnlyButton |> updateBackersOnlyButtonStyle - _ = self.titleTextField |> updateTitleTextFieldStyle - - _ = self.separatorViews ||> separatorStyle - } - - internal override func bindViewModel() { - super.bindViewModel() - - self.addAttachmentExpandedButton.rac.hidden = - self.viewModel.outputs.isAttachmentsSectionHidden.map(negate) - self.attachmentsSeparatorView.rac.hidden = self.viewModel.outputs.isAttachmentsSectionHidden - self.attachmentsStackView.rac.hidden = self.viewModel.outputs.isAttachmentsSectionHidden - self.bodyPlaceholderTextView.rac.hidden = self.viewModel.outputs.isBodyPlaceholderHidden - self.bodyTextView.rac.text = self.viewModel.outputs.body - self.bodyTextView.rac.becomeFirstResponder = self.viewModel.outputs.bodyTextViewBecomeFirstResponder - self.isBackersOnlyButton.rac.selected = self.viewModel.outputs.isBackersOnly - self.navigationItem.rac.title = self.viewModel.outputs.navigationItemTitle - self.previewBarButtonItem.rac.enabled = self.viewModel.outputs.isPreviewButtonEnabled - self.titleTextField.rac.text = self.viewModel.outputs.title - self.titleTextField.rac.becomeFirstResponder = self.viewModel.outputs.titleTextFieldBecomeFirstResponder - self.view.rac.endEditing = self.viewModel.outputs.resignFirstResponder - - self.viewModel.outputs.attachments - .observeForControllerAction() - .observeValues { [weak self] attachments in - guard let attachmentsStackView = self?.attachmentsStackView else { return } - _ = attachmentsStackView |> - UIStackView.lens.arrangedSubviews .~ attachments.compactMap { self?.imageView(forAttachment: $0) } - } - - self.viewModel.outputs.notifyPresenterViewControllerWantsDismissal - .observeForControllerAction() - .observeValues { [weak self] in - guard let _self = self else { return } - _self.delegate?.updateDraftViewControllerWantsDismissal(_self) - } - - self.viewModel.outputs.showAttachmentActions - .observeForControllerAction() - .observeValues { [weak self] actions in self?.showAttachmentActions(actions) } - - self.viewModel.outputs.showImagePicker - .observeForControllerAction() - .observeValues { [weak self] action in self?.showImagePicker(forAction: action) } - - self.viewModel.outputs.attachmentAdded - .observeForControllerAction() - .observeValues { [weak self] attachment in - guard let _self = self, let scrollView = _self.attachmentsScrollView else { return } - let imageView = _self.imageView(forAttachment: attachment) - _self.attachmentsStackView.addArrangedSubview(imageView) - - after(0.1) { - let offset = scrollView.contentSize.width - scrollView.bounds.size.width - guard offset >= scrollView.contentOffset.x else { return } - scrollView.setContentOffset(CGPoint(x: offset, y: 0), animated: true) - } - } - - self.viewModel.outputs.showRemoveAttachmentConfirmation - .observeForControllerAction() - .observeValues { [weak self] attachment in self?.showRemoveAttachmentAlert(attachment) } - - self.viewModel.outputs.attachmentRemoved - .observeForControllerAction() - .observeValues { [weak self] attachment in - guard let _self = self else { return } - UIView.animate(withDuration: 0.2) { - _self.attachmentsStackView.viewWithTag(attachment.id)?.removeFromSuperview() - } - } - - self.viewModel.outputs.goToPreview - .observeForControllerAction() - .observeValues { [weak self] draft in - let vc = UpdatePreviewViewController.configuredWith(draft: draft) - self?.navigationController?.pushViewController(vc, animated: true) - } - - self.viewModel.outputs.showAddAttachmentFailure - .observeForControllerAction() - .observeValues { [weak self] in - let alert = UIAlertController - .genericError(Strings.Couldnt_add_attachment()) - self?.present(alert, animated: true, completion: nil) - } - - self.viewModel.outputs.showRemoveAttachmentFailure - .observeForControllerAction() - .observeValues { [weak self] in - let alert = UIAlertController - .genericError(Strings.Couldnt_remove_attachment()) - self?.present(alert, animated: true, completion: nil) - } - - self.viewModel.outputs.showSaveFailure - .observeForControllerAction() - .observeValues { [weak self] in - let alert = UIAlertController - .genericError(Strings.dashboard_post_update_compose_error_could_not_save_update()) - self?.present(alert, animated: true, completion: nil) - } - - Keyboard.change.observeForUI() - .observeValues { [weak self] in self?.animateBottomConstraint($0) } - } - - internal override func viewDidLoad() { - super.viewDidLoad() - - self.addAttachmentButton.addTarget( - self, action: #selector(self.addAttachmentButtonTapped), - for: .touchUpInside - ) - self.addAttachmentExpandedButton.addTarget( - self, action: #selector(self.addAttachmentButtonTapped), - for: .touchUpInside - ) - self.bodyTextView.delegate = self - self.isBackersOnlyButton.addTarget( - self, action: #selector(self.isBackersOnlyButtonTapped), - for: .touchUpInside - ) - self.titleTextField.addTarget( - self, action: #selector(self.titleTextFieldDidChange), - for: .editingChanged - ) - self.titleTextField.addTarget( - self, action: #selector(self.titleTextFieldDoneEditing), - for: .editingDidEndOnExit - ) - - self.viewModel.inputs.viewDidLoad() - } - - internal override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - - self.viewModel.inputs.viewWillDisappear() - } - - @IBAction fileprivate func closeButtonTapped() { - self.viewModel.inputs.closeButtonTapped() - } - - @IBAction fileprivate func previewButtonTapped() { - self.viewModel.inputs.previewButtonTapped() - } - - @objc fileprivate func isBackersOnlyButtonTapped() { - self.viewModel.inputs.isBackersOnlyOn(!self.isBackersOnlyButton.isSelected) - } - - @objc fileprivate func addAttachmentButtonTapped() { - self.viewModel.inputs.addAttachmentButtonTapped( - availableSources: [.camera, .cameraRoll] - .filter { UIImagePickerController.isSourceTypeAvailable($0.sourceType) } - ) - } - - @objc fileprivate func titleTextFieldDidChange() { - self.viewModel.inputs.titleTextChanged(to: self.titleTextField.text ?? "") - } - - @objc fileprivate func titleTextFieldDoneEditing() { - self.viewModel.inputs.titleTextFieldDoneEditing() - } - - fileprivate func showAttachmentActions(_ actions: [AttachmentSource]) { - let attachmentSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - - for action in actions { - attachmentSheet.addAction(.init(title: action.title, style: .default, handler: { [weak self] _ in - self?.viewModel.inputs.addAttachmentSheetButtonTapped(action) - })) - } - attachmentSheet.addAction(.init( - title: Strings.dashboard_post_update_compose_attachment_buttons_cancel(), - style: .cancel, - handler: nil - )) - - // iPad provision - attachmentSheet.popoverPresentationController?.barButtonItem = self.navigationItem.rightBarButtonItem - - self.present(attachmentSheet, animated: true, completion: nil) - } - - fileprivate func showImagePicker(forAction action: AttachmentSource) { - let picker = UIImagePickerController() - picker.delegate = self - picker.sourceType = action.sourceType - self.present(picker, animated: true, completion: nil) - } - - fileprivate func animateBottomConstraint(_ change: Keyboard.Change) { - UIView.animate(withDuration: change.duration, delay: 0.0, options: change.options, animations: { - self.bottomConstraint.constant = self.view.frame.maxY - change.frame.minY - }, completion: nil) - } - - fileprivate func imageView(forAttachment attachment: UpdateDraft.Attachment) -> UIImageView { - let imageView = UIImageView() |> updateAttachmentsThumbStyle - |> UIImageView.lens.tag .~ attachment.id - - imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true - if let url = URL(string: attachment.thumbUrl) { - imageView.ksr_setImageWithURL(url) - } - - let tap = UITapGestureRecognizer(target: self, action: #selector(self.attachmentTapped)) - tap.cancelsTouchesInView = false - imageView.addGestureRecognizer(tap) - - return imageView - } - - @objc fileprivate func attachmentTapped(_ tap: UITapGestureRecognizer) { - guard let id = tap.view?.tag else { return } - self.viewModel.inputs.attachmentTapped(id: id) - } - - fileprivate func showRemoveAttachmentAlert(_ attachment: UpdateDraft.Attachment) { - let alert = UIAlertController( - title: Strings.dashboard_post_update_compose_attachment_alerts_image_remove_image(), - message: Strings - .dashboard_post_update_compose_attachment_alerts_image_are_you_sure_you_want_to_remove_image(), - preferredStyle: .alert - ) - alert.addAction( - UIAlertAction( - title: Strings.dashboard_post_update_compose_attachment_alerts_image_buttons_remove(), - style: .destructive - ) { [weak self] _ in - self?.viewModel.inputs.remove(attachment: attachment) - } - ) - alert.addAction( - UIAlertAction( - title: Strings.dashboard_post_update_compose_attachment_alerts_image_buttons_cancel(), - style: .cancel, - handler: nil - ) - ) - self.present(alert, animated: true, completion: nil) - } -} - -extension UpdateDraftViewController: UITextViewDelegate { - internal func textViewDidChange(_ textView: UITextView) { - self.viewModel.inputs.bodyTextChanged(to: textView.text) - } -} - -extension UpdateDraftViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { - @objc internal func imagePickerController( - _ picker: UIImagePickerController, - didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any] - ) { - // Local variable inserted by Swift 4.2 migrator. - let info = convertFromUIImagePickerControllerInfoKeyDictionary(info) - - guard - let image = info[ - convertFromUIImagePickerControllerInfoKey(UIImagePickerController.InfoKey.originalImage) - ] as? UIImage, - let imageData = image.jpegData(compressionQuality: 0.9), - let caches = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first - else { fatalError() } - - let file = URL(fileURLWithPath: caches).appendingPathComponent("\(image.hash).jpg") - try? imageData.write(to: file, options: [.atomic]) - - self.viewModel.inputs.imagePicked( - url: file, - fromSource: AttachmentSource(sourceType: picker.sourceType) - ) - picker.dismiss(animated: true, completion: nil) - } - - @objc internal func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { - self.viewModel.inputs.imagePickerCanceled() - picker.dismiss(animated: true, completion: nil) - } -} - -private func after( - _ seconds: TimeInterval, - queue: DispatchQueue = DispatchQueue.main, - body: @escaping () -> Void -) { - queue.asyncAfter( - deadline: DispatchTime.now() + Double(Int64(seconds * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), - execute: body - ) -} - -// Helper function inserted by Swift 4.2 migrator. -private func convertFromUIImagePickerControllerInfoKeyDictionary( - _ input: [UIImagePickerController.InfoKey: Any] -) -> [String: Any] { - return Dictionary(uniqueKeysWithValues: input.map { key, value in (key.rawValue, value) }) -} - -// Helper function inserted by Swift 4.2 migrator. -private func convertFromUIImagePickerControllerInfoKey( - _ input: UIImagePickerController.InfoKey -) -> String { - return input.rawValue -} diff --git a/Kickstarter-iOS/Features/Dashboard/Controller/UpdatePreviewViewController.swift b/Kickstarter-iOS/Features/Dashboard/Controller/UpdatePreviewViewController.swift deleted file mode 100644 index 9d88bf774a..0000000000 --- a/Kickstarter-iOS/Features/Dashboard/Controller/UpdatePreviewViewController.swift +++ /dev/null @@ -1,100 +0,0 @@ -import KsApi -import Library -import Prelude -import UIKit -import WebKit - -internal final class UpdatePreviewViewController: WebViewController { - fileprivate let viewModel: UpdatePreviewViewModelType = UpdatePreviewViewModel() - - @IBOutlet fileprivate var publishBarButtonItem: UIBarButtonItem! - - internal static func configuredWith(draft: UpdateDraft) -> UpdatePreviewViewController { - let vc = Storyboard.UpdateDraft.instantiate(UpdatePreviewViewController.self) - vc.viewModel.inputs.configureWith(draft: draft) - return vc - } - - internal override func viewDidLoad() { - super.viewDidLoad() - - _ = self.publishBarButtonItem - |> UIBarButtonItem.lens.targetAction .~ (self, #selector(self.publishButtonTapped)) - - self.viewModel.inputs.viewDidLoad() - } - - internal override func bindStyles() { - _ = self |> baseControllerStyle() - _ = self.publishBarButtonItem |> updatePreviewBarButtonItemStyle - - self.navigationItem.title = nil - } - - internal override func bindViewModel() { - self.viewModel.outputs.webViewLoadRequest - .observeForControllerAction() - .observeValues { [weak self] in _ = self?.webView.load($0) } - - self.viewModel.outputs.showPublishConfirmation - .observeForControllerAction() - .observeValues { [weak self] in self?.showPublishConfirmation(message: $0) } - - self.viewModel.outputs.showPublishFailure - .observeForControllerAction() - .observeValues { [weak self] in self?.showPublishFailure() } - - self.viewModel.outputs.goToUpdate - .observeForControllerAction() - .observeValues { [weak self] in self?.goTo(update: $1, forProject: $0) } - } - - internal func webView( - _: WKWebView, - decidePolicyFor navigationAction: WKNavigationAction, - decisionHandler: @escaping (WKNavigationActionPolicy) -> Void - ) { - decisionHandler( - self.viewModel.inputs.decidePolicyFor(navigationAction: .init(navigationAction: navigationAction)) - ) - } - - @objc fileprivate func publishButtonTapped() { - self.viewModel.inputs.publishButtonTapped() - } - - fileprivate func showPublishConfirmation(message: String) { - let alert = UIAlertController( - title: Strings.dashboard_post_update_preview_confirmation_alert_title(), - message: message, - preferredStyle: .alert - ) - alert.addAction( - UIAlertAction( - title: Strings.dashboard_post_update_preview_confirmation_alert_confirm_button(), - style: .default - ) { _ in - self.viewModel.inputs.publishConfirmationButtonTapped() - } - ) - alert.addAction( - UIAlertAction( - title: Strings.dashboard_post_update_preview_confirmation_alert_cancel_button(), - style: .cancel, - handler: nil - ) - ) - self.present(alert, animated: true, completion: nil) - } - - fileprivate func showPublishFailure() { - let alert = UIAlertController - .genericError(Strings.dashboard_post_update_preview_confirmation_alert_error_something_wrong()) - self.present(alert, animated: true, completion: nil) - } - - fileprivate func goTo(update: Update, forProject project: Project) { - let vc = UpdateViewController.configuredWith(project: project, update: update, context: .draftPreview) - self.navigationController?.setViewControllers([vc], animated: true) - } -} diff --git a/Kickstarter-iOS/Features/Dashboard/Datasource/DashboardDataSource.swift b/Kickstarter-iOS/Features/Dashboard/Datasource/DashboardDataSource.swift deleted file mode 100644 index 27abe63d25..0000000000 --- a/Kickstarter-iOS/Features/Dashboard/Datasource/DashboardDataSource.swift +++ /dev/null @@ -1,84 +0,0 @@ -import Foundation -import KsApi -import Library -import UIKit - -internal final class DashboardDataSource: ValueCellDataSource { - fileprivate enum Section: Int { - case context - case action - case fundingProgress - case rewards - case referrers - case video - } - - internal func load(project: Project) { - self.clearValues() - - self.set(values: [project], cellClass: DashboardContextCell.self, inSection: Section.context.rawValue) - - self.set(values: [project], cellClass: DashboardActionCell.self, inSection: Section.action.rawValue) - } - - internal func load(fundingDateStats stats: [ProjectStatsEnvelope.FundingDateStats], project: Project) { - self.set( - values: [(stats, project)], - cellClass: DashboardFundingCell.self, - inSection: Section.fundingProgress.rawValue - ) - } - - internal func load( - cumulative: ProjectStatsEnvelope.CumulativeStats, - project: Project, - aggregate: ProjectStatsEnvelope.ReferralAggregateStats, - referrers: [ProjectStatsEnvelope.ReferrerStats] - ) { - self.set( - values: [(cumulative, project, aggregate, referrers)], cellClass: DashboardReferrersCell.self, - inSection: Section.referrers.rawValue - ) - } - - internal func load( - rewardStats: [ProjectStatsEnvelope.RewardStats], - project: Project - ) { - self.set( - values: [(rewardStats: rewardStats, project: project)], cellClass: DashboardRewardsCell.self, - inSection: Section.rewards.rawValue - ) - } - - internal func load(videoStats: ProjectStatsEnvelope.VideoStats) { - self.set(values: [videoStats], cellClass: DashboardVideoCell.self, inSection: Section.video.rawValue) - } - - internal override func configureCell(tableCell cell: UITableViewCell, withValue value: Any) { - switch (cell, value) { - case let (cell as DashboardContextCell, value as Project): - cell.configureWith(value: value) - case let (cell as DashboardActionCell, value as Project): - cell.configureWith(value: value) - case let (cell as DashboardFundingCell, value as ([ProjectStatsEnvelope.FundingDateStats], Project)): - cell.configureWith(value: value) - case let (cell as DashboardReferrersCell, value as ( - ProjectStatsEnvelope.CumulativeStats, Project, - ProjectStatsEnvelope.ReferralAggregateStats, [ProjectStatsEnvelope.ReferrerStats] - )): - cell.configureWith(value: value) - case let (cell as DashboardVideoCell, value as ProjectStatsEnvelope.VideoStats): - cell.configureWith(value: value) - case let ( - cell as DashboardRewardsCell, - value as ([ProjectStatsEnvelope.RewardStats], Project) - ): - cell.configureWith(value: value) - case (is StaticTableViewCell, is Void): - return - default: - assertionFailure("Unrecognized (\(type(of: cell)), \(type(of: value))) combo.") - } - } -} diff --git a/Kickstarter-iOS/Features/Dashboard/Datasource/DashboardDataSourceTests.swift b/Kickstarter-iOS/Features/Dashboard/Datasource/DashboardDataSourceTests.swift deleted file mode 100644 index 50626123e1..0000000000 --- a/Kickstarter-iOS/Features/Dashboard/Datasource/DashboardDataSourceTests.swift +++ /dev/null @@ -1,41 +0,0 @@ -@testable import Kickstarter_Framework -@testable import KsApi -@testable import Library -import Prelude -import XCTest - -internal final class DashboardDataSourceTests: XCTestCase { - let dataSource = DashboardDataSource() - let tableView = UITableView() - - func testDataSource() { - let project = Project.template - - XCTAssertEqual(0, self.dataSource.numberOfSections(in: self.tableView)) - - self.dataSource.load(project: project) - - XCTAssertEqual(2, self.dataSource.numberOfSections(in: self.tableView)) - XCTAssertEqual(1, self.dataSource.tableView(self.tableView, numberOfRowsInSection: 0)) - XCTAssertEqual(1, self.dataSource.tableView(self.tableView, numberOfRowsInSection: 1)) - XCTAssertEqual("DashboardContextCell", self.dataSource.reusableId(item: 0, section: 0)) - XCTAssertEqual("DashboardActionCell", self.dataSource.reusableId(item: 0, section: 1)) - XCTAssertEqual(project, self.dataSource[itemSection: (0, 0)] as? Project) - - let rewardStats = [ProjectStatsEnvelope.RewardStats.template] - - self.dataSource.load(rewardStats: rewardStats, project: project) - - XCTAssertEqual(4, self.dataSource.numberOfSections(in: self.tableView)) - XCTAssertEqual(1, self.dataSource.tableView(self.tableView, numberOfRowsInSection: 3)) - XCTAssertEqual("DashboardRewardsCell", self.dataSource.reusableId(item: 0, section: 3)) - - let videoStats = ProjectStatsEnvelope.VideoStats.template - - self.dataSource.load(videoStats: videoStats) - - XCTAssertEqual(6, self.dataSource.numberOfSections(in: self.tableView)) - XCTAssertEqual(1, self.dataSource.tableView(self.tableView, numberOfRowsInSection: 5)) - XCTAssertEqual("DashboardVideoCell", self.dataSource.reusableId(item: 0, section: 5)) - } -} diff --git a/Kickstarter-iOS/Features/Dashboard/Storyboard/Dashboard.storyboard b/Kickstarter-iOS/Features/Dashboard/Storyboard/Dashboard.storyboard deleted file mode 100644 index 86829b3e18..0000000000 --- a/Kickstarter-iOS/Features/Dashboard/Storyboard/Dashboard.storyboard +++ /dev/null @@ -1,1035 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Kickstarter-iOS/Features/Dashboard/Storyboard/UpdateDraft.storyboard b/Kickstarter-iOS/Features/Dashboard/Storyboard/UpdateDraft.storyboard deleted file mode 100644 index 9ff38ab8b9..0000000000 --- a/Kickstarter-iOS/Features/Dashboard/Storyboard/UpdateDraft.storyboard +++ /dev/null @@ -1,214 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Kickstarter-iOS/Features/Dashboard/ViewModel/UpdatePreviewViewModel.swift b/Kickstarter-iOS/Features/Dashboard/ViewModel/UpdatePreviewViewModel.swift deleted file mode 100644 index 6814315214..0000000000 --- a/Kickstarter-iOS/Features/Dashboard/ViewModel/UpdatePreviewViewModel.swift +++ /dev/null @@ -1,138 +0,0 @@ -import KsApi -import Library -import ReactiveSwift -import WebKit - -internal protocol UpdatePreviewViewModelInputs { - /// Call with the update draft. - func configureWith(draft: UpdateDraft) - - /// Call when the webview needs to decide a policy for a navigation action. Returns the decision policy. - func decidePolicyFor(navigationAction: WKNavigationActionData) - -> WKNavigationActionPolicy - - /// Call when the publish button is tapped. - func publishButtonTapped() - - /// Call when the publish confirmation is tapped. - func publishConfirmationButtonTapped() - - /// Call when the view loads. - func viewDidLoad() -} - -internal protocol UpdatePreviewViewModelOutputs { - /// Emits when publishing succeeds. - var goToUpdate: Signal<(Project, Update), Never> { get } - - /// Emits when the view should show a publish confirmation alert with detail message. - var showPublishConfirmation: Signal { get } - - /// Emits when publishing fails. - var showPublishFailure: Signal<(), Never> { get } - - /// Emits a request that should be loaded into the webview. - var webViewLoadRequest: Signal { get } -} - -internal protocol UpdatePreviewViewModelType { - var inputs: UpdatePreviewViewModelInputs { get } - var outputs: UpdatePreviewViewModelOutputs { get } -} - -internal final class UpdatePreviewViewModel: UpdatePreviewViewModelInputs, - UpdatePreviewViewModelOutputs, UpdatePreviewViewModelType { - internal init() { - let draft = self.draftProperty.signal.skipNil() - - let initialRequest = draft - .takeWhen(self.viewDidLoadProperty.signal) - .map { AppEnvironment.current.apiService.previewUrl(forDraft: $0) } - .skipNil() - .map { AppEnvironment.current.apiService.preparedRequest(forURL: $0) } - - let redirectRequest = self.policyForNavigationActionProperty.signal.skipNil() - .map { $0.request } - .filter { - !AppEnvironment.current.apiService.isPrepared(request: $0) - && Navigation.Project.updateWithRequest($0) != nil - } - .map { AppEnvironment.current.apiService.preparedRequest(forRequest: $0) } - - self.webViewLoadRequest = Signal.merge(initialRequest, redirectRequest) - - self.policyDecisionProperty <~ self.policyForNavigationActionProperty.signal.skipNil() - .map { action in - action.navigationType == .other || action.targetFrame?.mainFrame == .some(false) - ? .allow - : .cancel - } - - let projectEvent = draft - .switchMap { - AppEnvironment.current.apiService.fetchProject(param: .id($0.update.projectId)) - .materialize() - } - let project = projectEvent - .values() - - self.showPublishConfirmation = project - .map { - // swiftformat:disable wrap - Strings.dashboard_post_update_preview_confirmation_alert_this_will_notify_backers_that_a_new_update_is_available(backer_count: $0.stats.backersCount) - // swiftformat:enable wrap - } - .takeWhen(self.publishButtonTappedProperty.signal) - - let publishEvent = draft - .takeWhen(self.publishConfirmationButtonTappedProperty.signal) - .switchMap { - AppEnvironment.current.apiService.publish(draft: $0) - .ksr_delay(AppEnvironment.current.apiDelayInterval, on: AppEnvironment.current.scheduler) - .materialize() - } - let update = publishEvent - .values() - - self.goToUpdate = Signal.combineLatest(project, update) - self.showPublishFailure = publishEvent - .errors() - .ignoreValues() - } - - fileprivate let policyForNavigationActionProperty = MutableProperty(nil) - fileprivate let policyDecisionProperty = MutableProperty(WKNavigationActionPolicy.allow) - internal func decidePolicyFor(navigationAction: WKNavigationActionData) - -> WKNavigationActionPolicy { - self.policyForNavigationActionProperty.value = navigationAction - return self.policyDecisionProperty.value - } - - fileprivate let publishButtonTappedProperty = MutableProperty(()) - internal func publishButtonTapped() { - self.publishButtonTappedProperty.value = () - } - - fileprivate let publishConfirmationButtonTappedProperty = MutableProperty(()) - internal func publishConfirmationButtonTapped() { - self.publishConfirmationButtonTappedProperty.value = () - } - - fileprivate let draftProperty = MutableProperty(nil) - internal func configureWith(draft: UpdateDraft) { - self.draftProperty.value = draft - } - - fileprivate let viewDidLoadProperty = MutableProperty(()) - internal func viewDidLoad() { - self.viewDidLoadProperty.value = () - } - - let goToUpdate: Signal<(Project, Update), Never> - let showPublishConfirmation: Signal - let showPublishFailure: Signal<(), Never> - let webViewLoadRequest: Signal - - internal var inputs: UpdatePreviewViewModelInputs { return self } - internal var outputs: UpdatePreviewViewModelOutputs { return self } -} diff --git a/Kickstarter-iOS/Features/Dashboard/ViewModel/UpdatePreviewViewModelTests.swift b/Kickstarter-iOS/Features/Dashboard/ViewModel/UpdatePreviewViewModelTests.swift deleted file mode 100644 index da8267f275..0000000000 --- a/Kickstarter-iOS/Features/Dashboard/ViewModel/UpdatePreviewViewModelTests.swift +++ /dev/null @@ -1,156 +0,0 @@ -@testable import Kickstarter_Framework -@testable import KsApi -@testable import Library -import Prelude -import ReactiveExtensions_TestHelpers -import ReactiveSwift -import WebKit -import XCTest - -final class UpdatePreviewViewModelTests: TestCase { - fileprivate let vm: UpdatePreviewViewModelType = UpdatePreviewViewModel() - - fileprivate let showPublishConfirmation = TestObserver() - fileprivate let showPublishFailure = TestObserver<(), Never>() - fileprivate let goToUpdate = TestObserver() - fileprivate let goToUpdateProject = TestObserver() - fileprivate let webViewLoadRequest = TestObserver() - - override func setUp() { - super.setUp() - - self.vm.outputs.showPublishConfirmation.observe(self.showPublishConfirmation.observer) - self.vm.outputs.showPublishFailure.observe(self.showPublishFailure.observer) - self.vm.outputs.goToUpdate.map(second).observe(self.goToUpdate.observer) - self.vm.outputs.goToUpdate.map(first).observe(self.goToUpdateProject.observer) - self.vm.outputs.webViewLoadRequest.map { $0.url?.absoluteString } - .observe(self.webViewLoadRequest.observer) - } - - func testWebViewLoaded() { - let draft = .template - |> UpdateDraft.lens.update.id .~ 1 - |> UpdateDraft.lens.update.projectId .~ 2 - self.vm.inputs.configureWith(draft: draft) - self.vm.inputs.viewDidLoad() - - let previewUrl = "https://\(Secrets.Api.Endpoint.production)/projects/2/updates/1/preview" - let query = "client_id=\(self.apiService.serverConfig.apiClientAuth.clientId)¤cy=USD" - self.webViewLoadRequest.assertValues( - ["\(previewUrl)?\(query)"] - ) - - let redirectUrl = "https://www.kickstarter.com/projects/smashmouth/somebody-once-told-me/posts/1" - let request = URLRequest(url: URL(string: redirectUrl)!) - let navigationAction = WKNavigationActionData( - navigationType: .other, - request: request, - sourceFrame: WKFrameInfoData(mainFrame: true, request: request), - targetFrame: WKFrameInfoData(mainFrame: true, request: request) - ) - - let policy = self.vm.inputs.decidePolicyFor(navigationAction: navigationAction) - - XCTAssertEqual(WKNavigationActionPolicy.allow.rawValue, policy.rawValue) - self.webViewLoadRequest.assertValues( - [ - "\(previewUrl)?\(query)", - "\(redirectUrl)?\(query)" - ] - ) - } - - func testPublishSuccess() { - let project = .template - |> Project.lens.id .~ 2 - |> Project.lens.stats.backersCount .~ 1_024 - let draft = .template - |> UpdateDraft.lens.update.id .~ 1 - |> UpdateDraft.lens.update.projectId .~ project.id - - let api = MockService(fetchProjectResult: .success(project), fetchUpdateResponse: draft.update) - withEnvironment(apiService: api) { - self.vm.inputs.configureWith(draft: draft) - self.vm.inputs.viewDidLoad() - - self.showPublishConfirmation.assertValueCount(0) - self.vm.inputs.publishButtonTapped() - let confirmation = - "This will notify 1,024 backers that a new update is available. Are you sure you want to post?" - self.showPublishConfirmation.assertValues([confirmation]) - - self.goToUpdate.assertValues([]) - self.goToUpdateProject.assertValues([]) - - self.vm.inputs.publishConfirmationButtonTapped() - - self.goToUpdate.assertValues([]) - self.goToUpdateProject.assertValues([]) - - self.scheduler.advance() - - self.goToUpdate.assertValues([draft.update]) - self.goToUpdateProject.assertValues([project]) - self.showPublishFailure.assertValueCount(0) - - XCTAssertEqual([], self.segmentTrackingClient.events) - } - } - - func testPublishCanceled() { - let project = .template - |> Project.lens.id .~ 2 - |> Project.lens.stats.backersCount .~ 1_024 - let draft = .template - |> UpdateDraft.lens.update.id .~ 1 - |> UpdateDraft.lens.update.projectId .~ project.id - - let api = MockService(fetchProjectResult: .success(project), fetchUpdateResponse: draft.update) - withEnvironment(apiService: api) { - self.vm.inputs.configureWith(draft: draft) - self.vm.inputs.viewDidLoad() - - self.showPublishConfirmation.assertValueCount(0) - self.vm.inputs.publishButtonTapped() - let confirmation = - "This will notify 1,024 backers that a new update is available. Are you sure you want to post?" - self.showPublishConfirmation.assertValues([confirmation]) - - self.goToUpdate.assertValues([]) - self.goToUpdateProject.assertValues([]) - - self.scheduler.advance() - - self.goToUpdate.assertValues([]) - self.goToUpdateProject.assertValues([]) - - XCTAssertEqual([], self.segmentTrackingClient.events) - } - } - - func testPublishFailure() { - let project = .template - |> Project.lens.id .~ 2 - |> Project.lens.stats.backersCount .~ 1_024 - let draft = .template - |> UpdateDraft.lens.update.id .~ 1 - |> UpdateDraft.lens.update.projectId .~ project.id - - let api = MockService(publishUpdateError: .couldNotParseJSON, fetchProjectResult: .success(project)) - withEnvironment(apiService: api) { - self.vm.inputs.configureWith(draft: draft) - self.vm.inputs.viewDidLoad() - self.vm.inputs.publishButtonTapped() - - self.showPublishFailure.assertValueCount(0) - self.vm.inputs.publishConfirmationButtonTapped() - - self.scheduler.advance() - - self.goToUpdate.assertValues([]) - self.showPublishFailure.assertValueCount(1) - - XCTAssertEqual([], self.segmentTrackingClient.events) - } - } -} diff --git a/Kickstarter-iOS/Features/Dashboard/Views/Cells/DashboardActionCell.swift b/Kickstarter-iOS/Features/Dashboard/Views/Cells/DashboardActionCell.swift deleted file mode 100644 index 40ca57efb3..0000000000 --- a/Kickstarter-iOS/Features/Dashboard/Views/Cells/DashboardActionCell.swift +++ /dev/null @@ -1,104 +0,0 @@ -import KsApi -import Library -import Prelude -import Prelude_UIKit -import UIKit - -internal protocol DashboardActionCellDelegate: AnyObject { - /// Call with project value when navigating to activity screen. - func goToActivity(_ cell: DashboardActionCell?, project: Project) - - /// Call with project value when navigating to messages screen. - func goToMessages(_ cell: DashboardActionCell?) - - /// Call with project value when navigating to post update screen. - func goToPostUpdate(_ cell: DashboardActionCell?, project: Project) -} - -internal final class DashboardActionCell: UITableViewCell, ValueCell { - internal weak var delegate: DashboardActionCellDelegate? - fileprivate let viewModel: DashboardActionCellViewModelType = DashboardActionCellViewModel() - - @IBOutlet fileprivate var activityButton: UIButton! - @IBOutlet fileprivate var activityRowStackView: UIStackView! - @IBOutlet fileprivate var lastUpdatePublishedAtLabel: UILabel! - @IBOutlet fileprivate var messagesButton: UIButton! - @IBOutlet fileprivate var messagesRowStackView: UIStackView! - @IBOutlet fileprivate var postUpdateButton: UIButton! - @IBOutlet fileprivate var separatorView: UIView! - @IBOutlet fileprivate var unseenActivitiesCountView: CountBadgeView! - @IBOutlet fileprivate var unreadMessagesCountView: CountBadgeView! - - internal override func awakeFromNib() { - super.awakeFromNib() - - self.activityButton.addTarget(self, action: #selector(self.activityTapped), for: .touchUpInside) - - self.messagesButton.addTarget(self, action: #selector(self.messagesTapped), for: .touchUpInside) - - self.postUpdateButton.addTarget( - self, - action: #selector(self.postUpdateTapped), - for: .touchUpInside - ) - } - - internal override func bindStyles() { - _ = self |> baseTableViewCellStyle() - self.isAccessibilityElement = false - self.accessibilityElements = [self.activityButton, self.messagesButton, self.postUpdateButton] - .compact() - _ = self.activityButton |> dashboardActivityButtonStyle - _ = self.lastUpdatePublishedAtLabel |> dashboardLastUpdatePublishedAtLabelStyle - _ = self.messagesButton |> dashboardMessagesButtonStyle - _ = self.postUpdateButton |> postUpdateButtonStyle - _ = self.separatorView |> separatorStyle - } - - internal override func bindViewModel() { - self.activityButton.rac.accessibilityLabel = self.viewModel.outputs.activityButtonAccessibilityLabel - self.activityRowStackView.rac.hidden = self.viewModel.outputs.activityRowHidden - self.lastUpdatePublishedAtLabel.rac.text = self.viewModel.outputs.lastUpdatePublishedAt - self.messagesButton.rac.accessibilityLabel = self.viewModel.outputs.messagesButtonAccessibilityLabel - self.messagesRowStackView.rac.hidden = self.viewModel.outputs.messagesRowHidden - self.unreadMessagesCountView.label.rac.text = self.viewModel.outputs.unreadMessagesCount - self.unreadMessagesCountView.rac.hidden = self.viewModel.outputs.unreadMessagesCountHidden - self.unseenActivitiesCountView.label.rac.text = self.viewModel.outputs.unseenActivitiesCount - self.unseenActivitiesCountView.rac.hidden = self.viewModel.outputs.unseenActivitiesCountHidden - self.lastUpdatePublishedAtLabel.rac.hidden = self.viewModel.outputs.lastUpdatePublishedLabelHidden - self.postUpdateButton.rac.accessibilityValue = self.viewModel.outputs.postUpdateButtonAccessibilityValue - self.postUpdateButton.rac.hidden = self.viewModel.outputs.postUpdateButtonHidden - - self.viewModel.outputs.goToActivity - .observeForUI() - .observeValues { [weak self] project in - self?.delegate?.goToActivity(self, project: project) - } - - self.viewModel.outputs.goToMessages - .observeForUI() - .observeValues { [weak self] in self?.delegate?.goToMessages(self) } - - self.viewModel.outputs.goToPostUpdate - .observeForUI() - .observeValues { [weak self] project in - self?.delegate?.goToPostUpdate(self, project: project) - } - } - - internal func configureWith(value: Project) { - self.viewModel.inputs.configureWith(project: value) - } - - @objc fileprivate func activityTapped() { - self.viewModel.inputs.activityTapped() - } - - @objc fileprivate func messagesTapped() { - self.viewModel.inputs.messagesTapped() - } - - @objc fileprivate func postUpdateTapped() { - self.viewModel.inputs.postUpdateTapped() - } -} diff --git a/Kickstarter-iOS/Features/Dashboard/Views/Cells/DashboardContextCell.swift b/Kickstarter-iOS/Features/Dashboard/Views/Cells/DashboardContextCell.swift deleted file mode 100644 index 6c39e2dba0..0000000000 --- a/Kickstarter-iOS/Features/Dashboard/Views/Cells/DashboardContextCell.swift +++ /dev/null @@ -1,42 +0,0 @@ -import KsApi -import Library -import Prelude -import Prelude_UIKit -import UIKit - -internal final class DashboardContextCell: UITableViewCell, ValueCell { - @IBOutlet fileprivate var containerView: UIView! - @IBOutlet fileprivate var projectNameLabel: UILabel! - @IBOutlet fileprivate var separatorView: UIView! - @IBOutlet fileprivate var viewProjectButton: UIButton! - - internal override func bindStyles() { - super.bindStyles() - - _ = self - |> dashboardContextCellStyle - |> UITableViewCell.lens.selectionStyle .~ .gray - |> UITableViewCell.lens.accessibilityTraits .~ UIAccessibilityTraits.button - |> UITableViewCell.lens.accessibilityHint %~ { _ in - Strings.dashboard_tout_accessibility_hint_opens_project() - } - - _ = self.containerView - |> containerViewBackgroundStyle - - _ = self.projectNameLabel - |> dashboardStatTitleLabelStyle - - _ = self.separatorView - |> separatorStyle - - _ = self.viewProjectButton - |> dashboardViewProjectButtonStyle - |> UIButton.lens.isUserInteractionEnabled .~ false - |> UIButton.lens.accessibilityElementsHidden .~ true - } - - internal func configureWith(value: Project) { - self.projectNameLabel.text = value.name - } -} diff --git a/Kickstarter-iOS/Features/Dashboard/Views/Cells/DashboardFundingCell.swift b/Kickstarter-iOS/Features/Dashboard/Views/Cells/DashboardFundingCell.swift deleted file mode 100644 index b160d43847..0000000000 --- a/Kickstarter-iOS/Features/Dashboard/Views/Cells/DashboardFundingCell.swift +++ /dev/null @@ -1,103 +0,0 @@ -import KsApi -import Library -import Prelude -import Prelude_UIKit -import UIKit - -internal final class DashboardFundingCell: UITableViewCell, ValueCell { - fileprivate let viewModel: DashboardFundingCellViewModelType = DashboardFundingCellViewModel() - - @IBOutlet fileprivate var backersTitleLabel: UILabel! - @IBOutlet fileprivate var backersSubtitleLabel: UILabel! - @IBOutlet fileprivate var deadlineDateLabel: UILabel! - @IBOutlet fileprivate var fundingProgressTitleLabel: UILabel! - @IBOutlet fileprivate var graphAxisSeparatorView: UIView! - @IBOutlet fileprivate var graphBackgroundView: UIView! - @IBOutlet fileprivate var graphView: FundingGraphView! - @IBOutlet fileprivate var graphViewHeightConstraint: NSLayoutConstraint! - @IBOutlet fileprivate var graphXAxisStackView: UIStackView! - @IBOutlet fileprivate var graphYAxisBottomLabel: UILabel! - @IBOutlet fileprivate var graphYAxisMiddleLabel: UILabel! - @IBOutlet fileprivate var graphYAxisTopLabel: UILabel! - @IBOutlet fileprivate var launchDateLabel: UILabel! - @IBOutlet fileprivate var pledgedSubtitleLabel: UILabel! - @IBOutlet fileprivate var pledgedTitleLabel: UILabel! - @IBOutlet fileprivate var rootStackView: UIStackView! - @IBOutlet fileprivate var separatorViews: [UIView]! - @IBOutlet fileprivate var statsStackView: UIStackView! - @IBOutlet fileprivate var timeRemainingSubtitleLabel: UILabel! - @IBOutlet fileprivate var timeRemainingTitleLabel: UILabel! - - internal override func bindStyles() { - super.bindStyles() - - self.accessibilityLabel = Strings.dashboard_graphs_funding_title_funding_progress() - - _ = self - |> baseTableViewCellStyle() - - _ = self.backersSubtitleLabel - |> dashboardStatSubtitleLabelStyle - |> UILabel.lens.text %~ { _ in Strings.dashboard_tout_backers() } - _ = self.backersTitleLabel |> dashboardStatTitleLabelStyle - _ = self.deadlineDateLabel |> dashboardFundingGraphXAxisLabelStyle - _ = self.fundingProgressTitleLabel |> dashboardFundingProgressTitleLabelStyle - _ = self.graphAxisSeparatorView |> dashboardFundingGraphAxisSeparatorViewStyle - _ = self.graphBackgroundView - |> containerViewBackgroundStyle - |> UIView.lens.accessibilityElementsHidden .~ true - _ = self.graphView |> UIView.lens.layoutMargins .~ .init(top: 0.0, left: 16.0, bottom: 0.0, right: 0.0) - _ = self.graphXAxisStackView |> dashboardFundingGraphXAxisStackViewStyle - _ = self.graphYAxisBottomLabel |> dashboardFundingGraphYAxisLabelStyle - _ = self.graphYAxisMiddleLabel |> dashboardFundingGraphYAxisLabelStyle - _ = self.graphYAxisTopLabel |> dashboardFundingGraphYAxisLabelStyle - _ = self.launchDateLabel |> dashboardFundingGraphXAxisLabelStyle - _ = self.pledgedSubtitleLabel |> dashboardStatSubtitleLabelStyle - _ = self.pledgedTitleLabel - |> dashboardStatTitleLabelStyle - |> UILabel.lens.textColor .~ .ksr_create_700 - - _ = self.rootStackView - |> UIStackView.lens.isLayoutMarginsRelativeArrangement .~ true - |> UIStackView.lens.layoutMargins %~~ { _, stack in - stack.traitCollection.isRegularRegular - ? .init(topBottom: Styles.grid(4), leftRight: Styles.grid(12)) - : .init(all: 0.0) - } - - _ = self.separatorViews ||> separatorStyle - _ = self.statsStackView |> dashboardFundingStatsStackView - _ = self.timeRemainingSubtitleLabel |> dashboardStatSubtitleLabelStyle - _ = self.timeRemainingTitleLabel |> dashboardStatTitleLabelStyle - - self.graphViewHeightConstraint.constant = self.traitCollection.isRegularRegular - ? Styles.grid(40) : Styles.grid(30) - } - - internal override func bindViewModel() { - super.bindViewModel() - - self.backersTitleLabel.rac.text = self.viewModel.outputs.backersText - self.deadlineDateLabel.rac.text = self.viewModel.outputs.deadlineDateText - self.graphYAxisBottomLabel.rac.text = self.viewModel.outputs.graphYAxisBottomLabelText - self.graphYAxisMiddleLabel.rac.text = self.viewModel.outputs.graphYAxisMiddleLabelText - self.graphYAxisTopLabel.rac.text = self.viewModel.outputs.graphYAxisTopLabelText - self.launchDateLabel.rac.text = self.viewModel.outputs.launchDateText - self.pledgedTitleLabel.rac.text = self.viewModel.outputs.pledgedText - self.pledgedSubtitleLabel.rac.text = self.viewModel.outputs.goalText - self.timeRemainingSubtitleLabel.rac.text = self.viewModel.outputs.timeRemainingSubtitleText - self.timeRemainingTitleLabel.rac.text = self.viewModel.outputs.timeRemainingTitleText - - self.viewModel.outputs.graphData - .observeForUI() - .observeValues { [weak self] data in - self?.graphView.project = data.project - self?.graphView.stats = data.stats - self?.graphView.yAxisTickSize = data.yAxisTickSize - } - } - - internal func configureWith(value: ([ProjectStatsEnvelope.FundingDateStats], Project)) { - self.viewModel.inputs.configureWith(fundingDateStats: value.0, project: value.1) - } -} diff --git a/Kickstarter-iOS/Features/Dashboard/Views/Cells/DashboardReferrersCell.swift b/Kickstarter-iOS/Features/Dashboard/Views/Cells/DashboardReferrersCell.swift deleted file mode 100644 index 711b682cda..0000000000 --- a/Kickstarter-iOS/Features/Dashboard/Views/Cells/DashboardReferrersCell.swift +++ /dev/null @@ -1,231 +0,0 @@ -import KsApi -import Library -import Prelude -import Prelude_UIKit -import UIKit - -internal protocol DashboardReferrersCellDelegate: AnyObject { - /// Call when referrer stack view rows are added to expand the cell size. - func dashboardReferrersCellDidAddReferrerRows(_ cell: DashboardReferrersCell?) -} - -internal final class DashboardReferrersCell: UITableViewCell, ValueCell { - internal weak var delegate: DashboardReferrersCellDelegate? - fileprivate let viewModel: DashboardReferrersCellViewModelType = DashboardReferrersCellViewModel() - - @IBOutlet fileprivate var averagePledgeAmountSubtitleLabel: UILabel! - @IBOutlet fileprivate var averagePledgeAmountTitleLabel: UILabel! - @IBOutlet fileprivate var averageStackView: UIStackView! - @IBOutlet fileprivate var backersColumnTitleButton: UIButton! - @IBOutlet fileprivate var cumulativeStackView: UIStackView! - @IBOutlet fileprivate var customPercentLabel: UILabel! - @IBOutlet fileprivate var customPercentIndicatorLabel: UILabel! - @IBOutlet fileprivate var customPledgedAmountSubtitleLabel: UILabel! - @IBOutlet fileprivate var customPledgedAmountTitleLabel: UILabel! - @IBOutlet fileprivate var externalPercentLabel: UILabel! - @IBOutlet fileprivate var externalPercentIndicatorLabel: UILabel! - @IBOutlet fileprivate var externalPledgedAmountSubtitleLabel: UILabel! - @IBOutlet fileprivate var externalPledgedAmountTitleLabel: UILabel! - @IBOutlet fileprivate var internalPercentLabel: UILabel! - @IBOutlet fileprivate var internalPercentIndicatorLabel: UILabel! - @IBOutlet fileprivate var internalPledgedAmountSubtitleLabel: UILabel! - @IBOutlet fileprivate var internalPledgedAmountTitleLabel: UILabel! - @IBOutlet fileprivate var pledgedColumnTitleButton: UIButton! - @IBOutlet fileprivate var referralChartView: ReferralChartView! - @IBOutlet fileprivate var referrersTitleLabel: UILabel! - @IBOutlet fileprivate var referrersStackView: UIStackView! - @IBOutlet fileprivate var showMoreReferrersButton: UIButton! - @IBOutlet fileprivate var sourceColumnTitleButton: UIButton! - @IBOutlet fileprivate var chartCardView: UIView! - @IBOutlet fileprivate var separatorViews: [UIView]! - - internal override func awakeFromNib() { - super.awakeFromNib() - - self.backersColumnTitleButton.addTarget( - self, - action: #selector(self.backersButtonTapped), - for: .touchUpInside - ) - - self.pledgedColumnTitleButton.addTarget( - self, - action: #selector(self.pledgedButtonTapped), - for: .touchUpInside - ) - - self.showMoreReferrersButton.addTarget( - self, - action: #selector(self.showMoreReferrersTapped), - for: .touchUpInside - ) - - self.sourceColumnTitleButton.addTarget( - self, - action: #selector(self.sourceButtonTapped), - for: .touchUpInside - ) - - self.viewModel.inputs.awakeFromNib() - } - - internal override func bindStyles() { - _ = self |> baseTableViewCellStyle() - - _ = self.averagePledgeAmountSubtitleLabel - |> dashboardStatSubtitleLabelStyle - |> UILabel.lens.text %~ { _ in Strings.dashboard_graphs_referrers_average_pledge_amount() } - - _ = self.averagePledgeAmountTitleLabel - |> dashboardStatTitleLabelStyle - - _ = self.backersColumnTitleButton - |> dashboardColumnTitleButtonStyle - |> UIButton.lens.title(for: .normal) %~ { _ in Strings.dashboard_graphs_referrers_backers() } - - _ = self.customPercentLabel - |> dashboardReferrersPledgePercentLabelStyle - - _ = self.customPercentIndicatorLabel - |> UILabel.lens.textColor .~ .ksr_trust_500 - - _ = self.customPledgedAmountTitleLabel - |> dashboardStatTitleLabelStyle - - _ = self.customPledgedAmountSubtitleLabel - |> dashboardStatSubtitleLabelStyle - |> UILabel.lens.text %~ { _ in Strings.dashboard_graphs_referrers_pledged_via_custom() } - - _ = self.externalPercentLabel - |> dashboardReferrersPledgePercentLabelStyle - - _ = self.externalPercentIndicatorLabel - |> UILabel.lens.textColor .~ .ksr_celebrate_500 - - _ = self.externalPledgedAmountSubtitleLabel - |> dashboardStatSubtitleLabelStyle - |> UILabel.lens.text %~ { _ in Strings.dashboard_graphs_referrers_pledged_via_external() } - - _ = self.externalPledgedAmountTitleLabel - |> dashboardStatTitleLabelStyle - - _ = self.internalPercentLabel - |> dashboardReferrersPledgePercentLabelStyle - - _ = self.internalPercentIndicatorLabel - |> UILabel.lens.textColor .~ .ksr_create_700 - - _ = self.internalPledgedAmountSubtitleLabel - |> dashboardStatSubtitleLabelStyle - |> UILabel.lens.text %~ { _ in Strings.dashboard_graphs_referrers_pledged_via_kickstarter() } - - _ = self.internalPledgedAmountTitleLabel - |> dashboardStatTitleLabelStyle - - _ = self.pledgedColumnTitleButton - |> dashboardColumnTitleButtonStyle - |> UIButton.lens.title(for: .normal) %~ { _ in Strings.dashboard_graphs_referrers_pledged() } - - _ = self.referrersTitleLabel - |> dashboardReferrersTitleLabelStyle - - _ = self.referralChartView - |> UIView.lens.backgroundColor .~ .clear - - _ = self.showMoreReferrersButton - |> dashboardReferrersShowMoreButtonStyle - - _ = self.sourceColumnTitleButton - |> dashboardColumnTitleButtonStyle - |> UIButton.lens.title(for: .normal) %~ { _ in Strings.dashboard_graphs_referrers_source() } - - _ = self.chartCardView - |> dashboardChartCardViewStyle - - _ = self.cumulativeStackView - |> dashboardReferrersCumulativeStackViewStyle - - _ = self.averageStackView - |> dashboardReferrersCumulativeStackViewStyle - - self.separatorViews.forEach { _ = $0 |> separatorStyle } - } - - internal override func bindViewModel() { - self.averagePledgeAmountTitleLabel.rac.text = self.viewModel.outputs.averagePledgeText - self.customPercentLabel.rac.text = self.viewModel.outputs.customPercentText - self.customPledgedAmountTitleLabel.rac.text = self.viewModel.outputs.customPledgedText - self.externalPercentLabel.rac.text = self.viewModel.outputs.externalPercentText - self.externalPledgedAmountTitleLabel.rac.text = self.viewModel.outputs.externalPledgedText - self.internalPercentLabel.rac.text = self.viewModel.outputs.internalPercentText - self.internalPledgedAmountTitleLabel.rac.text = self.viewModel.outputs.internalPledgedText - - self.viewModel.outputs.notifyDelegateAddedReferrerRows - .observeForUI() - .observeValues { [weak self] _ in - self?.delegate?.dashboardReferrersCellDidAddReferrerRows(self) - } - - self.viewModel.outputs.referrersRowData - .observeForUI() - .observeValues { [weak self] data in - self?.addReferrerRows(withData: data) - } - - self.showMoreReferrersButton.rac.hidden = self.viewModel.outputs.showMoreReferrersButtonHidden - - self.viewModel.outputs.externalPercentage - .observeForUI() - .observeValues { [weak self] in self?.referralChartView.externalPercentage = CGFloat($0) } - - self.viewModel.outputs.internalPercentage - .observeForUI() - .observeValues { [weak self] in self?.referralChartView.internalPercentage = CGFloat($0) } - } - - internal func addReferrerRows(withData data: ReferrersRowData) { - self.referrersStackView.subviews.forEach { $0.removeFromSuperview() } - - let referrers = data.referrers - .map { DashboardReferrerRowStackView(frame: self.frame, country: data.country, referrer: $0) } - - let refsCount = referrers.count - (0.. UIView.lens.backgroundColor .~ .ksr_support_300 - - divider.heightAnchor.constraint(equalToConstant: 1.0).isActive = true - - self.referrersStackView.addArrangedSubview(divider) - } - } - } - - internal func configureWith(value: ( - ProjectStatsEnvelope.CumulativeStats, Project, - ProjectStatsEnvelope.ReferralAggregateStats, [ProjectStatsEnvelope.ReferrerStats] - )) { - self.viewModel.inputs.configureWith( - cumulative: value.0, project: value.1, - referralAggregates: value.2, referrers: value.3 - ) - } - - @objc fileprivate func backersButtonTapped() { - self.viewModel.inputs.backersButtonTapped() - } - - @objc fileprivate func pledgedButtonTapped() { - self.viewModel.inputs.pledgedButtonTapped() - } - - @objc fileprivate func showMoreReferrersTapped() { - self.viewModel.inputs.showMoreReferrersTapped() - } - - @objc fileprivate func sourceButtonTapped() { - self.viewModel.inputs.sourceButtonTapped() - } -} diff --git a/Kickstarter-iOS/Features/Dashboard/Views/Cells/DashboardRewardsCell.swift b/Kickstarter-iOS/Features/Dashboard/Views/Cells/DashboardRewardsCell.swift deleted file mode 100644 index 0f55b9a6bf..0000000000 --- a/Kickstarter-iOS/Features/Dashboard/Views/Cells/DashboardRewardsCell.swift +++ /dev/null @@ -1,150 +0,0 @@ -import KsApi -import Library -import Prelude -import Prelude_UIKit -import UIKit - -internal protocol DashboardRewardsCellDelegate: AnyObject { - /// Call when stack view rows are added to expand the cell size. - func dashboardRewardsCellDidAddRewardRows(_ cell: DashboardRewardsCell?) -} - -internal final class DashboardRewardsCell: UITableViewCell, ValueCell { - fileprivate let viewModel: DashboardRewardsCellViewModelType = DashboardRewardsCellViewModel() - - @IBOutlet fileprivate var containerView: UIView! - @IBOutlet fileprivate var mainStackView: UIStackView! - @IBOutlet fileprivate var rewardsTitle: UILabel! - @IBOutlet fileprivate var topRewardsButton: UIButton! - @IBOutlet fileprivate var backersButton: UIButton! - @IBOutlet fileprivate var pledgedButton: UIButton! - @IBOutlet fileprivate var seeAllTiersButton: UIButton! - - internal weak var delegate: DashboardRewardsCellDelegate? - - internal override func awakeFromNib() { - super.awakeFromNib() - - self.topRewardsButton.addTarget( - self, - action: #selector(self.topRewardsButtonTapped), - for: .touchUpInside - ) - - self.backersButton.addTarget( - self, - action: #selector(self.backersButtonTapped), - for: .touchUpInside - ) - - self.pledgedButton.addTarget( - self, - action: #selector(self.pledgedButtonTapped), - for: .touchUpInside - ) - - self.seeAllTiersButton.addTarget( - self, - action: #selector(self.seeAllTiersButtonTapped), - for: .touchUpInside - ) - } - - internal override func bindStyles() { - super.bindStyles() - - _ = self - |> baseTableViewCellStyle() - - _ = self.containerView - |> UIView.lens.backgroundColor .~ .ksr_white - |> dashboardCardStyle - - _ = self.rewardsTitle - |> dashboardRewardTitleLabelStyle - - _ = self.topRewardsButton - |> dashboardColumnTitleButtonStyle - |> UIButton.lens.title(for: .normal) %~ { _ in Strings.dashboard_graphs_rewards_top_rewards() } - - _ = self.backersButton - |> dashboardColumnTitleButtonStyle - |> UIButton.lens.title(for: .normal) %~ { _ in Strings.dashboard_graphs_rewards_backers() } - - _ = self.pledgedButton - |> dashboardColumnTitleButtonStyle - |> UIButton.lens.title(for: .normal) %~ { _ in Strings.dashboard_graphs_rewards_pledged() } - - _ = self.seeAllTiersButton - |> greenButtonStyle - |> UIButton.lens.title(for: .normal) %~ { _ in - Strings.dashboard_graphs_rewards_view_more_reward_stats() - } - } - - internal override func bindViewModel() { - self.seeAllTiersButton.rac.hidden = self.viewModel.outputs.hideSeeAllTiersButton - - self.viewModel.outputs.notifyDelegateAddedRewardRows - .observeForUI() - .observeValues { [weak self] _ in - self?.delegate?.dashboardRewardsCellDidAddRewardRows(self) - } - - self.viewModel.outputs.rewardsRowData - .observeForUI() - .observeValues { [weak self] data in - self?.addRewardRows(withData: data) - } - } - - internal func addRewardRows(withData data: RewardsRowData) { - self.mainStackView.subviews.forEach { $0.removeFromSuperview() } - - let stats = data.rewardsStats - .map { - DashboardRewardRowStackView( - frame: self.frame, - country: data.country, - reward: $0, - totalPledged: data.totalPledged - ) - } - - let statsCount = stats.count - (0.. UIView.lens.backgroundColor .~ .ksr_support_300 - - divider.heightAnchor.constraint(equalToConstant: 1.0).isActive = true - - self.mainStackView.addArrangedSubview(divider) - } - } - } - - internal func configureWith(value: ( - rewardStats: [ProjectStatsEnvelope.RewardStats], - project: Project - )) { - self.viewModel.inputs.configureWith(rewardStats: value.0, project: value.1) - } - - @objc fileprivate func backersButtonTapped() { - self.viewModel.inputs.backersButtonTapped() - } - - @objc fileprivate func pledgedButtonTapped() { - self.viewModel.inputs.pledgedButtonTapped() - } - - @objc fileprivate func topRewardsButtonTapped() { - self.viewModel.inputs.topRewardsButtonTapped() - } - - @objc fileprivate func seeAllTiersButtonTapped() { - self.viewModel.inputs.seeAllTiersButtonTapped() - } -} diff --git a/Kickstarter-iOS/Features/Dashboard/Views/Cells/DashboardVideoCell.swift b/Kickstarter-iOS/Features/Dashboard/Views/Cells/DashboardVideoCell.swift deleted file mode 100644 index 711b26daf8..0000000000 --- a/Kickstarter-iOS/Features/Dashboard/Views/Cells/DashboardVideoCell.swift +++ /dev/null @@ -1,117 +0,0 @@ -import KsApi -import Library -import Prelude -import Prelude_UIKit -import UIKit - -internal final class DashboardVideoCell: UITableViewCell, ValueCell { - fileprivate let viewModel: DashboardVideoCellViewModelType = DashboardVideoCellViewModel() - - @IBOutlet fileprivate var completionPercentageLabel: UILabel! - @IBOutlet fileprivate var externalLabel: UILabel! - @IBOutlet fileprivate var externalPlaysCountLabel: UILabel! - @IBOutlet fileprivate var externalPlaysProgressView: UIView! - @IBOutlet fileprivate var graphBackgroundView: UIView! - @IBOutlet fileprivate var internalLabel: UILabel! - @IBOutlet fileprivate var internalPlaysCountLabel: UILabel! - @IBOutlet fileprivate var internalPlaysProgressView: UIView! - @IBOutlet fileprivate var separatorViews: [UIView]! - @IBOutlet fileprivate var statsContainerView: UIView! - @IBOutlet fileprivate var totalPlaysContainerView: UIView! - @IBOutlet fileprivate var totalPlaysCountLabel: UILabel! - @IBOutlet fileprivate var totalPlaysStackView: UIStackView! - @IBOutlet fileprivate var videoPlaysTitleLabel: UILabel! - - @IBOutlet fileprivate var graphStatsContainerView: UIView! - @IBOutlet fileprivate var graphStatsStackView: UIStackView! - - internal override func bindStyles() { - super.bindStyles() - - _ = self - |> baseTableViewCellStyle() - - _ = self.completionPercentageLabel - |> dashboardStatSubtitleLabelStyle - |> UILabel.lens.numberOfLines .~ 2 - - _ = self.externalLabel - |> dashboardStatSubtitleLabelStyle - |> UILabel.lens.numberOfLines .~ 2 - - _ = self.externalPlaysProgressView - |> dashboardVideoExternalPlaysProgressViewStyle - - _ = self.externalPlaysCountLabel - |> dashboardStatTitleLabelStyle - - _ = self.graphStatsContainerView - |> UIView.lens.backgroundColor .~ .ksr_support_100 - - _ = self.graphBackgroundView - |> containerViewBackgroundStyle - |> UIView.lens.accessibilityElementsHidden .~ true - - _ = self.graphStatsStackView - |> UIStackView.lens.isLayoutMarginsRelativeArrangement .~ true - |> UIStackView.lens.layoutMargins .~ .init(topBottom: Styles.grid(2), leftRight: Styles.grid(1)) - - _ = self.internalLabel - |> dashboardStatSubtitleLabelStyle - |> UILabel.lens.numberOfLines .~ 2 - - _ = self.internalPlaysProgressView - |> dashboardVideoInternalPlaysProgressViewStyle - - _ = self.internalPlaysCountLabel - |> dashboardStatTitleLabelStyle - - _ = self.separatorViews - ||> separatorStyle - - _ = self.statsContainerView - |> dashboardCardStyle - - _ = self.totalPlaysContainerView - |> UIView.lens.backgroundColor .~ .ksr_support_100 - - _ = self.totalPlaysCountLabel - |> dashboardStatTitleLabelStyle - - _ = self.totalPlaysStackView - |> UIStackView.lens.spacing .~ Styles.grid(1) - |> UIStackView.lens.isLayoutMarginsRelativeArrangement .~ true - |> UIStackView.lens.layoutMargins .~ .init(all: Styles.grid(2)) - - _ = self.videoPlaysTitleLabel |> dashboardVideoPlaysTitleLabelStyle - } - - internal override func bindViewModel() { - self.completionPercentageLabel.rac.text = self.viewModel.outputs.completionPercentage - self.externalPlaysCountLabel.rac.text = self.viewModel.outputs.externalStartCount - self.internalPlaysCountLabel.rac.text = self.viewModel.outputs.internalStartCount - self.internalLabel.rac.text = self.viewModel.outputs.internalText - self.externalLabel.rac.text = self.viewModel.outputs.externalText - self.totalPlaysCountLabel.rac.attributedText = self.viewModel.outputs.totalStartCount - - self.viewModel.outputs.externalStartProgress - .observeForUI() - .observeValues { [weak element = externalPlaysProgressView] progress in - let anchorY = progress == 0 ? 0 : 0.5 / progress - element?.layer.anchorPoint = CGPoint(x: 0.5, y: 1 - CGFloat(anchorY)) - element?.transform = CGAffineTransform(scaleX: 1.0, y: CGFloat(progress)) - } - - self.viewModel.outputs.internalStartProgress - .observeForUI() - .observeValues { [weak element = internalPlaysProgressView] progress in - let anchorY = progress == 0 ? 0 : 0.5 / progress - element?.layer.anchorPoint = CGPoint(x: 0.5, y: 1 - CGFloat(anchorY)) - element?.transform = CGAffineTransform(scaleX: 1.0, y: CGFloat(progress)) - } - } - - internal func configureWith(value: ProjectStatsEnvelope.VideoStats) { - self.viewModel.inputs.configureWith(videoStats: value) - } -} diff --git a/Kickstarter-iOS/Features/Dashboard/Views/DashboardReferrerRowStackView.swift b/Kickstarter-iOS/Features/Dashboard/Views/DashboardReferrerRowStackView.swift deleted file mode 100644 index 2774dd9cd8..0000000000 --- a/Kickstarter-iOS/Features/Dashboard/Views/DashboardReferrerRowStackView.swift +++ /dev/null @@ -1,43 +0,0 @@ -import KsApi -import Library -import Prelude -import UIKit - -internal final class DashboardReferrerRowStackView: UIStackView { - fileprivate let viewModel: DashboardReferrerRowStackViewViewModelType - = DashboardReferrerRowStackViewViewModel() - - fileprivate let backersLabel: UILabel = UILabel() - fileprivate let pledgedLabel: UILabel = UILabel() - fileprivate let sourceLabel: UILabel = UILabel() - - internal init( - frame: CGRect, - country: Project.Country, - referrer: ProjectStatsEnvelope.ReferrerStats - ) { - super.init(frame: frame) - - _ = self |> dashboardStatsRowStackViewStyle - - _ = self.backersLabel |> dashboardColumnTextLabelStyle - _ = self.pledgedLabel |> dashboardColumnTextLabelStyle - _ = self.sourceLabel |> dashboardReferrersSourceLabelStyle - - self.addArrangedSubview(self.sourceLabel) - self.addArrangedSubview(self.pledgedLabel) - self.addArrangedSubview(self.backersLabel) - - self.backersLabel.rac.text = self.viewModel.outputs.backersText - self.pledgedLabel.rac.text = self.viewModel.outputs.pledgedText - - self.sourceLabel.rac.text = self.viewModel.outputs.sourceText - self.sourceLabel.rac.textColor = self.viewModel.outputs.textColor - - self.viewModel.inputs.configureWith(country: country, referrer: referrer) - } - - required init(coder aDecoder: NSCoder) { - fatalError("init(coder:\(aDecoder)) has not been implemented") - } -} diff --git a/Kickstarter-iOS/Features/Dashboard/Views/DashboardRewardRowStackView.swift b/Kickstarter-iOS/Features/Dashboard/Views/DashboardRewardRowStackView.swift deleted file mode 100644 index c17f7b0631..0000000000 --- a/Kickstarter-iOS/Features/Dashboard/Views/DashboardRewardRowStackView.swift +++ /dev/null @@ -1,47 +0,0 @@ -import KsApi -import Library -import Prelude -import UIKit - -internal final class DashboardRewardRowStackView: UIStackView { - fileprivate let vm: DashboardRewardRowStackViewViewModelType = DashboardRewardRowStackViewViewModel() - - fileprivate let rewardsLabel: UILabel = UILabel() - fileprivate let backersLabel: UILabel = UILabel() - fileprivate let pledgedLabel: UILabel = UILabel() - - internal init( - frame: CGRect, - country: Project.Country, - reward: ProjectStatsEnvelope.RewardStats, - totalPledged: Int - ) { - super.init(frame: frame) - - _ = self - |> dashboardStatsRowStackViewStyle - |> UIStackView.lens.layoutMargins .~ .init(top: 0, left: Styles.grid(1), bottom: 0, right: 0) - - _ = self.rewardsLabel - |> dashboardColumnTextLabelStyle - |> UILabel.lens.font .~ UIFont.ksr_subhead().bolded - |> UILabel.lens.numberOfLines .~ 0 - - _ = self.pledgedLabel |> dashboardColumnTextLabelStyle - _ = self.backersLabel |> dashboardColumnTextLabelStyle - - self.addArrangedSubview(self.rewardsLabel) - self.addArrangedSubview(self.pledgedLabel) - self.addArrangedSubview(self.backersLabel) - - self.rewardsLabel.rac.text = self.vm.outputs.topRewardText - self.pledgedLabel.rac.text = self.vm.outputs.pledgedText - self.backersLabel.rac.text = self.vm.outputs.backersText - - self.vm.inputs.configureWith(country: country, reward: reward, totalPledged: totalPledged) - } - - required init(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} diff --git a/Kickstarter-iOS/Features/Dashboard/Views/DashboardTitleView.swift b/Kickstarter-iOS/Features/Dashboard/Views/DashboardTitleView.swift deleted file mode 100644 index 6aa8994ff9..0000000000 --- a/Kickstarter-iOS/Features/Dashboard/Views/DashboardTitleView.swift +++ /dev/null @@ -1,96 +0,0 @@ -import KsApi -import Library -import Prelude -import Prelude_UIKit -import UIKit - -internal protocol DashboardTitleViewDelegate: AnyObject { - /// Call when dashboard should show/hide the projects drawer view controller. - func dashboardTitleViewShowHideProjectsDrawer() -} - -internal final class DashboardTitleView: UIView { - fileprivate let viewModel: DashboardTitleViewViewModelType = DashboardTitleViewViewModel() - - @IBOutlet fileprivate var titleButton: UIButton! - @IBOutlet fileprivate var titleLabel: UILabel! - @IBOutlet fileprivate var arrowImageView: UIImageView! - - internal weak var delegate: DashboardTitleViewDelegate? - - override func awakeFromNib() { - super.awakeFromNib() - - _ = self.titleButton - |> UIButton.lens.contentEdgeInsets %~ { insets in .init(topBottom: insets.top, leftRight: 0) } - |> UIButton.lens.accessibilityLabel %~ { _ in Strings.tabbar_dashboard() } - |> UIButton.lens.accessibilityTraits .~ UIAccessibilityTraits.staticText - |> UIButton.lens.targets .~ [(self, #selector(self.titleButtonTapped), .touchUpInside)] - - _ = self.arrowImageView - |> UIImageView.lens.isHidden .~ true - |> UIImageView.lens.tintColor .~ .ksr_support_700 - - _ = self.titleLabel |> dashboardTitleViewTextDisabledStyle - - self.titleButton.rac.accessibilityLabel = self.viewModel.outputs.titleAccessibilityLabel - self.titleButton.rac.accessibilityHint = self.viewModel.outputs.titleAccessibilityHint - self.titleLabel.rac.text = self.viewModel.outputs.titleText - self.titleButton.rac.enabled = self.viewModel.outputs.titleButtonIsEnabled - - self.viewModel.outputs.hideArrow - .observeForUI() - .observeValues { [weak self] hide in - guard let _self = self else { return } - UIView.animate(withDuration: 0.2) { - _self.arrowImageView.isHidden = hide - } - if !hide { - _ = _self.titleButton |> UIView.lens.accessibilityTraits .~ UIAccessibilityTraits.button - } - } - - self.viewModel.outputs.updateArrowState - .observeForUI() - .observeValues { [weak self] drawerState in - self?.animateArrow(forDrawerState: drawerState) - } - - self.viewModel.outputs.notifyDelegateShowHideProjectsDrawer - .observeForUI() - .observeValues { [weak self] in - self?.delegate?.dashboardTitleViewShowHideProjectsDrawer() - } - - self.viewModel.outputs.titleButtonIsEnabled - .observeForUI() - .observeValues { [weak self] isEnabled in - guard let _titleLabel = self?.titleLabel else { return } - if isEnabled { - _ = _titleLabel |> dashboardTitleViewTextEnabledStyle - } - } - } - - internal func updateData(_ data: DashboardTitleViewData) { - self.viewModel.inputs.updateData(data) - } - - fileprivate func animateArrow(forDrawerState drawerState: DrawerState) { - var scale: CGFloat = 1.0 - switch drawerState { - case .open: - scale = -1.0 - case .closed: - scale = 1.0 - } - - UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseOut, animations: { - self.arrowImageView.transform = CGAffineTransform(scaleX: 1.0, y: scale) - }, completion: nil) - } - - @objc fileprivate func titleButtonTapped() { - self.viewModel.inputs.titleButtonTapped() - } -} diff --git a/Kickstarter-iOS/Features/Dashboard/Views/ReferralChartView.swift b/Kickstarter-iOS/Features/Dashboard/Views/ReferralChartView.swift deleted file mode 100644 index a7d3b63b7c..0000000000 --- a/Kickstarter-iOS/Features/Dashboard/Views/ReferralChartView.swift +++ /dev/null @@ -1,85 +0,0 @@ -import Library -import UIKit - -public final class ReferralChartView: UIView { - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - public init() { - super.init(frame: .zero) - self.backgroundColor = .clear - } - - public var internalPercentage: CGFloat = 0.0 { - didSet { - self.setNeedsDisplay() - } - } - - public var externalPercentage: CGFloat = 0.0 { - didSet { - self.setNeedsDisplay() - } - } - - public override func draw(_ rect: CGRect) { - super.draw(rect) - - guard let context = UIGraphicsGetCurrentContext() else { return } - - let internalPercentageAngle = CGFloat(-.pi / 2.0) + self.internalPercentage * CGFloat(2.0 * .pi) - let internalAndExternalPercentage = min(self.internalPercentage + self.externalPercentage, 1.0) - let internalAndExternalPercentageAngle - = CGFloat(-.pi / 2.0) + internalAndExternalPercentage * CGFloat(2.0 * .pi) - - UIColor.ksr_create_700.set() - context.move(to: CGPoint(x: self.bounds.width / 2, y: self.bounds.height / 2)) - context.addLine(to: CGPoint(x: self.bounds.width / 2, y: 0)) - context.addArc( - center: .init(x: self.bounds.width / 2, y: self.bounds.height / 2), - radius: self.bounds.width / 2, - startAngle: CGFloat(-.pi / 2.0), - endAngle: internalPercentageAngle, - clockwise: false - ) - context.closePath() - context.fillPath() - - UIColor.ksr_celebrate_500.set() - context.move(to: CGPoint(x: self.bounds.width / 2, y: self.bounds.height / 2)) - context.addLine( - to: CGPoint( - x: self.bounds.width / 2 + self.bounds.width / 2 * cos(internalPercentageAngle), - y: self.bounds.height / 2 + self.bounds.height / 2 * sin(internalPercentageAngle) - ) - ) - context.addArc( - center: .init(x: self.bounds.width / 2, y: self.bounds.height / 2), - radius: self.bounds.width / 2, - startAngle: CGFloat(-.pi / 2.0) + self.internalPercentage * CGFloat(2.0 * .pi), - endAngle: CGFloat(-.pi / 2.0) + internalAndExternalPercentage * CGFloat(2.0 * .pi), - clockwise: false - ) - context.closePath() - context.fillPath() - - UIColor.ksr_trust_500.set() - context.move(to: CGPoint(x: self.bounds.width / 2, y: self.bounds.height / 2)) - context.addLine( - to: CGPoint( - x: self.bounds.width / 2 + self.bounds.width / 2 * cos(internalAndExternalPercentageAngle), - y: self.bounds.height / 2 + self.bounds.height / 2 * sin(internalAndExternalPercentageAngle) - ) - ) - context.addArc( - center: .init(x: self.bounds.width / 2, y: self.bounds.height / 2), - radius: self.bounds.height / 2, - startAngle: CGFloat(-.pi / 2.0) + internalAndExternalPercentage * CGFloat(2.0 * .pi), - endAngle: CGFloat(-.pi / 2.0), - clockwise: false - ) - context.closePath() - context.fillPath() - } -} diff --git a/Kickstarter-iOS/Features/DashboardDeprecationBanner/DashboardDeprecationView.swift b/Kickstarter-iOS/Features/DashboardDeprecationBanner/DashboardDeprecationView.swift deleted file mode 100644 index a054fd3ea0..0000000000 --- a/Kickstarter-iOS/Features/DashboardDeprecationBanner/DashboardDeprecationView.swift +++ /dev/null @@ -1,52 +0,0 @@ -import Library -import SwiftUI - -struct DashboardDeprecationView: View { - private let contentPadding = 12.0 - private let imageSizeMultiplier = 1.5 - private var deprecationDateText: String { - self.formatted(dateString: "2023-09-5") - } - - var body: some View { - HStack { - if let iconImage = image(named: "fix-icon", inBundle: Bundle.framework) { - Image(uiImage: iconImage) - .frame(width: contentPadding * imageSizeMultiplier) - .scaledToFit() - .foregroundColor(Color(UIColor.ksr_white)) - .padding(.horizontal, contentPadding) - } - - Text(Strings.Creator_dashboard_removal_warning(expiration_date: deprecationDateText)) - .font(Font(UIFont.ksr_subhead(size: 15))) - .foregroundColor(Color(UIColor.ksr_white)) - .lineLimit(nil) - .padding([.vertical, .trailing], contentPadding) - } - .frame(maxWidth: .infinity, alignment: .center) - .background(Color(UIColor.ksr_alert)) - } - - private func formatted(dateString: String) -> String { - let date = self.toDate(dateString: dateString) - return Format.date( - secondsInUTC: date.timeIntervalSince1970, - template: "MMMM d, yyyy", - timeZone: UTCTimeZone - ) - } - - private func toDate(dateString: String) -> Date { - // Always use UTC timezone here this date should be timezone agnostic - guard let date = Format.date( - from: dateString, - dateFormat: "yyyy-MM-dd", - timeZone: UTCTimeZone - ) else { - fatalError("Unable to parse date format") - } - - return date - } -} diff --git a/Kickstarter-iOS/Features/DashboardProjects/Controller/DashboardProjectsDrawerViewController.swift b/Kickstarter-iOS/Features/DashboardProjects/Controller/DashboardProjectsDrawerViewController.swift deleted file mode 100644 index 96335d20c9..0000000000 --- a/Kickstarter-iOS/Features/DashboardProjects/Controller/DashboardProjectsDrawerViewController.swift +++ /dev/null @@ -1,154 +0,0 @@ -import KsApi -import Library -import Prelude -import Prelude_UIKit -import UIKit - -internal protocol DashboardProjectsDrawerViewControllerDelegate: AnyObject { - /// Call when a project cell is tapped with the project. - func dashboardProjectsDrawerCellDidTapProject(_ project: Project) - - /// Call when drawer view has completed animating out. - func dashboardProjectsDrawerDidAnimateOut() - - /// Call when background view is tapped to close. - func dashboardProjectsDrawerHideDrawer() -} - -internal final class DashboardProjectsDrawerViewController: UITableViewController { - internal weak var delegate: DashboardProjectsDrawerViewControllerDelegate? - - fileprivate let viewModel: DashboardProjectsDrawerViewModelType = DashboardProjectsDrawerViewModel() - fileprivate let dataSource = DashboardProjectsDrawerDataSource() - - internal static func configuredWith(data: [ProjectsDrawerData]) - -> DashboardProjectsDrawerViewController { - let vc = Storyboard.DashboardProjectsDrawer.instantiate(DashboardProjectsDrawerViewController.self) - vc.viewModel.inputs.configureWith(data: data) - return vc - } - - internal override func viewDidLoad() { - super.viewDidLoad() - - self.tableView.dataSource = self.dataSource - - self.viewModel.inputs.viewDidLoad() - } - - override func viewWillDisappear(_: Bool) { - if let tapGesture = self.tableView.backgroundView?.gestureRecognizers?.first { - self.tableView.backgroundView?.removeGestureRecognizer(tapGesture) - } - } - - internal override func bindViewModel() { - super.bindViewModel() - - self.viewModel.outputs.projectsDrawerData - .observeForControllerAction() - .observeValues { [weak self] data in - self?.dataSource.load(data: data) - self?.tableView.reloadData() - self?.animateIn() - } - - self.viewModel.outputs.notifyDelegateToCloseDrawer - .observeForControllerAction() - .observeValues { [weak self] in - self?.delegate?.dashboardProjectsDrawerHideDrawer() - } - - self.viewModel.outputs.notifyDelegateDidAnimateOut - .observeForControllerAction() - .observeValues { [weak self] in - self?.delegate?.dashboardProjectsDrawerDidAnimateOut() - } - - self.viewModel.outputs.notifyDelegateProjectCellTapped - .observeForControllerAction() - .observeValues { [weak self] project in - self?.delegate?.dashboardProjectsDrawerCellDidTapProject(project) - } - - self.viewModel.outputs.focusScreenReaderOnFirstProject - .observeForControllerAction() - .observeValues { [weak self] in - self?.accessibilityFocusOnFirstProject() - } - } - - override func bindStyles() { - _ = self - |> baseTableControllerStyle(estimatedRowHeight: 44.0) - |> UITableViewController.lens.view.backgroundColor .~ .clear - - _ = self.tableView |> UITableView.lens.backgroundView .~ ( - UIView() - |> UIView.lens.backgroundColor .~ .ksr_support_700 - |> UIView.lens.alpha .~ 0.0 - ) - - self.animateIn() - } - - internal override func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) { - guard let project = self.dataSource.projectAtIndexPath(indexPath) else { return } - - self.viewModel.inputs.projectCellTapped(project) - } - - internal func animateOut() { - self.tableView.backgroundView?.isUserInteractionEnabled = false - - UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseOut, animations: { - self.tableView.backgroundView?.alpha = 0.0 - self.tableView.contentOffset = CGPoint(x: 0.0, y: self.tableView.frame.size.height / 2) - }, completion: { _ in - self.viewModel.inputs.animateOutCompleted() - }) - } - - fileprivate func animateIn() { - self.tableView.contentOffset = CGPoint(x: 0.0, y: self.tableView.frame.size.height) - - UIView.animate(withDuration: 0.3, delay: 0.0, options: .curveEaseOut, animations: { - self.tableView.backgroundView?.alpha = 0.4 - }, completion: { _ in - self.tableView.backgroundView?.addGestureRecognizer( - UITapGestureRecognizer( - target: self, action: #selector(DashboardProjectsDrawerViewController.backgroundTapped) - ) - ) - }) - - UIView.animate( - withDuration: 0.3, - delay: 0.0, - usingSpringWithDamping: 0.95, - initialSpringVelocity: 0.9, - options: .curveEaseOut, - animations: { - self.tableView.contentOffset = CGPoint(x: 0.0, y: 0.0) - }, completion: { _ in - self.viewModel.inputs.animateInCompleted() - } - ) - } - - fileprivate func accessibilityFocusOnFirstProject() { - let cell = self.tableView.visibleCells.first { $0 is DashboardProjectsDrawerCell } - if let cell = cell { - UIAccessibility.post(notification: UIAccessibility.Notification.screenChanged, argument: cell) - } - } - - override func accessibilityPerformEscape() -> Bool { - self.viewModel.inputs.backgroundTapped() - return true - } - - @objc fileprivate func backgroundTapped() { - self.viewModel.inputs.backgroundTapped() - } -} diff --git a/Kickstarter-iOS/Features/DashboardProjects/Datasource/DashboardProjectsDrawerDataSource.swift b/Kickstarter-iOS/Features/DashboardProjects/Datasource/DashboardProjectsDrawerDataSource.swift deleted file mode 100644 index 7702b58f38..0000000000 --- a/Kickstarter-iOS/Features/DashboardProjects/Datasource/DashboardProjectsDrawerDataSource.swift +++ /dev/null @@ -1,28 +0,0 @@ -import Foundation -import KsApi -import Library -import UIKit - -internal final class DashboardProjectsDrawerDataSource: ValueCellDataSource { - internal func load(data: [ProjectsDrawerData]) { - self.set( - values: data, - cellClass: DashboardProjectsDrawerCell.self, - inSection: 0 - ) - } - - internal override func configureCell(tableCell cell: UITableViewCell, withValue value: Any) { - switch (cell, value) { - case let (cell as DashboardProjectsDrawerCell, value as ProjectsDrawerData): - cell.configureWith(value: value) - default: - assertionFailure("Unrecognized (\(cell), \(value)) combo.") - } - } - - internal func projectAtIndexPath(_ indexPath: IndexPath) -> Project? { - guard let data = self[indexPath] as? ProjectsDrawerData else { return nil } - return data.project - } -} diff --git a/Kickstarter-iOS/Features/DashboardProjects/Datasource/DashboardProjectsDrawerDataSourceTests.swift b/Kickstarter-iOS/Features/DashboardProjects/Datasource/DashboardProjectsDrawerDataSourceTests.swift deleted file mode 100644 index 0ded89248e..0000000000 --- a/Kickstarter-iOS/Features/DashboardProjects/Datasource/DashboardProjectsDrawerDataSourceTests.swift +++ /dev/null @@ -1,31 +0,0 @@ -@testable import Kickstarter_Framework -@testable import KsApi -@testable import Library -import Prelude -import XCTest - -internal final class DashboardProjectsDrawerDataSourceTests: XCTestCase { - let dataSource = DashboardProjectsDrawerDataSource() - let tableView = UITableView() - - func testDataSource() { - let project1 = Project.template - let project2 = .template |> Project.lens.id .~ 2 - - let data1 = ProjectsDrawerData(project: project1, indexNum: 0, isChecked: true) - let data2 = ProjectsDrawerData(project: project2, indexNum: 1, isChecked: false) - let data = [data1, data2] - - XCTAssertEqual(0, self.dataSource.numberOfSections(in: self.tableView)) - - self.dataSource.load(data: data) - - XCTAssertEqual(1, self.dataSource.numberOfSections(in: self.tableView)) - XCTAssertEqual(2, self.dataSource.tableView(self.tableView, numberOfRowsInSection: 0)) - XCTAssertEqual("DashboardProjectsDrawerCell", self.dataSource.reusableId(item: 0, section: 0)) - XCTAssertEqual("DashboardProjectsDrawerCell", self.dataSource.reusableId(item: 1, section: 0)) - - XCTAssertEqual(project1, self.dataSource.projectAtIndexPath(IndexPath(row: 0, section: 0))) - XCTAssertEqual(project2, self.dataSource.projectAtIndexPath(IndexPath(row: 1, section: 0))) - } -} diff --git a/Kickstarter-iOS/Features/DashboardProjects/Storyboard/DashboardProjectsDrawer.storyboard b/Kickstarter-iOS/Features/DashboardProjects/Storyboard/DashboardProjectsDrawer.storyboard deleted file mode 100644 index 5a11a39e7c..0000000000 --- a/Kickstarter-iOS/Features/DashboardProjects/Storyboard/DashboardProjectsDrawer.storyboard +++ /dev/null @@ -1,106 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Kickstarter-iOS/Features/DashboardProjects/Views/Cells/DashboardProjectsDrawerCell.swift b/Kickstarter-iOS/Features/DashboardProjects/Views/Cells/DashboardProjectsDrawerCell.swift deleted file mode 100644 index 579e039321..0000000000 --- a/Kickstarter-iOS/Features/DashboardProjects/Views/Cells/DashboardProjectsDrawerCell.swift +++ /dev/null @@ -1,45 +0,0 @@ -import KsApi -import Library -import Prelude -import UIKit - -internal final class DashboardProjectsDrawerCell: UITableViewCell, ValueCell { - @IBOutlet fileprivate var projectNumLabel: UILabel! - @IBOutlet fileprivate var projectNameLabel: UILabel! - @IBOutlet fileprivate var checkmarkImageView: UIImageView! - - fileprivate let viewModel: DashboardProjectsDrawerCellViewModelType = - DashboardProjectsDrawerCellViewModel() - - internal func configureWith(value: ProjectsDrawerData) { - self.viewModel.inputs.configureWith( - project: value.project, - indexNum: value.indexNum, - isChecked: value.isChecked - ) - } - - internal override func bindViewModel() { - super.bindViewModel() - - self.projectNumLabel.rac.text = self.viewModel.outputs.projectNumberText - self.projectNameLabel.rac.text = self.viewModel.outputs.projectNameText - self.checkmarkImageView.rac.hidden = self.viewModel.outputs.isCheckmarkHidden - - self.rac.accessibilityLabel = self.viewModel.outputs.cellAccessibilityLabel - self.rac.accessibilityValue = self.viewModel.outputs.cellAccessibilityValue - } - - internal override func bindStyles() { - super.bindStyles() - - _ = self - |> UITableViewCell.lens.isAccessibilityElement .~ true - |> UITableViewCell.lens.accessibilityTraits .~ UIAccessibilityTraits.button - - _ = self.projectNumLabel |> dashboardDrawerProjectNumberTextLabelStyle - _ = self.projectNameLabel |> dashboardDrawerProjectNameTextLabelStyle - - _ = self.checkmarkImageView |> UIImageView.lens.tintColor .~ .ksr_support_400 - } -} diff --git a/Kickstarter-iOS/Features/DashboardProjects/Views/FundingGraphView.swift b/Kickstarter-iOS/Features/DashboardProjects/Views/FundingGraphView.swift deleted file mode 100644 index 3e925525f0..0000000000 --- a/Kickstarter-iOS/Features/DashboardProjects/Views/FundingGraphView.swift +++ /dev/null @@ -1,276 +0,0 @@ -import KsApi -import Library -import Prelude -import UIKit - -private typealias Line = (start: CGPoint, end: CGPoint) - -public final class FundingGraphView: UIView { - fileprivate let goalLabel = UILabel() - - public override init(frame: CGRect) { - super.init(frame: frame) - self.setUp() - } - - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - self.setUp() - } - - fileprivate func setUp() { - self.backgroundColor = .clear - self.addSubview(self.goalLabel) - _ = self.goalLabel - |> UILabel.lens.font .~ .ksr_headline(size: 12) - |> UILabel.lens.textColor .~ .ksr_white - |> UILabel.lens.backgroundColor .~ .ksr_create_700 - |> UILabel.lens.textAlignment .~ .center - } - - internal var fundedPointRadius: CGFloat = 12.0 { - didSet { - self.setNeedsDisplay() - } - } - - internal var lineThickness: CGFloat = 1.5 { - didSet { - self.setNeedsDisplay() - } - } - - internal var project: Project! { - didSet { - self.setNeedsDisplay() - } - } - - internal var stats: [ProjectStatsEnvelope.FundingDateStats]! { - didSet { - self.setNeedsDisplay() - } - } - - internal var yAxisTickSize: CGFloat = 1.0 { - didSet { - self.setNeedsDisplay() - } - } - - public override func draw(_ rect: CGRect) { - super.draw(rect) - - // Map the date and pledged amount to (dayNumber, pledgedAmount). - let datePledgedPoints = self.stats - .enumerated() - .map { index, stat in CGPoint(x: index, y: stat.cumulativePledged) } - - var durationInDays: CGFloat = 1 - - if let launchedAt = project.dates.launchedAt, - let deadline = project.dates.deadline { - durationInDays = dateToDayNumber( - launchDate: launchedAt, - currentDate: deadline - ) - } - - let goal = self.project.stats.goal - - let pointsPerDay = (self.bounds.width - self.layoutMargins.left) / durationInDays - - let pointsPerDollar = self.bounds.height / - (CGFloat(DashboardFundingCellViewModel.tickCount) * self.yAxisTickSize) - - // Draw the funding progress grey line and fill. - let line = UIBezierPath() - line.lineWidth = self.lineThickness - - var lastPoint = CGPoint(x: self.layoutMargins.left, y: self.bounds.height) - line.move(to: lastPoint) - - datePledgedPoints.forEach { point in - let x = point.x * pointsPerDay + self.layoutMargins.left - let y = self.bounds.height - min(point.y * pointsPerDollar, self.bounds.height) - line.addLine(to: CGPoint(x: x, y: y)) - lastPoint = CGPoint(x: x, y: y) - } - - // Stroke the darker graph line before filling with lighter color. - UIColor.ksr_support_400.setStroke() - line.stroke() - - line.addLine(to: CGPoint(x: lastPoint.x, y: self.bounds.height)) - line.close() - - UIColor.ksr_support_300.setFill() - line.fill(with: .color, alpha: 0.4) - - let projectHasFunded = self.stats.last?.cumulativePledged ?? 0 >= goal - if projectHasFunded { - let rightFundedStat = self.stats.split { $0.cumulativePledged < goal }.last?.first - let rightFundedPoint = CGPoint( - x: dateToDayNumber( - launchDate: stats.first?.date ?? 0, - currentDate: rightFundedStat?.date ?? 0 - ), - y: CGFloat(rightFundedStat?.cumulativePledged ?? 0) - ) - - let leftFundedStat = self.stats.filter { $0.cumulativePledged < goal }.last - let leftFundedPoint = isNil(leftFundedStat) ? - CGPoint(x: 0.0, y: 0.0) : - CGPoint( - x: dateToDayNumber( - launchDate: self.stats.first?.date ?? 0, - currentDate: leftFundedStat?.date ?? 0 - ), - y: CGFloat(leftFundedStat?.cumulativePledged ?? 0) - ) - - let leftPointX = leftFundedPoint.x * pointsPerDay + self.layoutMargins.left - let leftPointY = self.bounds.height - min(leftFundedPoint.y * pointsPerDollar, self.bounds.height) - - let rightPointX = rightFundedPoint.x * pointsPerDay + self.layoutMargins.left - let rightPointY = self.bounds.height - min(rightFundedPoint.y * pointsPerDollar, self.bounds.height) - - // Surrounding left and right points, used to find slope of line containing funded point. - let lineAPoint1 = CGPoint(x: leftPointX, y: leftPointY) - let lineAPoint2 = CGPoint(x: rightPointX, y: rightPointY) - let lineA = Line(start: lineAPoint1, end: lineAPoint2) - - // Intersecting funded horizontal line. - let lineBPoint1 = CGPoint( - x: self.layoutMargins.left, - y: self.bounds.height - - min(CGFloat(goal) * pointsPerDollar, self.bounds.height) - ) - let lineBPoint2 = CGPoint( - x: self.bounds.width, - y: self.bounds.height - - min(CGFloat(goal) * pointsPerDollar, self.bounds.height) - ) - let lineB = Line(start: lineBPoint1, end: lineBPoint2) - - let fundedPoint = intersection(ofLine: lineA, withLine: lineB) - - let fundedDotOutline = UIBezierPath( - ovalIn: CGRect( - x: fundedPoint.x - (self.fundedPointRadius / 2), - y: fundedPoint.y - (self.fundedPointRadius / 2), - width: self.fundedPointRadius, - height: self.fundedPointRadius - ) - ) - - let fundedDotFill = UIBezierPath( - ovalIn: CGRect( - x: fundedPoint.x - (self.fundedPointRadius / 2 / 2), - y: fundedPoint.y - (self.fundedPointRadius / 2 / 2), - width: self.fundedPointRadius / 2, - height: self.fundedPointRadius / 2 - ) - ) - - // Draw funding progress line in green from funding point on. - let fundedProgressLine = UIBezierPath() - fundedProgressLine.lineWidth = self.lineThickness - - var lastFundedPoint = CGPoint(x: fundedPoint.x, y: fundedPoint.y) - fundedProgressLine.move(to: lastFundedPoint) - - datePledgedPoints.forEach { point in - let x = point.x * pointsPerDay + self.layoutMargins.left - if x >= fundedPoint.x { - let y = self.bounds.height - point.y * pointsPerDollar - fundedProgressLine.addLine(to: CGPoint(x: x, y: y)) - lastFundedPoint = CGPoint(x: x, y: y) - } - } - - // Stroke the darker graph line before filling with lighter color. - UIColor.ksr_create_700.setStroke() - fundedProgressLine.stroke() - - fundedProgressLine.addLine(to: CGPoint(x: lastFundedPoint.x, y: self.bounds.height)) - fundedProgressLine.addLine(to: CGPoint(x: fundedPoint.x, y: self.bounds.height)) - fundedProgressLine.close() - - UIColor.ksr_create_500.setFill() - fundedProgressLine.fill(with: .color, alpha: 0.4) - - UIColor.ksr_create_700.set() - fundedDotOutline.stroke() - fundedDotFill.fill() - - } else { - let goalLine = Line( - start: CGPoint(x: 0.0, y: self.bounds.height - CGFloat(goal) * pointsPerDollar), - end: CGPoint( - x: self.bounds.width, - y: self.bounds.height - CGFloat(goal) * pointsPerDollar - ) - ) - let goalPath = UIBezierPath() - goalPath.lineWidth = self.lineThickness / 2 - goalPath.move(to: goalLine.start) - goalPath.addLine(to: goalLine.end) - - UIColor.ksr_create_700.setStroke() - goalPath.stroke() - - self.goalLabel.text = Strings.dashboard_graphs_funding_goal() - self.goalLabel.sizeToFit() - - self.goalLabel.frame = self.goalLabel.frame.insetBy(dx: -6, dy: -3).integral - - self.goalLabel.center = CGPoint( - x: self.bounds.width - 16 - self.goalLabel.frame.width / 2, - y: goalLine.end.y - self.goalLabel.frame.height / 2 - ) - } - - self.goalLabel.isHidden = projectHasFunded - } -} - -// Calculates the point of intersection of Line 1 and Line 2. -private func intersection(ofLine line1: Line, withLine line2: Line) -> CGPoint { - guard line1.start.x != line1.end.x else { - return CGPoint(x: line1.start.x, y: line2.start.y) - } - - let line1Slope = slope(ofLine: line1) - let line1YIntercept = yIntercept(ofLine: line1) - - let line2Slope = slope(ofLine: line2) - let line2YIntercept = yIntercept(ofLine: line2) - - let x = (line2YIntercept - line1YIntercept) / (line1Slope - line2Slope) - let y = line1Slope * x + line1YIntercept - - return CGPoint(x: x, y: y) -} - -// Calculates where a given line will intercept the y-axis, if ever. -private func yIntercept(ofLine line: Line) -> CGFloat { - return slope(ofLine: line) * (-line.start.x) + line.start.y -} - -// Calculates the slope between two given points, if any. -private func slope(ofLine line: Line) -> CGFloat { - if line.start.x == line.end.x { - fatalError() - } - return (line.end.y - line.start.y) / (line.end.x - line.start.x) -} - -// Returns the day number, given the start and current date in seconds. -private func dateToDayNumber( - launchDate: TimeInterval, - currentDate: TimeInterval, - calendar _: Calendar = AppEnvironment.current.calendar -) -> CGFloat { - return CGFloat((currentDate - launchDate) / 60.0 / 60.0 / 24.0) -} diff --git a/Kickstarter-iOS/Features/DashboardProjects/Views/FundingGraphViewTests.swift b/Kickstarter-iOS/Features/DashboardProjects/Views/FundingGraphViewTests.swift deleted file mode 100644 index 256fc6fa6e..0000000000 --- a/Kickstarter-iOS/Features/DashboardProjects/Views/FundingGraphViewTests.swift +++ /dev/null @@ -1,138 +0,0 @@ -@testable import Kickstarter_Framework -@testable import KsApi -import Library -import Prelude -import ReactiveExtensions_TestHelpers -import SnapshotTesting -import XCTest - -internal final class FundingGraphViewTests: TestCase { - fileprivate let vm: DashboardFundingCellViewModelType = DashboardFundingCellViewModel() - fileprivate let graphData = TestObserver() - - override func setUp() { - super.setUp() - AppEnvironment.pushEnvironment(mainBundle: Bundle.framework) - self.vm.outputs.graphData.observe(self.graphData.observer) - } - - override func tearDown() { - super.tearDown() - AppEnvironment.popEnvironment() - } - - func testGoalLabelLanguages() { - let graphView = FundingGraphView(frame: CGRect(x: 0, y: 0, width: 300, height: 225)) - let stats = [3_000, 4_000, 5_000, 7_000, 8_000, 13_000, 14_000, 15_000, 17_000, 18_000] - - self.vm.inputs.configureWith( - fundingDateStats: fundingStats(forProject: project, pledgeValues: stats), - project: project - ) - - graphView.project = self.graphData.lastValue!.project - graphView.stats = self.graphData.lastValue!.stats - graphView.yAxisTickSize = self.graphData.lastValue!.yAxisTickSize - - Language.allLanguages.forEach { language in - withEnvironment(language: language) { - graphView.setNeedsDisplay() - - assertSnapshot(matching: graphView, as: .image, named: "lang_\(language)") - } - } - } - - func testGraphStates() { - let graphView = FundingGraphView(frame: CGRect(x: 0, y: 0, width: 300, height: 225)) - - let underFundedStats = [ - 3_000, 4_000, 5_000, 7_000, 8_000 - ] - - let justFundedStats = underFundedStats + [ - 13_000, 14_000, 15_000, 17_000, 18_000, - 20_000, 21_000, 22_000, 24_000 - ] - - let backUnderFundedStats = justFundedStats + [ - 23_000, 22_500, 20_000, 19_500, 18_000 - ] - - let backOverFunded = backUnderFundedStats + [ - 21_000, 21_500, 22_500, 24_000, 25_000, - 26_000, 29_000 - ] - - let oneDayLeft = backOverFunded + [ - 32_000, 38_000, 48_000, 50_000 - ] - - let completedStats = oneDayLeft + [55_000] - - let statStates = [ - "Under Funded": underFundedStats, - "Just Funded": justFundedStats, - "Back Under Funded": backUnderFundedStats, - "Back Over Funded": backOverFunded, - "One Day Left": oneDayLeft, - "Completed": completedStats - ] - - for (key, stats) in statStates { - self.vm.inputs.configureWith( - fundingDateStats: fundingStats(forProject: project, pledgeValues: stats), - project: project - ) - - graphView.project = self.graphData.lastValue!.project - graphView.stats = self.graphData.lastValue!.stats - graphView.yAxisTickSize = self.graphData.lastValue!.yAxisTickSize - - assertSnapshot(matching: graphView, as: .image, named: "state_\(key)") - } - } - - func testOneDayProject() { - let oneDayProject = .template - |> Project.lens.stats.goal .~ 2_000 - |> Project.lens.dates.launchedAt .~ 123_456_789.0 - |> Project.lens.dates.deadline .~ Double(123_456_789 + 60 * 60 * 24) - - let graphView = FundingGraphView(frame: CGRect(x: 0, y: 0, width: 300, height: 225)) - - let statStates = [ - "Under Funded": [200, 1_000], - "Just Funded": [200, 2_100], - "Way Over Funded": [3_000, 6_000] - ] - - for (key, stats) in statStates { - self.vm.inputs.configureWith( - fundingDateStats: fundingStats(forProject: oneDayProject, pledgeValues: stats), - project: oneDayProject - ) - - graphView.project = self.graphData.lastValue!.project - graphView.stats = self.graphData.lastValue!.stats - graphView.yAxisTickSize = self.graphData.lastValue!.yAxisTickSize - - assertSnapshot(matching: graphView, as: .image, named: "state_\(key)") - } - } -} - -private let project = .template - |> Project.lens.stats.goal .~ 22_000 - |> Project.lens.dates.launchedAt .~ 1_477_494_745.0 - |> Project.lens.dates.deadline .~ 1_480_187_443.0 - -private func fundingStats(forProject project: Project, pledgeValues: [Int]) - -> [ProjectStatsEnvelope.FundingDateStats] { - return pledgeValues.enumerated().map { (idx: Int, pledged: Int) in - ProjectStatsEnvelope.FundingDateStats.template - |> ProjectStatsEnvelope.FundingDateStats.lens.cumulativePledged .~ pledged - |> ProjectStatsEnvelope.FundingDateStats.lens.date - .~ ((project.dates.launchedAt ?? 0.0) + TimeInterval(idx * 60 * 60 * 24)) - } -} diff --git a/Kickstarter-iOS/Features/ProjectActivities/Controller/ProjectActivitiesViewController.swift b/Kickstarter-iOS/Features/ProjectActivities/Controller/ProjectActivitiesViewController.swift deleted file mode 100644 index 809059299e..0000000000 --- a/Kickstarter-iOS/Features/ProjectActivities/Controller/ProjectActivitiesViewController.swift +++ /dev/null @@ -1,197 +0,0 @@ -import KsApi -import Library -import Prelude -import UIKit - -internal final class ProjectActivitiesViewController: UITableViewController { - fileprivate let viewModel: ProjectActivitiesViewModelType = ProjectActivitiesViewModel() - fileprivate let dataSource = ProjectActivitiesDataSource() - - internal static func configuredWith(project: Project) -> ProjectActivitiesViewController { - let vc = Storyboard.ProjectActivity.instantiate(ProjectActivitiesViewController.self) - vc.viewModel.inputs.configureWith(project) - return vc - } - - internal override func viewDidLoad() { - super.viewDidLoad() - - self.tableView.dataSource = self.dataSource - - self.viewModel.inputs.viewDidLoad() - } - - internal override func bindViewModel() { - super.bindViewModel() - - self.viewModel.outputs.projectActivityData - .observeForUI() - .observeValues { [weak self] projectActivityData in - self?.dataSource.load(projectActivityData: projectActivityData) - self?.tableView.reloadData() - } - - self.viewModel.outputs.goTo - .observeForControllerAction() - .observeValues { [weak self] goTo in - switch goTo { - case let .backing(params): - self?.goToBacking(params: params) - case let .comments(project, update): - self?.goToComments(project: project, update: update) - case let .project(project): - self?.goToProject(project: project) - case let .sendReply(project, update, comment): - self?.goToSendReply(project: project, update: update, comment: comment) - case let .sendMessage(backing, context): - self?.goToSendMessage(backing: backing, context: context) - case let .update(project, update): - self?.goToUpdate(project: project, update: update) - } - } - - self.viewModel.outputs.showEmptyState - .observeForUI() - .observeValues { [weak self] visible in - self?.dataSource.emptyState(visible: visible) - self?.tableView.reloadData() - } - } - - internal override func bindStyles() { - super.bindStyles() - - _ = self - |> baseTableControllerStyle(estimatedRowHeight: 200.0) - - self.title = Strings.activity_navigation_title_activity() - } - - internal override func tableView( - _: UITableView, - willDisplay cell: UITableViewCell, - forRowAt indexPath: IndexPath - ) { - if let cell = cell as? ProjectActivityBackingCell, cell.delegate == nil { - cell.delegate = self - } else if let cell = cell as? ProjectActivityCommentCell, cell.delegate == nil { - cell.delegate = self - } - - self.viewModel.inputs.willDisplayRow( - self.dataSource.itemIndexAt(indexPath), - outOf: self.dataSource.numberOfItems() - ) - } - - internal override func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) { - guard let (activity, project) = self.dataSource.activityAndProjectAtIndexPath(indexPath) else { return } - self.viewModel.inputs.activityAndProjectCellTapped(activity: activity, project: project) - } - - internal func goToBacking(params: ManagePledgeViewParamConfigData) { - let vc = ManagePledgeViewController.controller(with: params) - - if self.traitCollection.userInterfaceIdiom == .pad { - vc.modalPresentationStyle = UIModalPresentationStyle.formSheet - } - - self.present(vc, animated: true) - } - - internal func goToComments(project: Project?, update: Update?) { - let vc = commentsViewController(for: project, update: update) - if self.traitCollection.userInterfaceIdiom == .pad { - let nav = UINavigationController(rootViewController: vc) - nav.modalPresentationStyle = UIModalPresentationStyle.formSheet - self.present(nav, animated: true, completion: nil) - } else { - self.navigationController?.pushViewController(vc, animated: true) - } - } - - internal func goToProject(project: Project) { - let projectParam = Either(left: project) - let vc = ProjectPageViewController.configuredWith( - projectOrParam: projectParam, - refTag: .dashboardActivity - ) - - let nav = NavigationController(rootViewController: vc) - nav.modalPresentationStyle = self.traitCollection.userInterfaceIdiom == .pad ? .fullScreen : .formSheet - - self.present(nav, animated: true, completion: nil) - } - - internal func goToSendMessage( - backing: Backing, - context: KSRAnalytics.MessageDialogContext - ) { - let vc = MessageDialogViewController.configuredWith(messageSubject: .backing(backing), context: context) - vc.modalPresentationStyle = .formSheet - vc.delegate = self - self.present( - UINavigationController(rootViewController: vc), - animated: true, - completion: nil - ) - } - - internal func goToSendReply(project: Project, update: Update?, comment: ActivityComment) { - let dialog = CommentDialogViewController - .configuredWith(project: project, update: update, recipient: comment.author, context: .projectActivity) - dialog.modalPresentationStyle = .formSheet - dialog.delegate = self - self.present( - UINavigationController(rootViewController: dialog), - animated: true, - completion: nil - ) - } - - internal func goToUpdate(project: Project, update: Update) { - let vc = UpdateViewController.configuredWith(project: project, update: update, context: .creatorActivity) - self.navigationController?.pushViewController(vc, animated: true) - } -} - -extension ProjectActivitiesViewController: MessageDialogViewControllerDelegate { - internal func messageDialogWantsDismissal(_ dialog: MessageDialogViewController) { - dialog.dismiss(animated: true, completion: nil) - } - - internal func messageDialog(_: MessageDialogViewController, postedMessage _: Message) {} -} - -extension ProjectActivitiesViewController: ProjectActivityBackingCellDelegate { - internal func projectActivityBackingCellGoToBacking(project: Project, backing: Backing) { - self.viewModel.inputs.projectActivityBackingCellGoToBacking(project: project, backing: backing) - } - - internal func projectActivityBackingCellGoToSendMessage(project: Project, backing: Backing) { - self.viewModel.inputs.projectActivityBackingCellGoToSendMessage(project: project, backing: backing) - } -} - -extension ProjectActivitiesViewController: ProjectActivityCommentCellDelegate { - internal func projectActivityCommentCellGoToBacking(project: Project, user: User) { - self.viewModel.inputs.projectActivityCommentCellGoToBacking(project: project, user: user) - } - - func projectActivityCommentCellGoToSendReply(project: Project, update: Update?, - comment: ActivityComment) { - self.viewModel.inputs.projectActivityCommentCellGoToSendReply( - project: project, - update: update, - comment: comment - ) - } -} - -extension ProjectActivitiesViewController: CommentDialogDelegate { - internal func commentDialogWantsDismissal(_ dialog: CommentDialogViewController) { - dialog.dismiss(animated: true, completion: nil) - } - - internal func commentDialog(_: CommentDialogViewController, postedComment _: Comment) {} -} diff --git a/Kickstarter-iOS/Features/ProjectActivities/Controller/ProjectActivityViewControllerTests.swift b/Kickstarter-iOS/Features/ProjectActivities/Controller/ProjectActivityViewControllerTests.swift deleted file mode 100644 index d04ddfa064..0000000000 --- a/Kickstarter-iOS/Features/ProjectActivities/Controller/ProjectActivityViewControllerTests.swift +++ /dev/null @@ -1,126 +0,0 @@ -@testable import Kickstarter_Framework -@testable import KsApi -import Library -import Prelude -import SnapshotTesting -import XCTest - -internal final class ProjectActivityViewControllerTests: TestCase { - override func setUp() { - super.setUp() - AppEnvironment.pushEnvironment( - apiService: MockService( - oauthToken: OauthToken(token: "deadbeef"), - fetchProjectActivitiesResponse: activityCategories.map { - baseActivity |> Activity.lens.category .~ $0 - } - + backingActivities - ), - currentUser: Project.cosmicSurgery.creator, - mainBundle: Bundle.framework - ) - - UIView.setAnimationsEnabled(false) - } - - override func tearDown() { - AppEnvironment.popEnvironment() - UIView.setAnimationsEnabled(true) - super.tearDown() - } - - func testProjectActivityView() { - combos(Language.allLanguages, Device.allCases).forEach { language, device in - withEnvironment(language: language) { - let controller = ProjectActivitiesViewController.configuredWith(project: project) - let (parent, _) = traitControllers(device: device, orientation: .portrait, child: controller) - - self.scheduler.run() - - assertSnapshot( - matching: parent.view, - as: .image(perceptualPrecision: 0.98), - named: "lang_\(language)_device_\(device)" - ) - } - } - } -} - -private let project = - Project.cosmicSurgery - |> Project.lens.dates.deadline .~ 123_456_789.0 - |> Project.lens.dates.launchedAt .~ 123_456_789.0 - |> Project.lens.photo.small .~ "" - |> Project.lens.photo.med .~ "" - |> Project.lens.photo.full .~ "" - -private let user = - User.brando - |> \.avatar.large .~ "" - |> \.avatar.medium .~ "" - |> \.avatar.small .~ "" - -private let baseActivity = - .template - |> Activity.lens.createdAt .~ 123_456_789.0 - |> Activity.lens.memberData.amount .~ 25 - |> Activity.lens.project .~ project - |> Activity.lens.update .~ ( - .template - |> Update.lens.title .~ "Spirit animal reward available again" - |> Update.lens.body .~ ("Due to popular demand, and the inspirational momentum of this project, we've" - + " added more spirit animal rewards!") - |> Update.lens.publishedAt .~ 123_456_789.0 - ) - |> Activity.lens.user .~ user - |> Activity.lens.memberData.backing .~ .some( - .template - |> Backing.lens.amount .~ 25 - |> Backing.lens.backerId .~ user.id - |> Backing.lens.backer .~ user - ) - -private let backingActivity = - baseActivity - |> Activity.lens.category .~ .backing - |> Activity.lens.memberData.amount .~ 25 - |> Activity.lens.memberData.rewardId .~ 1 - -private let backingAmountActivity = - baseActivity - |> Activity.lens.category .~ .backingAmount - |> Activity.lens.memberData.newAmount .~ 25 - |> Activity.lens.memberData.newRewardId .~ 1 - |> Activity.lens.memberData.oldRewardId .~ 2 - |> Activity.lens.memberData.oldAmount .~ 100 - -private let backingCanceledActivity = - baseActivity - |> Activity.lens.category .~ .backingCanceled - |> Activity.lens.memberData.amount .~ 25 - |> Activity.lens.memberData.rewardId .~ 1 - -private let backingRewardActivity = - baseActivity - |> Activity.lens.category .~ .backingReward - |> Activity.lens.memberData.newRewardId .~ 1 - |> Activity.lens.memberData.oldRewardId .~ 2 - -private let activityCategories: [Activity.Category] = [ - .update, - .suspension, - .cancellation, - .failure, - .success, - .launch, - .commentPost, - .commentProject -] - -private let backingActivities = [ - backingActivity, - backingAmountActivity, - backingCanceledActivity, - backingRewardActivity -] diff --git a/Kickstarter-iOS/Features/ProjectActivities/Datasource/ProjectActivitiesDataSource.swift b/Kickstarter-iOS/Features/ProjectActivities/Datasource/ProjectActivitiesDataSource.swift deleted file mode 100644 index a274810a6e..0000000000 --- a/Kickstarter-iOS/Features/ProjectActivities/Datasource/ProjectActivitiesDataSource.swift +++ /dev/null @@ -1,124 +0,0 @@ -import KsApi -import Library -import UIKit - -internal final class ProjectActivitiesDataSource: ValueCellDataSource { - internal enum Section: Int { - case emptyState - case activities - } - - internal func emptyState(visible: Bool) { - self.set( - values: visible ? [()] : [], - cellClass: ProjectActivityEmptyStateCell.self, - inSection: Section.emptyState.rawValue - ) - } - - internal func load(projectActivityData: ProjectActivityData) { - let section = Section.activities.rawValue - - self.clearValues(section: section) - - projectActivityData.activities - .groupedBy { activity in - AppEnvironment.current.calendar.startOfDay( - for: Date(timeIntervalSince1970: activity.createdAt) - ) - } - .sorted { $0.0.timeIntervalSince1970 > $1.0.timeIntervalSince1970 } - .forEach { date, activitiesForDate in - - if projectActivityData.groupedDates { - appendDateRow(date: date, section: section) - } - - activitiesForDate - .sorted(comparator: Activity.lens.createdAt.comparator.reversed) - .forEach { activity in - if !projectActivityData.groupedDates { - appendDateRow(date: date, section: section) - } - appendActivityRow(activity: activity, project: projectActivityData.project, section: section) - } - } - } - - internal override func configureCell(tableCell cell: UITableViewCell, withValue value: Any) { - switch (cell, value) { - case let (cell as ProjectActivityBackingCell, value as (Activity, Project)): - cell.configureWith(value: value) - case let (cell as ProjectActivityCommentCell, value as (Activity, Project)): - cell.configureWith(value: value) - case let (cell as ProjectActivityDateCell, value as Date): - cell.configureWith(value: value) - case let (cell as ProjectActivityEmptyStateCell, value as Void): - cell.configureWith(value: value) - case let (cell as ProjectActivityLaunchCell, value as (Activity, Project)): - cell.configureWith(value: value) - case let (cell as ProjectActivityNegativeStateChangeCell, value as (Activity, Project)): - cell.configureWith(value: value) - case let (cell as ProjectActivitySuccessCell, value as (Activity, Project)): - cell.configureWith(value: value) - case let (cell as ProjectActivityUpdateCell, value as (Activity, Project)): - cell.configureWith(value: value) - case (is StaticTableViewCell, is Void): - return - default: - assertionFailure("Unrecognized combo: \(cell), \(value)") - } - } - - internal func appendDateRow(date: Date, section: Int) { - self.appendRow(value: date, cellClass: ProjectActivityDateCell.self, toSection: section) - } - - internal func appendActivityRow(activity: Activity, project: Project, section: Int) { - switch activity.category { - case .backing, .backingAmount, .backingCanceled, .backingReward: - self.appendRow( - value: (activity, project), - cellClass: ProjectActivityBackingCell.self, - toSection: section - ) - case .cancellation, .failure, .suspension: - self.appendRow( - value: (activity, project), - cellClass: ProjectActivityNegativeStateChangeCell.self, - toSection: section - ) - case .commentPost, .commentProject: - self.appendRow( - value: (activity, project), - cellClass: ProjectActivityCommentCell.self, - toSection: section - ) - case .launch: - self.appendRow( - value: (activity, project), - cellClass: ProjectActivityLaunchCell.self, - toSection: section - ) - case .success: - self.appendRow( - value: (activity, project), - cellClass: ProjectActivitySuccessCell.self, - toSection: section - ) - case .update: - self.appendRow( - value: (activity, project), - cellClass: ProjectActivityUpdateCell.self, - toSection: section - ) - case .backingDropped, .follow, .funding, .watch, .unknown: - assertionFailure("Unsupported activity: \(activity)") - } - } - - internal func activityAndProjectAtIndexPath(_ indexPath: IndexPath) -> (Activity, Project)? { - guard let value = self[indexPath] as? (Activity, Project) else { return nil } - return value - } -} diff --git a/Kickstarter-iOS/Features/ProjectActivities/Datasource/ProjectActivitiesDataSourceTests.swift b/Kickstarter-iOS/Features/ProjectActivities/Datasource/ProjectActivitiesDataSourceTests.swift deleted file mode 100644 index f4a9740d8d..0000000000 --- a/Kickstarter-iOS/Features/ProjectActivities/Datasource/ProjectActivitiesDataSourceTests.swift +++ /dev/null @@ -1,97 +0,0 @@ -@testable import Kickstarter_Framework -@testable import KsApi -@testable import Library -import Prelude -import XCTest - -internal final class ProjectActivitiesDataSourceTests: XCTestCase { - let dataSource = ProjectActivitiesDataSource() - let tableView = UITableView() - - func testDataSource() { - let timeZone = TimeZone(abbreviation: "UTC")! - var calendar = Calendar(identifier: .gregorian) - calendar.timeZone = timeZone - - withEnvironment(calendar: calendar) { - let section = ProjectActivitiesDataSource.Section.activities.rawValue - let project = Project.template - let activities = [ - Activity.template - |> Activity.lens.category .~ Activity.Category.backing - |> Activity.lens.createdAt .~ 1_474_606_800 // 2016-09-23T05:00:00Z - |> Activity.lens.project .~ project, - Activity.template - |> Activity.lens.category .~ Activity.Category.commentPost - |> Activity.lens.createdAt .~ 1_474_605_000 // 2016-09-23T04:30:00Z - |> Activity.lens.project .~ project, - Activity.template - |> Activity.lens.category .~ Activity.Category.success - |> Activity.lens.createdAt .~ 1_474_700_400 // 2016-09-24T07:00:00Z - |> Activity.lens.project .~ project, - Activity.template - |> Activity.lens.category .~ Activity.Category.launch - |> Activity.lens.createdAt .~ 1_474_538_400 // 2016-09-22T10:00:00Z - |> Activity.lens.project .~ project - ] - - self.dataSource.load( - projectActivityData: - ProjectActivityData(activities: activities, project: project, groupedDates: true) - ) - - XCTAssertEqual(section + 1, self.dataSource.numberOfSections(in: tableView)) - XCTAssertEqual(7, self.dataSource.tableView(tableView, numberOfRowsInSection: section)) - XCTAssertEqual("ProjectActivityDateCell", self.dataSource.reusableId(item: 0, section: section)) - XCTAssertEqual("ProjectActivitySuccessCell", self.dataSource.reusableId(item: 1, section: section)) - XCTAssertEqual("ProjectActivityDateCell", self.dataSource.reusableId(item: 2, section: section)) - XCTAssertEqual("ProjectActivityBackingCell", self.dataSource.reusableId(item: 3, section: section)) - XCTAssertEqual("ProjectActivityCommentCell", self.dataSource.reusableId(item: 4, section: section)) - XCTAssertEqual("ProjectActivityDateCell", self.dataSource.reusableId(item: 5, section: section)) - XCTAssertEqual("ProjectActivityLaunchCell", self.dataSource.reusableId(item: 6, section: section)) - } - } - - func testGroupedDatesIsFalse() { - let timeZone = TimeZone(abbreviation: "UTC")! - var calendar = Calendar(identifier: .gregorian) - calendar.timeZone = timeZone - - withEnvironment(calendar: calendar) { - let section = ProjectActivitiesDataSource.Section.activities.rawValue - let project = Project.template - let activities = [ - Activity.template - |> Activity.lens.category .~ Activity.Category.backing - |> Activity.lens.createdAt .~ 1_474_606_800 // 2016-09-23T05:00:00Z - |> Activity.lens.project .~ project, - Activity.template - |> Activity.lens.category .~ Activity.Category.success - |> Activity.lens.createdAt .~ 1_474_605_000 // 2016-09-23T04:30:00Z - |> Activity.lens.project .~ project - ] - - self.dataSource.load( - projectActivityData: - ProjectActivityData(activities: activities, project: project, groupedDates: false) - ) - - XCTAssertEqual(4, self.dataSource.tableView(tableView, numberOfRowsInSection: section)) - XCTAssertEqual("ProjectActivityDateCell", self.dataSource.reusableId(item: 0, section: section)) - XCTAssertEqual("ProjectActivityBackingCell", self.dataSource.reusableId(item: 1, section: section)) - XCTAssertEqual( - "ProjectActivityDateCell", self.dataSource.reusableId(item: 2, section: section), - "Should append second date cell, even though date is the same as the first date cell" - ) - XCTAssertEqual("ProjectActivitySuccessCell", self.dataSource.reusableId(item: 3, section: section)) - } - } - - func testEmptyState() { - let section = ProjectActivitiesDataSource.Section.emptyState.rawValue - self.dataSource.emptyState(visible: true) - - XCTAssertEqual(1, self.dataSource.tableView(self.tableView, numberOfRowsInSection: section)) - XCTAssertEqual("ProjectActivityEmptyStateCell", self.dataSource.reusableId(item: 0, section: section)) - } -} diff --git a/Kickstarter-iOS/Features/ProjectActivities/Storyboard/ProjectActivity.storyboard b/Kickstarter-iOS/Features/ProjectActivities/Storyboard/ProjectActivity.storyboard deleted file mode 100644 index c53ab833cb..0000000000 --- a/Kickstarter-iOS/Features/ProjectActivities/Storyboard/ProjectActivity.storyboard +++ /dev/null @@ -1,738 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivityBackingCell.swift b/Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivityBackingCell.swift deleted file mode 100644 index 86336f329c..0000000000 --- a/Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivityBackingCell.swift +++ /dev/null @@ -1,202 +0,0 @@ -import KsApi -import Library -import Prelude -import Prelude_UIKit -import UIKit - -internal protocol ProjectActivityBackingCellDelegate: AnyObject { - func projectActivityBackingCellGoToBacking(project: Project, backing: Backing) - func projectActivityBackingCellGoToSendMessage(project: Project, backing: Backing) -} - -internal final class ProjectActivityBackingCell: UITableViewCell, ValueCell { - fileprivate let viewModel: ProjectActivityBackingCellViewModelType = ProjectActivityBackingCellViewModel() - internal weak var delegate: ProjectActivityBackingCellDelegate? - - @IBOutlet fileprivate var backerImageView: CircleAvatarImageView! - @IBOutlet fileprivate var backingButton: UIButton! - @IBOutlet fileprivate var bulletSeparatorView: UIView! - @IBOutlet fileprivate var cardView: UIView! - @IBOutlet fileprivate var footerDividerView: UIView! - @IBOutlet fileprivate var footerStackView: UIStackView! - @IBOutlet fileprivate var headerDividerView: UIView! - @IBOutlet fileprivate var headerStackView: UIStackView! - @IBOutlet fileprivate var pledgeAmountLabel: UILabel! - @IBOutlet fileprivate var pledgeAmountLabelsStackView: UIStackView! - @IBOutlet fileprivate var pledgeAmountsStackView: UIView! - @IBOutlet fileprivate var pledgeDetailsSeparatorView: UIView! - @IBOutlet fileprivate var pledgeDetailsStackView: UIStackView! - @IBOutlet fileprivate var previousPledgeAmountLabel: UILabel! - @IBOutlet fileprivate var previousPledgeStrikethroughView: UIView! - @IBOutlet fileprivate var rewardLabel: UILabel! - @IBOutlet fileprivate var sendMessageButton: UIButton! - @IBOutlet fileprivate var titleLabel: UILabel! - - internal override func awakeFromNib() { - super.awakeFromNib() - - _ = self.backingButton - |> UIButton.lens.targets .~ [(self, #selector(self.backingButtonPressed), .touchUpInside)] - - _ = self.sendMessageButton - |> UIButton.lens.targets .~ [(self, #selector(self.sendMessageButtonPressed), .touchUpInside)] - } - - internal func configureWith(value activityAndProject: (Activity, Project)) { - self.viewModel.inputs.configureWith( - activity: activityAndProject.0, - project: activityAndProject.1 - ) - } - - internal override func bindViewModel() { - super.bindViewModel() - - self.rac.accessibilityLabel = self.viewModel.outputs.cellAccessibilityLabel - self.rac.accessibilityValue = self.viewModel.outputs.cellAccessibilityValue - - self.viewModel.outputs.backerImageURL - .observeForUI() - .on(event: { [weak self] _ in - self?.backerImageView.af.cancelImageRequest() - self?.backerImageView.image = nil - }) - .skipNil() - .observeValues { [weak self] url in - self?.backerImageView.af.setImage(withURL: url) - } - - self.viewModel.outputs.notifyDelegateGoToBacking - .observeForUI() - .observeValues { [weak self] project, backing in - self?.delegate?.projectActivityBackingCellGoToBacking(project: project, backing: backing) - } - - self.viewModel.outputs.notifyDelegateGoToSendMessage - .observeForUI() - .observeValues { [weak self] project, backing in - self?.delegate?.projectActivityBackingCellGoToSendMessage(project: project, backing: backing) - } - - self.pledgeAmountLabel.rac.hidden = self.viewModel.outputs.pledgeAmountLabelIsHidden - - self.pledgeAmountLabel.rac.text = self.viewModel.outputs.pledgeAmount - - self.pledgeAmountsStackView.rac.hidden = self.viewModel.outputs.pledgeAmountsStackViewIsHidden - - self.pledgeDetailsSeparatorView.rac.hidden = - self.viewModel.outputs.pledgeDetailsSeparatorStackViewIsHidden - - self.previousPledgeAmountLabel.rac.hidden = self.viewModel.outputs.previousPledgeAmountLabelIsHidden - - self.previousPledgeAmountLabel.rac.text = self.viewModel.outputs.previousPledgeAmount - - self.rewardLabel.rac.hidden = self.viewModel.outputs.rewardLabelIsHidden - - self.sendMessageButton.rac.hidden = self.viewModel.outputs.sendMessageButtonAndBulletSeparatorHidden - - self.bulletSeparatorView.rac.hidden = self.viewModel.outputs.sendMessageButtonAndBulletSeparatorHidden - - self.viewModel.outputs.reward.observeForUI() - .observeValues { [weak rewardLabel] title in - guard let rewardLabel = rewardLabel else { return } - - rewardLabel.attributedText = title.simpleHtmlAttributedString( - font: .ksr_body(size: 12), - bold: UIFont.ksr_body(size: 12).bolded, - italic: nil - ) - - _ = rewardLabel - |> UILabel.lens.numberOfLines .~ 0 - |> UILabel.lens.textColor .~ .ksr_support_400 - } - - self.viewModel.outputs.title.observeForUI() - .observeValues { [weak titleLabel] title in - guard let titleLabel = titleLabel else { return } - - titleLabel.attributedText = title.simpleHtmlAttributedString( - base: [ - NSAttributedString.Key.font: UIFont.ksr_title3(size: 14), - NSAttributedString.Key.foregroundColor: UIColor.ksr_support_400 - ], - bold: [ - NSAttributedString.Key.font: UIFont.ksr_title3(size: 14), - NSAttributedString.Key.foregroundColor: UIColor.ksr_support_700 - ], - italic: nil - ) - ?? .init() - } - } - - internal override func bindStyles() { - super.bindStyles() - - _ = self - |> baseTableViewCellStyle() - |> ProjectActivityBackingCell.lens.contentView.layoutMargins %~~ { layoutMargins, cell in - cell.traitCollection.isRegularRegular - ? projectActivityRegularRegularLayoutMargins - : layoutMargins - } - |> UITableViewCell.lens.accessibilityHint %~ { _ in Strings.Opens_pledge_info() } - - _ = self.backerImageView - |> ignoresInvertColorsImageViewStyle - - _ = self.backingButton - |> projectActivityFooterButton - |> UIButton.lens.title(for: .normal) %~ { _ in Strings.dashboard_activity_pledge_info() } - - _ = self.bulletSeparatorView - |> projectActivityBulletSeparatorViewStyle - - _ = self.cardView - |> dropShadowStyleMedium() - - _ = self.footerDividerView - |> projectActivityDividerViewStyle - - _ = self.footerStackView - |> projectActivityFooterStackViewStyle - |> UIStackView.lens.layoutMargins .~ .init(all: Styles.grid(2)) - - _ = self.headerDividerView - |> projectActivityDividerViewStyle - - _ = self.headerStackView - |> projectActivityHeaderStackViewStyle - - _ = self.pledgeAmountLabel - |> UILabel.lens.textColor .~ .ksr_create_700 - |> UILabel.lens.font .~ .ksr_callout(size: 24) - - _ = self.pledgeAmountLabelsStackView - |> UIStackView.lens.spacing .~ Styles.grid(2) - - _ = self.pledgeDetailsStackView - |> UIStackView.lens.layoutMargins .~ .init(topBottom: Styles.grid(3), leftRight: Styles.grid(2)) - |> UIStackView.lens.isLayoutMarginsRelativeArrangement .~ true - - _ = self.previousPledgeAmountLabel - |> UILabel.lens.font .~ .ksr_callout(size: 24) - |> UILabel.lens.textColor .~ .ksr_support_400 - - _ = self.previousPledgeStrikethroughView - |> UIView.lens.backgroundColor .~ .ksr_support_400 - - _ = self.sendMessageButton - |> projectActivityFooterButton - |> UIButton.lens.title(for: .normal) %~ { _ in Strings.dashboard_activity_send_message() } - } - - @objc fileprivate func backingButtonPressed(_: UIButton) { - self.viewModel.inputs.backingButtonPressed() - } - - @objc fileprivate func sendMessageButtonPressed(_: UIButton) { - self.viewModel.inputs.sendMessageButtonPressed() - } -} diff --git a/Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivityCommentCell.swift b/Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivityCommentCell.swift deleted file mode 100644 index 5d5cae9fed..0000000000 --- a/Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivityCommentCell.swift +++ /dev/null @@ -1,166 +0,0 @@ -import KsApi -import Library -import Prelude -import Prelude_UIKit -import UIKit - -internal protocol ProjectActivityCommentCellDelegate: AnyObject { - func projectActivityCommentCellGoToBacking(project: Project, user: User) - func projectActivityCommentCellGoToSendReply(project: Project, update: Update?, comment: ActivityComment) -} - -internal final class ProjectActivityCommentCell: UITableViewCell, ValueCell { - fileprivate let viewModel: ProjectActivityCommentCellViewModelType = ProjectActivityCommentCellViewModel() - internal weak var delegate: ProjectActivityCommentCellDelegate? - - @IBOutlet fileprivate var authorImageView: UIImageView! - @IBOutlet fileprivate var backingButton: UIButton! - @IBOutlet fileprivate var bodyLabel: UILabel! - @IBOutlet fileprivate var bodyView: UIView! - @IBOutlet fileprivate var bulletSeparatorView: UIView! - @IBOutlet fileprivate var cardView: UIView! - @IBOutlet fileprivate var containerStackView: UIStackView! - @IBOutlet fileprivate var footerDividerView: UIView! - @IBOutlet fileprivate var footerStackView: UIStackView! - @IBOutlet fileprivate var headerDividerView: UIView! - @IBOutlet fileprivate var headerStackView: UIStackView! - @IBOutlet fileprivate var replyButton: UIButton! - @IBOutlet fileprivate var titleLabel: UILabel! - - internal override func awakeFromNib() { - super.awakeFromNib() - - _ = self.backingButton - |> UIButton.lens.targets .~ [(self, #selector(self.backingButtonPressed), .touchUpInside)] - - _ = self.replyButton - |> UIButton.lens.targets .~ [(self, #selector(self.replyButtonPressed), .touchUpInside)] - } - - internal func configureWith(value activityAndProject: (Activity, Project)) { - self.viewModel.inputs.configureWith( - activity: activityAndProject.0, - project: activityAndProject.1 - ) - } - - internal override func bindViewModel() { - super.bindViewModel() - - self.footerStackView.rac.hidden = self.viewModel.outputs.pledgeFooterIsHidden - - self.viewModel.outputs.authorImageURL - .observeForUI() - .on(event: { [weak self] _ in - self?.authorImageView.af.cancelImageRequest() - self?.authorImageView.image = nil - }) - .skipNil() - .observeValues { [weak self] url in - self?.authorImageView.ksr_setImageWithURL(url) - } - - self.rac.accessibilityLabel = self.viewModel.outputs.cellAccessibilityLabel - self.rac.accessibilityValue = self.viewModel.outputs.cellAccessibilityValue - - self.viewModel.outputs.notifyDelegateGoToBacking - .observeForUI() - .observeValues { [weak self] project, user in - self?.delegate?.projectActivityCommentCellGoToBacking(project: project, user: user) - } - - self.viewModel.outputs.notifyDelegateGoToSendReply - .observeForUI() - .observeValues { [weak self] project, update, comment in - self?.delegate?.projectActivityCommentCellGoToSendReply( - project: project, update: update, comment: comment - ) - } - - self.bodyLabel.rac.text = self.viewModel.outputs.body - - self.viewModel.outputs.title - .observeForUI() - .observeValues { [weak titleLabel] title in - guard let titleLabel = titleLabel else { return } - - titleLabel.attributedText = title.simpleHtmlAttributedString( - base: [ - NSAttributedString.Key.font: UIFont.ksr_title3(size: 14), - NSAttributedString.Key.foregroundColor: UIColor.ksr_support_400 - ], - bold: [ - NSAttributedString.Key.font: UIFont.ksr_title3(size: 14), - NSAttributedString.Key.foregroundColor: UIColor.ksr_support_700 - ], - italic: nil - ) - ?? .init() - } - } - - internal override func bindStyles() { - super.bindStyles() - - _ = self - |> baseTableViewCellStyle() - |> ProjectActivityCommentCell.lens.contentView.layoutMargins %~~ { layoutMargins, cell in - cell.traitCollection.isRegularRegular - ? projectActivityRegularRegularLayoutMargins - : layoutMargins - } - |> UITableViewCell.lens.accessibilityHint %~ { _ in Strings.Opens_comments() } - - _ = self.authorImageView - |> ignoresInvertColorsImageViewStyle - - _ = self.backingButton - |> projectActivityFooterButton - |> UIButton.lens.title(for: .normal) %~ { _ in Strings.dashboard_activity_pledge_info() } - - _ = self.bodyLabel - |> UILabel.lens.textColor .~ .ksr_support_400 - |> UILabel.lens.font %~~ { _, label in - label.traitCollection.isRegularRegular - ? UIFont.ksr_body() - : UIFont.ksr_body(size: 14) - } - - _ = self.bodyView - |> UIView.lens.layoutMargins .~ .init(topBottom: Styles.grid(3), leftRight: Styles.grid(2)) - - _ = self.bulletSeparatorView - |> projectActivityBulletSeparatorViewStyle - - _ = self.cardView - |> dropShadowStyleMedium() - - _ = self.footerDividerView - |> projectActivityDividerViewStyle - - _ = self.footerStackView - |> projectActivityFooterStackViewStyle - |> UIStackView.lens.layoutMargins .~ .init(all: Styles.grid(2)) - - _ = self.headerDividerView - |> projectActivityDividerViewStyle - - _ = self.headerStackView - |> projectActivityHeaderStackViewStyle - - _ = self.replyButton - |> projectActivityFooterButton - |> UIButton.lens.title(for: .normal) %~ { _ in Strings.dashboard_activity_reply() } - - _ = self.titleLabel - |> UILabel.lens.numberOfLines .~ 2 - } - - @objc fileprivate func backingButtonPressed(_: UIButton) { - self.viewModel.inputs.backingButtonPressed() - } - - @objc fileprivate func replyButtonPressed(_: UIButton) { - self.viewModel.inputs.replyButtonPressed() - } -} diff --git a/Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivityDateCell.swift b/Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivityDateCell.swift deleted file mode 100644 index 0ddfe467ce..0000000000 --- a/Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivityDateCell.swift +++ /dev/null @@ -1,38 +0,0 @@ -import KsApi -import Library -import Prelude -import Prelude_UIKit -import UIKit - -internal final class ProjectActivityDateCell: UITableViewCell, ValueCell { - @IBOutlet fileprivate var dateLabel: UILabel! - - internal func configureWith(value date: Date) { - self.dateLabel.text = Format.date( - secondsInUTC: date.timeIntervalSince1970, - dateStyle: .long, - timeStyle: .none - ) - } - - internal override func bindStyles() { - super.bindStyles() - - _ = self - |> baseTableViewCellStyle() - |> ProjectActivityDateCell.lens.contentView.layoutMargins %~~ { _, cell in - cell.traitCollection.isRegularRegular - ? .init( - top: Styles.grid(4), - left: projectActivityRegularRegularLeftRight, - bottom: 0, - right: projectActivityRegularRegularLeftRight - ) - : .init(top: Styles.grid(4), left: Styles.grid(2), bottom: Styles.grid(1), right: Styles.grid(2)) - } - - _ = self.dateLabel - |> UILabel.lens.font .~ .ksr_headline(size: 13) - |> UILabel.lens.textColor .~ .ksr_support_400 - } -} diff --git a/Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivityEmptyStateCell.swift b/Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivityEmptyStateCell.swift deleted file mode 100644 index fd7938d2de..0000000000 --- a/Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivityEmptyStateCell.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Library -import UIKit - -internal final class ProjectActivityEmptyStateCell: UITableViewCell, ValueCell { - internal func configureWith(value _: Void) {} -} diff --git a/Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivityLaunchCell.swift b/Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivityLaunchCell.swift deleted file mode 100644 index 36ffef3890..0000000000 --- a/Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivityLaunchCell.swift +++ /dev/null @@ -1,56 +0,0 @@ -import KsApi -import Library -import Prelude -import Prelude_UIKit -import UIKit - -internal final class ProjectActivityLaunchCell: UITableViewCell, ValueCell { - fileprivate let viewModel: ProjectActivityLaunchCellViewModelType = ProjectActivityLaunchCellViewModel() - - @IBOutlet fileprivate var cardView: UIView! - @IBOutlet fileprivate var titleLabel: UILabel! - - internal func configureWith(value activityAndProject: (Activity, Project)) { - self.viewModel.inputs.configureWith( - activity: activityAndProject.0, - project: activityAndProject.1 - ) - } - - internal override func bindViewModel() { - super.bindViewModel() - - self.viewModel.outputs.title.observeForUI() - .observeValues { [weak titleLabel] title in - guard let titleLabel = titleLabel else { return } - - titleLabel.attributedText = title.simpleHtmlAttributedString( - font: .ksr_body(), - bold: UIFont.ksr_body().bolded, - italic: nil - ) - - _ = titleLabel - |> projectActivityStateChangeLabelStyle - |> UILabel.lens.textColor .~ .ksr_support_700 - } - } - - internal override func bindStyles() { - super.bindStyles() - - _ = self - |> baseTableViewCellStyle() - |> ProjectActivityLaunchCell.lens.contentView.layoutMargins %~~ { layoutMargins, cell in - cell.traitCollection.isRegularRegular - ? projectActivityRegularRegularLayoutMargins - : layoutMargins - } - |> UITableViewCell.lens.accessibilityHint %~ { _ in Strings.Opens_project() } - - _ = self.cardView - |> cardStyle() - |> dropShadowStyleMedium() - |> UIView.lens.layer.borderColor .~ UIColor.ksr_support_700.cgColor - } -} diff --git a/Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivityNegativeStateChangeCell.swift b/Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivityNegativeStateChangeCell.swift deleted file mode 100644 index bc0f5989be..0000000000 --- a/Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivityNegativeStateChangeCell.swift +++ /dev/null @@ -1,57 +0,0 @@ -import KsApi -import Library -import Prelude -import Prelude_UIKit -import UIKit - -internal final class ProjectActivityNegativeStateChangeCell: UITableViewCell, ValueCell { - fileprivate let viewModel: ProjectActivityNegativeStateChangeCellViewModelType = - ProjectActivityNegativeStateChangeCellViewModel() - - @IBOutlet fileprivate var cardView: UIView! - @IBOutlet fileprivate var titleLabel: UILabel! - - internal func configureWith(value activityAndProject: (Activity, Project)) { - self.viewModel.inputs.configureWith( - activity: activityAndProject.0, - project: activityAndProject.1 - ) - } - - internal override func bindViewModel() { - super.bindViewModel() - - self.viewModel.outputs.title.observeForUI() - .observeValues { [weak titleLabel] title in - guard let titleLabel = titleLabel else { return } - - titleLabel.attributedText = title.simpleHtmlAttributedString( - font: .ksr_body(), - bold: UIFont.ksr_body().bolded, - italic: nil - ) - - _ = titleLabel - |> projectActivityStateChangeLabelStyle - |> UILabel.lens.textColor .~ .ksr_support_400 - } - } - - internal override func bindStyles() { - super.bindStyles() - - _ = self - |> baseTableViewCellStyle() - |> ProjectActivityNegativeStateChangeCell.lens.contentView.layoutMargins %~~ { layoutMargins, cell in - cell.traitCollection.isRegularRegular - ? projectActivityRegularRegularLayoutMargins - : layoutMargins - } - |> UITableViewCell.lens.accessibilityHint %~ { _ in Strings.Opens_project() } - - _ = self.cardView - |> cardStyle() - |> dropShadowStyleMedium() - |> UIView.lens.layer.borderColor .~ UIColor.ksr_support_400.cgColor - } -} diff --git a/Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivitySuccessCell.swift b/Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivitySuccessCell.swift deleted file mode 100644 index 74c61bbb7c..0000000000 --- a/Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivitySuccessCell.swift +++ /dev/null @@ -1,75 +0,0 @@ -import KsApi -import Library -import Prelude -import Prelude_UIKit -import UIKit - -internal final class ProjectActivitySuccessCell: UITableViewCell, ValueCell { - fileprivate let viewModel: ProjectActivitySuccessCellViewModelType = ProjectActivitySuccessCellViewModel() - - @IBOutlet fileprivate var backgroundImageView: UIImageView! - @IBOutlet fileprivate var dropShadowView: UIView! - @IBOutlet fileprivate var cardView: UIView! - @IBOutlet fileprivate var titleLabel: UILabel! - - internal func configureWith(value activityAndProject: (Activity, Project)) { - self.viewModel.inputs.configureWith( - activity: activityAndProject.0, - project: activityAndProject.1 - ) - } - - internal override func bindViewModel() { - super.bindViewModel() - - self.viewModel.outputs.backgroundImageURL - .observeForUI() - .on(event: { [weak backgroundImageView] _ in - backgroundImageView?.af.cancelImageRequest() - backgroundImageView?.image = nil - }) - .skipNil() - .observeValues { [weak backgroundImageView] url in - backgroundImageView?.af.setImage(withURL: url) - } - - self.viewModel.outputs.title.observeForUI() - .observeValues { [weak titleLabel] title in - guard let titleLabel = titleLabel else { return } - - titleLabel.attributedText = title.simpleHtmlAttributedString( - font: .ksr_body(), - bold: UIFont.ksr_body().bolded, - italic: nil - ) - - _ = titleLabel - |> projectActivityStateChangeLabelStyle - |> UILabel.lens.textColor .~ .ksr_white - } - } - - internal override func bindStyles() { - super.bindStyles() - - _ = self - |> baseTableViewCellStyle() - |> ProjectActivitySuccessCell.lens.contentView.layoutMargins %~~ { layoutMargins, cell in - cell.traitCollection.isRegularRegular - ? projectActivityRegularRegularLayoutMargins - : layoutMargins - } - |> UITableViewCell.lens.accessibilityHint %~ { _ in - Strings.Opens_project() - } - - _ = self.cardView - |> roundedStyle() - |> UIView.lens.layoutMargins .~ .init(all: 24.0) - - _ = self.dropShadowView - |> roundedStyle() - |> UIView.lens.backgroundColor .~ .ksr_white - |> dropShadowStyleMedium() - } -} diff --git a/Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivityUpdateCell.swift b/Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivityUpdateCell.swift deleted file mode 100644 index 31e42cb099..0000000000 --- a/Kickstarter-iOS/Features/ProjectActivities/Views/Cells/ProjectActivityUpdateCell.swift +++ /dev/null @@ -1,122 +0,0 @@ -import KsApi -import Library -import Prelude -import Prelude_UIKit -import UIKit - -internal final class ProjectActivityUpdateCell: UITableViewCell, ValueCell { - fileprivate let viewModel: ProjectActivityUpdateCellViewModelType = ProjectActivityUpdateCellViewModel() - - @IBOutlet fileprivate var activityTitleLabel: UILabel! - @IBOutlet fileprivate var bodyLabel: UILabel! - @IBOutlet fileprivate var cardView: UIView! - @IBOutlet fileprivate var commentsCountImageView: UIImageView! - @IBOutlet fileprivate var commentsCountLabel: UILabel! - @IBOutlet fileprivate var commentsStackView: UIStackView! - @IBOutlet fileprivate var containerStackView: UIStackView! - @IBOutlet fileprivate var contentAndFooterStackView: UIStackView! - @IBOutlet fileprivate var footerDividerView: UIView! - @IBOutlet fileprivate var likeAndCommentsCountStackView: UIStackView! - @IBOutlet fileprivate var likesCountImageView: UIImageView! - @IBOutlet fileprivate var likesCountLabel: UILabel! - @IBOutlet fileprivate var likesStackView: UIStackView! - @IBOutlet fileprivate var updateTitleLabel: UILabel! - - internal func configureWith(value activityAndProject: (Activity, Project)) { - self.viewModel.inputs.configureWith( - activity: activityAndProject.0, - project: activityAndProject.1 - ) - } - - internal override func bindViewModel() { - super.bindViewModel() - - self.viewModel.outputs.activityTitle.observeForUI() - .observeValues { [weak activityTitleLabel] title in - guard let activityTitleLabel = activityTitleLabel else { return } - - activityTitleLabel.attributedText = title.simpleHtmlAttributedString( - font: .ksr_title3(size: 14), - bold: UIFont.ksr_title3(size: 14).bolded, - italic: nil - ) - - _ = activityTitleLabel - |> projectActivityTitleLabelStyle - } - - self.bodyLabel.rac.text = self.viewModel.outputs.body - self.rac.accessibilityLabel = self.viewModel.outputs.cellAccessibilityLabel - self.rac.accessibilityValue = self.viewModel.outputs.cellAccessibilityValue - self.commentsCountLabel.rac.text = self.viewModel.outputs.commentsCount - self.likesCountLabel.rac.text = self.viewModel.outputs.likesCount - self.updateTitleLabel.rac.text = self.viewModel.outputs.updateTitle - } - - internal override func bindStyles() { - super.bindStyles() - - let statLabel = - UILabel.lens.font .~ .ksr_caption1(size: 12) - <> UILabel.lens.textColor .~ .ksr_support_400 - - _ = self - |> baseTableViewCellStyle() - |> ProjectActivityUpdateCell.lens.contentView.layoutMargins %~~ { layoutMargins, cell in - cell.traitCollection.isRegularRegular - ? projectActivityRegularRegularLayoutMargins - : layoutMargins - } - |> UITableViewCell.lens.accessibilityHint %~ { _ in Strings.Opens_update() } - - _ = self.cardView - |> dropShadowStyleMedium() - - _ = self.bodyLabel - |> UILabel.lens.numberOfLines .~ 4 - |> UILabel.lens.textColor .~ .ksr_support_400 - |> UILabel.lens.font %~~ { _, label in - label.traitCollection.isRegularRegular - ? UIFont.ksr_body() - : UIFont.ksr_body(size: 14) - } - - _ = self.commentsCountImageView - |> UIImageView.lens.tintColor .~ .ksr_support_400 - - _ = self.commentsCountLabel - |> statLabel - - _ = self.commentsStackView - |> UIStackView.lens.spacing .~ Styles.grid(1) - - _ = self.containerStackView - |> UIStackView.lens.spacing .~ Styles.grid(4) - |> UIStackView.lens.layoutMargins .~ .init(topBottom: Styles.grid(3), leftRight: Styles.grid(2)) - |> UIStackView.lens.isLayoutMarginsRelativeArrangement .~ true - - _ = self.contentAndFooterStackView - |> UIStackView.lens.spacing .~ Styles.grid(3) - - _ = self.footerDividerView - |> projectActivityDividerViewStyle - - _ = self.likeAndCommentsCountStackView - |> UIStackView.lens.spacing .~ Styles.grid(3) - - _ = self.likesCountImageView - |> UIImageView.lens.tintColor .~ .ksr_support_400 - - _ = self.likesCountLabel - |> statLabel - - _ = self.likesStackView - |> UIStackView.lens.spacing .~ Styles.grid(1) - - _ = self.updateTitleLabel - |> UILabel.lens.font .~ .ksr_title1(size: 22) - |> UILabel.lens.numberOfLines .~ 0 - |> UILabel.lens.textColor .~ .ksr_support_700 - } -} diff --git a/Kickstarter-iOS/Features/ProjectPage/Controller/ProjectPageViewController.swift b/Kickstarter-iOS/Features/ProjectPage/Controller/ProjectPageViewController.swift index 6147adc21b..a313eb9e76 100644 --- a/Kickstarter-iOS/Features/ProjectPage/Controller/ProjectPageViewController.swift +++ b/Kickstarter-iOS/Features/ProjectPage/Controller/ProjectPageViewController.swift @@ -371,16 +371,6 @@ public final class ProjectPageViewController: UIViewController, MessageBannerVie self?.goToComments(project: $0) } - self.viewModel.outputs.goToDashboard - .observeForControllerAction() - .observeValues { [weak self] param in - guard featureCreatorDashboardEnabled() else { - return - } - - self?.goToDashboard(param: param) - } - self.viewModel.outputs.goToReportProject .observeForControllerAction() .observeValues { [weak self] flagged, projectID, projectUrl in @@ -674,18 +664,6 @@ public final class ProjectPageViewController: UIViewController, MessageBannerVie } } - private func goToDashboard(param: Param) { - self.view.window?.rootViewController - .flatMap { $0 as? RootTabBarViewController } - .doIfSome { root in - UIView.transition(with: root.view, duration: 0.3, options: [.transitionCrossDissolve], animations: { - root.switchToDashboard(project: param) - }, completion: { [weak self] _ in - self?.dismiss(animated: true, completion: nil) - }) - } - } - private func goToUpdates(project: Project) { let vc = ProjectUpdatesViewController.configuredWith(project: project) self.viewModel.inputs.showNavigationBar(false) diff --git a/Kickstarter-iOS/Features/RemoteConfigFeatureFlagTools/Controller/RemoteConfigFeatureFlagToolsViewControllerTests.swift b/Kickstarter-iOS/Features/RemoteConfigFeatureFlagTools/Controller/RemoteConfigFeatureFlagToolsViewControllerTests.swift index 97b357fa39..1898d17c58 100644 --- a/Kickstarter-iOS/Features/RemoteConfigFeatureFlagTools/Controller/RemoteConfigFeatureFlagToolsViewControllerTests.swift +++ b/Kickstarter-iOS/Features/RemoteConfigFeatureFlagTools/Controller/RemoteConfigFeatureFlagToolsViewControllerTests.swift @@ -25,9 +25,7 @@ final class RemoteConfigFeatureFlagToolsViewControllerTests: TestCase { RemoteConfigFeature.consentManagementDialogEnabled.rawValue: false, RemoteConfigFeature.facebookLoginInterstitialEnabled .rawValue: false, - RemoteConfigFeature.reportThisProjectEnabled.rawValue: false, - RemoteConfigFeature.creatorDashboardEnabled - .rawValue: false + RemoteConfigFeature.reportThisProjectEnabled.rawValue: false ] withEnvironment(language: .en, mainBundle: MockBundle(), remoteConfigClient: mockRemoteConfigClient) { diff --git a/Kickstarter-iOS/Features/RemoteConfigFeatureFlagTools/Controller/__Snapshots__/RemoteConfigFeatureFlagToolsViewControllerTests/testRemoteConfigFeatureFlagToolsViewController.1.png b/Kickstarter-iOS/Features/RemoteConfigFeatureFlagTools/Controller/__Snapshots__/RemoteConfigFeatureFlagToolsViewControllerTests/testRemoteConfigFeatureFlagToolsViewController.1.png index dd3a8c22bdd6b5a814ff39511da974b71d9064fb..e932abc085625cd31ce9469ec998e92b81465f1d 100644 GIT binary patch literal 50082 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 NMOi~B>&o57{|k1RFuVW& literal 56298 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 NyZ4}V_8u4dzX9G6T#Enz diff --git a/Kickstarter-iOS/Features/RootTabBar/RootTabBarViewController.swift b/Kickstarter-iOS/Features/RootTabBar/RootTabBarViewController.swift index 532d70d330..f7ed94f466 100644 --- a/Kickstarter-iOS/Features/RootTabBar/RootTabBarViewController.swift +++ b/Kickstarter-iOS/Features/RootTabBar/RootTabBarViewController.swift @@ -147,14 +147,6 @@ public final class RootTabBarViewController: UITabBarController, MessageBannerVi .skipNil() .observeValues { $0.filter(with: $1) } - self.viewModel.outputs.switchDashboardProject - .observeForControllerAction() - .map { [weak self] index, param -> (DashboardViewController, Param)? in - self?.viewControllerAndParam(with: index, param: param) - } - .skipNil() - .observeValues { $0.switch(toProject: $1) } - self.viewModel.outputs.setBadgeValueAtIndex .observeForUI() .observeValues { [weak self] value, index in @@ -166,14 +158,6 @@ public final class RootTabBarViewController: UITabBarController, MessageBannerVi self.viewModel.inputs.switchToActivities() } - public func switchToDashboard(project param: Param?) { - guard featureCreatorDashboardEnabled() else { - return - } - - self.viewModel.inputs.switchToDashboard(project: param) - } - public func switchToDiscovery(params: DiscoveryParams?) { self.viewModel.inputs.switchToDiscovery(params: params) } @@ -216,58 +200,18 @@ public final class RootTabBarViewController: UITabBarController, MessageBannerVi profileNav.setViewControllers([profileVC, threadsVC, messageThreadVC], animated: true) } - public func switchToCreatorMessageThread(projectId: Param, messageThread: MessageThread) { - guard featureCreatorDashboardEnabled() else { - return - } - - self.switchToDashboard(project: nil) - - guard - let dashboardNav = self.selectedViewController as? UINavigationController, - let dashboardVC = dashboardNav.viewControllers.first as? DashboardViewController - else { return } - - self.presentedViewController?.dismiss(animated: false, completion: nil) - - dashboardVC.navigateToProjectMessageThread(projectId: projectId, messageThread: messageThread) - } - - public func switchToProjectActivities(projectId: Param) { - guard featureCreatorDashboardEnabled() else { - return - } - - self.switchToDashboard(project: nil) - - guard - let dashboardNav = self.selectedViewController as? UINavigationController, - let dashboardVC = dashboardNav.viewControllers.first as? DashboardViewController - else { return } - - self.presentedViewController?.dismiss(animated: false, completion: nil) - - dashboardVC.navigateToProjectActivities(projectId: projectId) - } - fileprivate func setTabBarItemStyles(withData data: TabBarItemsData) { data.items.forEach { item in switch item { case let .home(index): - _ = tabBarItem(atIndex: index) ?|> homeTabBarItemStyle(isMember: data.isMember) + _ = tabBarItem(atIndex: index) ?|> homeTabBarItemStyle case let .activity(index): - _ = tabBarItem(atIndex: index) ?|> activityTabBarItemStyle(isMember: data.isMember) + _ = tabBarItem(atIndex: index) ?|> activityTabBarItemStyle case let .search(index): _ = tabBarItem(atIndex: index) ?|> searchTabBarItemStyle - case let .dashboard(index): - let style = self - .isTabBarItemLastItem(for: index) ? - profileTabBarItemStyle(isLoggedIn: data.isLoggedIn, isMember: data.isMember) : - dashboardTabBarItemStyle - _ = tabBarItem(atIndex: index) ?|> style case let .profile(avatarUrl, index): _ = tabBarItem(atIndex: index) - ?|> profileTabBarItemStyle(isLoggedIn: data.isLoggedIn, isMember: data.isMember) + ?|> profileTabBarItemStyle(isLoggedIn: data.isLoggedIn) setProfileImage(with: data, avatarUrl: avatarUrl, index: index) } @@ -288,7 +232,7 @@ public final class RootTabBarViewController: UITabBarController, MessageBannerVi if let imageData = try? Data(contentsOf: imageUrl) { let (defaultImage, selectedImage) = tabbarAvatarImageFromData(imageData) _ = self.tabBarItem(atIndex: index) - ?|> profileTabBarItemStyle(isLoggedIn: true, isMember: data.isMember) + ?|> profileTabBarItemStyle(isLoggedIn: true) ?|> UITabBarItem.lens.image .~ defaultImage ?|> UITabBarItem.lens.selectedImage .~ selectedImage } else { @@ -300,7 +244,7 @@ public final class RootTabBarViewController: UITabBarController, MessageBannerVi let (defaultImage, selectedImage) = tabbarAvatarImageFromData(avatarData) _ = self?.tabBarItem(atIndex: index) - ?|> profileTabBarItemStyle(isLoggedIn: true, isMember: data.isMember) + ?|> profileTabBarItemStyle(isLoggedIn: true) ?|> UITabBarItem.lens.image .~ defaultImage ?|> UITabBarItem.lens.selectedImage .~ selectedImage } @@ -333,8 +277,6 @@ public final class RootTabBarViewController: UITabBarController, MessageBannerVi return ActivitiesViewController.instantiate() case .search: return SearchViewController.instantiate() - case let .dashboard(isMember): - return isMember ? DashboardViewController.instantiate() : nil case let .profile(isLoggedIn): return isLoggedIn ? BackerDashboardViewController.instantiate() diff --git a/Kickstarter-iOS/Library/Storyboard.swift b/Kickstarter-iOS/Library/Storyboard.swift index 0ca01b64f3..787172b627 100644 --- a/Kickstarter-iOS/Library/Storyboard.swift +++ b/Kickstarter-iOS/Library/Storyboard.swift @@ -5,8 +5,6 @@ public enum Storyboard: String { case Backing case BackerDashboard case CommentsDialog - case Dashboard - case DashboardProjectsDrawer case DebugPushNotifications case Discovery case DiscoveryPage diff --git a/Kickstarter.xcodeproj/project.pbxproj b/Kickstarter.xcodeproj/project.pbxproj index 186eac1021..ed9d8e5523 100644 --- a/Kickstarter.xcodeproj/project.pbxproj +++ b/Kickstarter.xcodeproj/project.pbxproj @@ -33,36 +33,22 @@ 015706551E64BFC80087DD68 /* BackerDashboardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 015706541E64BFC80087DD68 /* BackerDashboardViewModel.swift */; }; 0157067D1E65F0420087DD68 /* BackerDashboardProjectsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0157067C1E65F0420087DD68 /* BackerDashboardProjectsViewController.swift */; }; 015706BB1E68DE580087DD68 /* ProfileSortBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 015706BA1E68DE580087DD68 /* ProfileSortBarView.swift */; }; - 015A06F41D219156007AE210 /* DashboardRewardRowStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 015A06F21D21914E007AE210 /* DashboardRewardRowStackView.swift */; }; - 015A06F71D219513007AE210 /* DashboardRewardsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 015A06F51D219513007AE210 /* DashboardRewardsCell.swift */; }; - 015A07461D247564007AE210 /* UpdateDraftViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 803BDF731D11AF7C004A785A /* UpdateDraftViewController.swift */; }; 0160E5101EFAD7B2004F61D6 /* MessagesEmptyStateCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0160E50E1EFAD7B2004F61D6 /* MessagesEmptyStateCell.swift */; }; 0169F8C11D6CA27500C8D5C5 /* RootCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0169F8C01D6CA27500C8D5C5 /* RootCategory.swift */; }; 0169F9841D6E0B2000C8D5C5 /* DiscoveryFiltersStaticRowCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0169F9831D6E0B2000C8D5C5 /* DiscoveryFiltersStaticRowCell.swift */; }; 0169F9861D6F4E1D00C8D5C5 /* DiscoveryFiltersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0169F9851D6F4E1D00C8D5C5 /* DiscoveryFiltersViewController.swift */; }; 0169F9881D6F51C400C8D5C5 /* DiscoveryFiltersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0169F9871D6F51C400C8D5C5 /* DiscoveryFiltersViewModel.swift */; }; - 0170E7701D25C55200E2CCE4 /* ProjectActivityCommentCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D2546F71D23101E0053844D /* ProjectActivityCommentCell.swift */; }; 017508161D67A4E300BB1863 /* DiscoveryNavigationHeaderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017508151D67A4E300BB1863 /* DiscoveryNavigationHeaderViewController.swift */; }; 0176E13B1C9742FD009CA092 /* UIBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0176E13A1C9742FD009CA092 /* UIBarButtonItem.swift */; }; - 018422BB1D2C483000CA7566 /* DashboardTitleViewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018422B81D2C482900CA7566 /* DashboardTitleViewViewModel.swift */; }; - 018422BD1D2C484200CA7566 /* DashboardProjectsDrawerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018422B61D2C47D600CA7566 /* DashboardProjectsDrawerCell.swift */; }; - 018422BE1D2C484700CA7566 /* DashboardTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018422841D2C47A400CA7566 /* DashboardTitleView.swift */; }; - 018422C01D2C486900CA7566 /* DashboardProjectsDrawerCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018422BF1D2C486900CA7566 /* DashboardProjectsDrawerCellViewModel.swift */; }; - 018422C51D2C48AA00CA7566 /* DashboardProjectsDrawerDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018422C31D2C48AA00CA7566 /* DashboardProjectsDrawerDataSource.swift */; }; 018F1F841C8E182200643DAA /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018F1F821C8E182200643DAA /* LoginViewController.swift */; }; 01940B261D42DC1A0074FCE3 /* HelpViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01940B251D42DC1A0074FCE3 /* HelpViewModel.swift */; }; 01940B291D467ECE0074FCE3 /* HelpWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01940B271D467EC60074FCE3 /* HelpWebViewController.swift */; }; 01940B2B1D46814E0074FCE3 /* HelpWebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01940B2A1D46814E0074FCE3 /* HelpWebViewModel.swift */; }; 01940B2E1D46A9AD0074FCE3 /* Help.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 01940B2D1D46A9AD0074FCE3 /* Help.storyboard */; }; - 0199545F1D2D818E00BC1390 /* DashboardProjectsDrawerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0199545E1D2D818E00BC1390 /* DashboardProjectsDrawerViewModel.swift */; }; 019DDFED1CB6FF4500BDC113 /* ResetPasswordViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 019DDFEB1CB6FF4500BDC113 /* ResetPasswordViewController.swift */; }; - 01A120D11D2D646300B42F73 /* DashboardProjectsDrawer.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 01A120D01D2D646300B42F73 /* DashboardProjectsDrawer.storyboard */; }; - 01A120D31D2D6E6200B42F73 /* DashboardProjectsDrawerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01A120D21D2D6E6200B42F73 /* DashboardProjectsDrawerViewController.swift */; }; 01A7A4C01C9690220036E553 /* UITextField+LocalizedPlaceholderKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01A7A4BF1C9690220036E553 /* UITextField+LocalizedPlaceholderKey.swift */; }; 01AFE30E1D5A97FB0094C263 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A73923B31D272242004524C3 /* Main.storyboard */; }; 01B3B0301E78890800B8BF46 /* BackerDashboardPagesDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B3B02F1E78890800B8BF46 /* BackerDashboardPagesDataSource.swift */; }; - 01C7CDB11D13462500D9E0D1 /* DashboardRewardRowStackViewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0156B5571D133BA0000C4252 /* DashboardRewardRowStackViewViewModel.swift */; }; - 01C7CDB31D13462A00D9E0D1 /* DashboardRewardsCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0156B5231D1327A1000C4252 /* DashboardRewardsCellViewModel.swift */; }; 01DEFB961CB44A5D003709C0 /* TwoFactorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01DEFB941CB44A5D003709C0 /* TwoFactorViewController.swift */; }; 01F219561DC12BF7005DD2E4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 01F219521DC12697005DD2E4 /* LaunchScreen.storyboard */; }; 01F547ED1D53994B000A98EF /* TabBarItemStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F547EC1D53994B000A98EF /* TabBarItemStyles.swift */; }; @@ -230,7 +216,6 @@ 191E60262A953E2B001413B2 /* ProjectTabCheckmarkListCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191E60242A953DC7001413B2 /* ProjectTabCheckmarkListCellViewModelTests.swift */; }; 19338D9B2A0D4E2300075F29 /* Stripe in Frameworks */ = {isa = PBXBuildFile; productRef = 19338D9A2A0D4E2300075F29 /* Stripe */; }; 1937A72328C9570A00DD732D /* ErroredBackingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1937A72228C9570900DD732D /* ErroredBackingView.swift */; }; - 1937A72628C959DD00DD732D /* FundingGraphViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED205B1E83240D00BFFA01 /* FundingGraphViewTests.swift */; }; 194154CE28D8ED69004648C8 /* CreatePaymentSourceSetupIntentInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 194154CD28D8ED69004648C8 /* CreatePaymentSourceSetupIntentInput.swift */; }; 194154D128D8FBAA004648C8 /* CreatePaymentSourceSetupIntentClientSecret+Constructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 194154CF28D8F2DE004648C8 /* CreatePaymentSourceSetupIntentClientSecret+Constructor.swift */; }; 194154D328D928C9004648C8 /* CreatePaymentSourceSetupIntentInputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 194154D228D928C9004648C8 /* CreatePaymentSourceSetupIntentInputTests.swift */; }; @@ -437,15 +422,8 @@ 47F4CA63267A7B2300356DBF /* RemoteConfigFeatureFlagToolsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47F4CA52267A7A8A00356DBF /* RemoteConfigFeatureFlagToolsViewController.swift */; }; 47F85271272206840050DB39 /* ProjectFAQsCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47F85270272206840050DB39 /* ProjectFAQsCellViewModelTests.swift */; }; 47F95ED72672C594001365B2 /* ViewRepliesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47F95ECF2672C587001365B2 /* ViewRepliesView.swift */; }; - 59019FB61D21A47700EAEC9D /* DashboardReferrerRowStackViewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59019FB51D21A47700EAEC9D /* DashboardReferrerRowStackViewViewModel.swift */; }; - 59019FBA1D21ABD200EAEC9D /* DashboardReferrerRowStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59019FB81D21ABD200EAEC9D /* DashboardReferrerRowStackView.swift */; }; 59392BEC1D7094B0001C99A4 /* ProjectUpdatesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59392BEB1D7094B0001C99A4 /* ProjectUpdatesViewController.swift */; }; 59392C1E1D7095B3001C99A4 /* ProjectUpdatesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59392C1D1D7095B3001C99A4 /* ProjectUpdatesViewModel.swift */; }; - 593AC5CF1D33F4BF002613F4 /* DashboardFundingCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 593AC5CE1D33F4BF002613F4 /* DashboardFundingCell.swift */; }; - 593AC6011D33F517002613F4 /* DashboardFundingCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 593AC6001D33F517002613F4 /* DashboardFundingCellViewModel.swift */; }; - 5955E64F1D21800300B4153D /* DashboardReferrersCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5955E64D1D21800300B4153D /* DashboardReferrersCell.swift */; }; - 5955E6811D21805200B4153D /* DashboardReferrersCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5955E6801D21805200B4153D /* DashboardReferrersCellViewModel.swift */; }; - 595CDAB81D3537180051C816 /* FundingGraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 595CDAB71D3537180051C816 /* FundingGraphView.swift */; }; 595F82641D679346008B8C56 /* DiscoveryPostcardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 595F82631D679346008B8C56 /* DiscoveryPostcardViewModel.swift */; }; 59673C8B1D50EC920035AFD9 /* Video.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 59673C8A1D50EC920035AFD9 /* Video.storyboard */; }; 59673CBD1D50ED380035AFD9 /* VideoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59673CBC1D50ED380035AFD9 /* VideoViewController.swift */; }; @@ -465,13 +443,7 @@ 598D96CB1D42AE85003F3F66 /* ActivitySampleProjectCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 598D96CA1D42AE85003F3F66 /* ActivitySampleProjectCellViewModel.swift */; }; 59AE35B01D67631B00A310E6 /* DiscoveryPage.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 59AE35AF1D67631B00A310E6 /* DiscoveryPage.storyboard */; }; 59AE35E21D67643100A310E6 /* DiscoveryPostcardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59AE35E11D67643100A310E6 /* DiscoveryPostcardCell.swift */; }; - 59B0DFC51D11AC850081D2DC /* DashboardDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B0DFC31D11AC850081D2DC /* DashboardDataSource.swift */; }; - 59B0DFFE1D11B2E50081D2DC /* DashboardContextCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B0DFFC1D11B2E50081D2DC /* DashboardContextCell.swift */; }; - 59B0E0041D1203970081D2DC /* DashboardActionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B0E0021D1203970081D2DC /* DashboardActionCell.swift */; }; - 59B0E0061D1207070081D2DC /* DashboardActionCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B0E0051D1207070081D2DC /* DashboardActionCellViewModel.swift */; }; 59B0E07E1D147F340081D2DC /* DashboardStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B0E07D1D147F340081D2DC /* DashboardStyles.swift */; }; - 59D1E6261D1865AC00896A4C /* DashboardVideoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59D1E6241D1865AC00896A4C /* DashboardVideoCell.swift */; }; - 59D1E6581D1866F800896A4C /* DashboardVideoCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59D1E6571D1866F800896A4C /* DashboardVideoCellViewModel.swift */; }; 59E877381DC9419700BCD1F7 /* Newsletter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59E877371DC9419700BCD1F7 /* Newsletter.swift */; }; 6008632C29B8F64800B87B39 /* UserEmailFragment.graphql in Resources */ = {isa = PBXBuildFile; fileRef = 6008632B29B8F64700B87B39 /* UserEmailFragment.graphql */; }; 6008632E29B8F66F00B87B39 /* FetchUserEmail.graphql in Resources */ = {isa = PBXBuildFile; fileRef = 6008632D29B8F66F00B87B39 /* FetchUserEmail.graphql */; }; @@ -520,7 +492,6 @@ 60DA50FE28C38DDB002E2DF1 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = 60DA50FD28C38DDB002E2DF1 /* AlamofireImage */; }; 60DA510F28C7E04B002E2DF1 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 60DA510E28C7E04B002E2DF1 /* Kingfisher */; }; 60DA511428C96A65002E2DF1 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 60DA511328C96A65002E2DF1 /* SwiftSoup */; }; - 60DF50982A434E75002C771F /* DashboardDeprecationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60DF50962A434E6B002C771F /* DashboardDeprecationView.swift */; }; 60EAD1C728D25A36009F9474 /* AppCenterDistribute in Frameworks */ = {isa = PBXBuildFile; productRef = 60EAD1C628D25A36009F9474 /* AppCenterDistribute */; }; 701160D4291ECB9F0095BF24 /* LoadingBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 701160D2291ECB250095BF24 /* LoadingBarButtonItem.swift */; }; 70495690299D53ED00B273DF /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 7049568F299D53ED00B273DF /* SnapshotTesting */; }; @@ -656,8 +627,6 @@ 8001D4C91D415692009E6667 /* UpdateDraftStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8001D4971D41568C009E6667 /* UpdateDraftStyles.swift */; }; 8016BFE81D0F582D00067956 /* String+Whitespace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8016BFE71D0F582D00067956 /* String+Whitespace.swift */; }; 8053D3111D3848A3007B85DB /* Reachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8053D3101D3848A3007B85DB /* Reachability.swift */; }; - 8072F41D1D46B75200999EF1 /* UpdatePreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8072F41C1D46B75200999EF1 /* UpdatePreviewViewController.swift */; }; - 8072F44F1D46BAA400999EF1 /* UpdatePreviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8072F44E1D46BAA400999EF1 /* UpdatePreviewViewModel.swift */; }; 809F8B661D08B4FF005BADD9 /* UpdateDraftViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 809F8B651D08B4FF005BADD9 /* UpdateDraftViewModel.swift */; }; 80D73AF61D50F1A60099231F /* Navigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80E26A121D500C6A007B3022 /* Navigation.swift */; }; 80E8EAC81D3EC65A007BDA4B /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80E8EAC71D3EC65A007BDA4B /* Image.swift */; }; @@ -862,26 +831,11 @@ 94F4A95A26125C8C000C21F9 /* TimeInterval+ISO8601Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94F4A95926125C8C000C21F9 /* TimeInterval+ISO8601Date.swift */; }; 94F4A96926125EE8000C21F9 /* TimeInterval+ISO8601DateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94F4A96126125EA0000C21F9 /* TimeInterval+ISO8601DateTests.swift */; }; 9D10B91B1D35407C008B8045 /* String+Truncate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D10B91A1D35407C008B8045 /* String+Truncate.swift */; }; - 9D14FF8D1D133351005F4ABB /* ProjectActivityBackingCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9F57CB1D131AF200CE81DE /* ProjectActivityBackingCell.swift */; }; - 9D14FF8E1D133351005F4ABB /* ProjectActivityEmptyStateCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9F57CC1D131AF200CE81DE /* ProjectActivityEmptyStateCell.swift */; }; - 9D14FF8F1D133351005F4ABB /* ProjectActivityLaunchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9F57CD1D131AF200CE81DE /* ProjectActivityLaunchCell.swift */; }; - 9D14FF901D133351005F4ABB /* ProjectActivityNegativeStateChangeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9F57CE1D131AF200CE81DE /* ProjectActivityNegativeStateChangeCell.swift */; }; - 9D14FF921D133351005F4ABB /* ProjectActivitySuccessCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9F57D01D131AF200CE81DE /* ProjectActivitySuccessCell.swift */; }; - 9D14FF931D133351005F4ABB /* ProjectActivityUpdateCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9F57D11D131AF200CE81DE /* ProjectActivityUpdateCell.swift */; }; - 9D14FFC61D135C12005F4ABB /* ProjectActivityUpdateCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D14FFC51D135C12005F4ABB /* ProjectActivityUpdateCellViewModel.swift */; }; - 9D25472D1D23135F0053844D /* ProjectActivityCommentCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D25472C1D23135F0053844D /* ProjectActivityCommentCellViewModel.swift */; }; - 9D2F4BE01D1AE02700B7C554 /* ProjectActivitySuccessCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D2F4BDF1D1AE02700B7C554 /* ProjectActivitySuccessCellViewModel.swift */; }; 9D50E9471D2EDBE50096DAEC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A7D1F9501C850B7C000D41D5 /* Assets.xcassets */; }; - 9D525F101D4158BA003CAE04 /* ProjectActivityDateCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D525F0E1D4158AC003CAE04 /* ProjectActivityDateCell.swift */; }; 9D7536CD1D78D78600A7623B /* SurveyResponseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7536CC1D78D78600A7623B /* SurveyResponseViewController.swift */; }; 9D7536CF1D78D88D00A7623B /* SurveyResponseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7536CE1D78D88D00A7623B /* SurveyResponseViewModel.swift */; }; - 9D8772131D19E84E003A4E96 /* ProjectActivityLaunchCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D8772121D19E84E003A4E96 /* ProjectActivityLaunchCellViewModel.swift */; }; - 9D9F58191D13243900CE81DE /* ProjectActivitiesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9F580F1D131B4000CE81DE /* ProjectActivitiesViewModel.swift */; }; - 9D9F581B1D1324E200CE81DE /* DashboardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9F580E1D131B4000CE81DE /* DashboardViewModel.swift */; }; - 9DC204B71D1B46BD003C1636 /* ProjectActivityNegativeStateChangeCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DC204B61D1B46BD003C1636 /* ProjectActivityNegativeStateChangeCellViewModel.swift */; }; 9DC572E51D36CA9800AE209C /* ProjectActivityStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DC572E41D36CA9800AE209C /* ProjectActivityStyles.swift */; }; 9DD1E3881D50035E00D4829E /* ProjectActivityData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD1E3871D50035E00D4829E /* ProjectActivityData.swift */; }; - 9DEE3B561D1D819D0020C2BE /* ProjectActivityBackingCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEE3B241D1D81950020C2BE /* ProjectActivityBackingCellViewModel.swift */; }; A707BAD81CFFAB9400653B2F /* LoginIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A707BAD41CFFAB9400653B2F /* LoginIntent.swift */; }; A707BADA1CFFAB9400653B2F /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = A707BAD51CFFAB9400653B2F /* Notifications.swift */; }; A709697E1D143D1300DB39D3 /* AlertError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A709697D1D143D1300DB39D3 /* AlertError.swift */; }; @@ -917,22 +871,18 @@ A73379601D0EDFEE00C91445 /* UIViewController-Preparation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A733795F1D0EDFEE00C91445 /* UIViewController-Preparation.swift */; }; A734A2671D21A1790080BBD5 /* WKNavigationActionData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A734A2661D21A1790080BBD5 /* WKNavigationActionData.swift */; }; A73923BC1D272242004524C3 /* Activity.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A73923AB1D272242004524C3 /* Activity.storyboard */; }; - A73923BF1D272242004524C3 /* Dashboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A73923AE1D272242004524C3 /* Dashboard.storyboard */; }; A73923C01D272242004524C3 /* Discovery.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A73923AF1D272242004524C3 /* Discovery.storyboard */; }; A73923C11D272242004524C3 /* Friends.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A73923B01D272242004524C3 /* Friends.storyboard */; }; A73923C31D272242004524C3 /* Login.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A73923B21D272242004524C3 /* Login.storyboard */; }; A73923C51D272242004524C3 /* Messages.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A73923B41D272242004524C3 /* Messages.storyboard */; }; - A73923C81D272242004524C3 /* ProjectActivity.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A73923B71D272242004524C3 /* ProjectActivity.storyboard */; }; A73923C91D272242004524C3 /* Search.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A73923B81D272242004524C3 /* Search.storyboard */; }; A73923CB1D272242004524C3 /* Update.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A73923BA1D272242004524C3 /* Update.storyboard */; }; - A73923CC1D272242004524C3 /* UpdateDraft.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A73923BB1D272242004524C3 /* UpdateDraft.storyboard */; }; A73924001D27230B004524C3 /* Kickstarter_Framework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7C7959E1C873A870081977F /* Kickstarter_Framework.framework */; }; A73924011D272312004524C3 /* Kickstarter_Framework.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A7C7959E1C873A870081977F /* Kickstarter_Framework.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; A73924041D272404004524C3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D1F9471C850B7C000D41D5 /* AppDelegate.swift */; }; A73924051D27247E004524C3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A73923B31D272242004524C3 /* Main.storyboard */; }; A74382051D3458C900040A95 /* PaddingCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74382041D3458C900040A95 /* PaddingCell.swift */; }; A745D0221CA897FF00C12802 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A745D0201CA897FF00C12802 /* SearchViewController.swift */; }; - A745D0481CA8985B00C12802 /* DashboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A745D0461CA8985B00C12802 /* DashboardViewController.swift */; }; A745D1411CAAB48F00C12802 /* LoginToutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F761761C85FACB005405ED /* LoginToutViewController.swift */; }; A74900181D00E23000BC3BE7 /* SignupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74900171D00E23000BC3BE7 /* SignupViewModel.swift */; }; A749001F1D00E27100BC3BE7 /* SignupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A749001D1D00E27100BC3BE7 /* SignupViewController.swift */; }; @@ -959,8 +909,6 @@ A75798911D6A201F0063CEEC /* DebugPushNotificationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A75798901D6A201F0063CEEC /* DebugPushNotificationsViewController.swift */; }; A75798C31D6A24CD0063CEEC /* DebugPushNotifications.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A75798C21D6A24CD0063CEEC /* DebugPushNotifications.storyboard */; }; A757E9EF1D19C37F00A5C978 /* ActivitySurveyResponseCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A757E9BD1D19C34600A5C978 /* ActivitySurveyResponseCell.swift */; }; - A757EABD1D19FAEE00A5C978 /* ProjectActivitiesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9F580C1D131B1200CE81DE /* ProjectActivitiesViewController.swift */; }; - A757EAEE1D19FAFA00A5C978 /* ProjectActivitiesDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9F58141D131D4A00CE81DE /* ProjectActivitiesDataSource.swift */; }; A757EAF01D1ABE7400A5C978 /* ActivitySurveyResponseCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A757EAEF1D1ABE7400A5C978 /* ActivitySurveyResponseCellViewModel.swift */; }; A757EB2B1D1AD89E00A5C978 /* DiscoveryStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = A757EB2A1D1AD89E00A5C978 /* DiscoveryStyles.swift */; }; A757EBB31D1B084F00A5C978 /* DiscoveryOnboardingCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A757EBB11D1B084F00A5C978 /* DiscoveryOnboardingCell.swift */; }; @@ -990,7 +938,6 @@ A773531F1D5E8AEF0017E239 /* MostPopularSearchProjectCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A773531E1D5E8AEF0017E239 /* MostPopularSearchProjectCell.swift */; }; A775B5211CA8705B00BBB587 /* RootTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A775B51F1CA8705B00BBB587 /* RootTabBarViewController.swift */; }; A77D7B071CBAAF5D0077586B /* Paginate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A77D7B061CBAAF5D0077586B /* Paginate.swift */; }; - A78012651D2EEA620027396E /* ReferralChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A78012641D2EEA620027396E /* ReferralChartView.swift */; }; A7808BBE1D6240B9001CF96A /* ProjectCreatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7808BBD1D6240B9001CF96A /* ProjectCreatorViewController.swift */; }; A7808BF01D625C6A001CF96A /* ProjectCreatorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7808BEF1D625C6A001CF96A /* ProjectCreatorViewModel.swift */; }; A78214741DB9326A00D0DD91 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = A7A5F8231D11ECF60036139A /* Localizable.strings */; }; @@ -1059,35 +1006,21 @@ A7ED1FAC1E831C5C00BFFA01 /* ActivitySampleFollowCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F501E831C5C00BFFA01 /* ActivitySampleFollowCellViewModelTests.swift */; }; A7ED1FAE1E831C5C00BFFA01 /* FindFriendsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F521E831C5C00BFFA01 /* FindFriendsViewModelTests.swift */; }; A7ED1FB11E831C5C00BFFA01 /* ActivitySampleProjectCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F551E831C5C00BFFA01 /* ActivitySampleProjectCellViewModelTests.swift */; }; - A7ED1FB21E831C5C00BFFA01 /* DashboardTitleViewViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F561E831C5C00BFFA01 /* DashboardTitleViewViewModelTests.swift */; }; A7ED1FB31E831C5C00BFFA01 /* ActivitySampleBackingCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F571E831C5C00BFFA01 /* ActivitySampleBackingCellViewModelTests.swift */; }; A7ED1FB41E831C5C00BFFA01 /* MessagesSearchViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F581E831C5C00BFFA01 /* MessagesSearchViewModelTests.swift */; }; A7ED1FB61E831C5C00BFFA01 /* MessageCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F5A1E831C5C00BFFA01 /* MessageCellViewModelTests.swift */; }; A7ED1FB81E831C5C00BFFA01 /* BackingCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F5C1E831C5C00BFFA01 /* BackingCellViewModelTests.swift */; }; - A7ED1FBA1E831C5C00BFFA01 /* ProjectActivityCommentCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F5E1E831C5C00BFFA01 /* ProjectActivityCommentCellViewModelTests.swift */; }; A7ED1FBC1E831C5C00BFFA01 /* FindFriendsFriendFollowCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F601E831C5C00BFFA01 /* FindFriendsFriendFollowCellViewModelTests.swift */; }; A7ED1FBE1E831C5C00BFFA01 /* ProjectNotificationCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F621E831C5C00BFFA01 /* ProjectNotificationCellViewModelTests.swift */; }; A7ED1FBF1E831C5C00BFFA01 /* SortPagerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F631E831C5C00BFFA01 /* SortPagerViewModelTests.swift */; }; - A7ED1FC41E831C5C00BFFA01 /* ProjectActivitySuccessCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F681E831C5C00BFFA01 /* ProjectActivitySuccessCellViewModelTests.swift */; }; A7ED1FC61E831C5C00BFFA01 /* SurveyResponseViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F6A1E831C5C00BFFA01 /* SurveyResponseViewModelTests.swift */; }; - A7ED1FC71E831C5C00BFFA01 /* DashboardFundingCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F6B1E831C5C00BFFA01 /* DashboardFundingCellViewModelTests.swift */; }; A7ED1FC81E831C5C00BFFA01 /* MessageThreadsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F6C1E831C5C00BFFA01 /* MessageThreadsViewModelTests.swift */; }; A7ED1FC91E831C5C00BFFA01 /* ActivitiesViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F6D1E831C5C00BFFA01 /* ActivitiesViewModelTests.swift */; }; - A7ED1FCA1E831C5C00BFFA01 /* DashboardProjectsDrawerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F6E1E831C5C00BFFA01 /* DashboardProjectsDrawerViewModelTests.swift */; }; A7ED1FCB1E831C5C00BFFA01 /* ActivityFriendBackingViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F6F1E831C5C00BFFA01 /* ActivityFriendBackingViewModelTests.swift */; }; - A7ED1FCC1E831C5C00BFFA01 /* ProjectActivityBackingCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F701E831C5C00BFFA01 /* ProjectActivityBackingCellViewModelTests.swift */; }; - A7ED1FCD1E831C5C00BFFA01 /* ProjectActivityUpdateCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F711E831C5C00BFFA01 /* ProjectActivityUpdateCellViewModelTests.swift */; }; - A7ED1FCE1E831C5C00BFFA01 /* DashboardReferrerRowStackViewViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F721E831C5C00BFFA01 /* DashboardReferrerRowStackViewViewModelTests.swift */; }; - A7ED1FD01E831C5C00BFFA01 /* DashboardActionCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F741E831C5C00BFFA01 /* DashboardActionCellViewModelTests.swift */; }; A7ED1FD21E831C5C00BFFA01 /* LoginToutViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F761E831C5C00BFFA01 /* LoginToutViewModelTests.swift */; }; - A7ED1FD41E831C5C00BFFA01 /* DashboardRewardRowStackViewViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F781E831C5C00BFFA01 /* DashboardRewardRowStackViewViewModelTests.swift */; }; A7ED1FD51E831C5C00BFFA01 /* FindFriendsStatsCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F791E831C5C00BFFA01 /* FindFriendsStatsCellViewModelTests.swift */; }; A7ED1FD61E831C5C00BFFA01 /* ActivityUpdateViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F7A1E831C5C00BFFA01 /* ActivityUpdateViewModelTests.swift */; }; A7ED1FD71E831C5C00BFFA01 /* SearchEmptyStateCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F7B1E831C5C00BFFA01 /* SearchEmptyStateCellViewModelTests.swift */; }; - A7ED1FD81E831C5C00BFFA01 /* DashboardRewardsCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F7C1E831C5C00BFFA01 /* DashboardRewardsCellViewModelTests.swift */; }; - A7ED1FD91E831C5C00BFFA01 /* ProjectActivitiesViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F7D1E831C5C00BFFA01 /* ProjectActivitiesViewModelTests.swift */; }; - A7ED1FDB1E831C5C00BFFA01 /* DashboardViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F7F1E831C5C00BFFA01 /* DashboardViewModelTests.swift */; }; - A7ED1FDC1E831C5C00BFFA01 /* DashboardVideoCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F801E831C5C00BFFA01 /* DashboardVideoCellViewModelTests.swift */; }; A7ED1FDD1E831C5C00BFFA01 /* ShareViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F811E831C5C00BFFA01 /* ShareViewModelTests.swift */; }; A7ED1FDE1E831C5C00BFFA01 /* DiscoveryExpandableRowCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F821E831C5C00BFFA01 /* DiscoveryExpandableRowCellViewModelTests.swift */; }; A7ED1FDF1E831C5C00BFFA01 /* DiscoveryFiltersViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F831E831C5C00BFFA01 /* DiscoveryFiltersViewModelTests.swift */; }; @@ -1100,13 +1033,9 @@ A7ED1FE71E831C5C00BFFA01 /* FacebookConfirmationViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F8B1E831C5C00BFFA01 /* FacebookConfirmationViewModelTests.swift */; }; A7ED1FE81E831C5C00BFFA01 /* FindFriendsHeaderCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F8C1E831C5C00BFFA01 /* FindFriendsHeaderCellViewModelTests.swift */; }; A7ED1FEA1E831C5C00BFFA01 /* HelpViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F8E1E831C5C00BFFA01 /* HelpViewModelTests.swift */; }; - A7ED1FEB1E831C5C00BFFA01 /* DashboardReferrersCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F8F1E831C5C00BFFA01 /* DashboardReferrersCellViewModelTests.swift */; }; - A7ED1FED1E831C5C00BFFA01 /* DashboardProjectsDrawerCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F911E831C5C00BFFA01 /* DashboardProjectsDrawerCellViewModelTests.swift */; }; - A7ED1FEE1E831C5C00BFFA01 /* ProjectActivityNegativeStateChangeCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F921E831C5C00BFFA01 /* ProjectActivityNegativeStateChangeCellViewModelTests.swift */; }; A7ED1FEF1E831C5C00BFFA01 /* LoginViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F931E831C5C00BFFA01 /* LoginViewModelTests.swift */; }; A7ED1FF01E831C5C00BFFA01 /* MessagesViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F941E831C5C00BFFA01 /* MessagesViewModelTests.swift */; }; A7ED1FF11E831C5C00BFFA01 /* MessageThreadCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F951E831C5C00BFFA01 /* MessageThreadCellViewModelTests.swift */; }; - A7ED1FF21E831C5C00BFFA01 /* ProjectActivityLaunchCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F961E831C5C00BFFA01 /* ProjectActivityLaunchCellViewModelTests.swift */; }; A7ED1FF31E831C5C00BFFA01 /* ProjectCreatorViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F971E831C5C00BFFA01 /* ProjectCreatorViewModelTests.swift */; }; A7ED1FF41E831C5C00BFFA01 /* ProjectDescriptionViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F981E831C5C00BFFA01 /* ProjectDescriptionViewModelTests.swift */; }; A7ED1FF71E831C5C00BFFA01 /* ResetPasswordViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F9B1E831C5C00BFFA01 /* ResetPasswordViewModelTests.swift */; }; @@ -1122,13 +1051,10 @@ A7ED20031E831C5C00BFFA01 /* TwoFactorViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1FA71E831C5C00BFFA01 /* TwoFactorViewModelTests.swift */; }; A7ED20041E831C5C00BFFA01 /* UpdateDraftViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1FA81E831C5C00BFFA01 /* UpdateDraftViewModelTests.swift */; }; A7ED20111E83229E00BFFA01 /* ActivitiesDataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED20051E83229E00BFFA01 /* ActivitiesDataSourceTests.swift */; }; - A7ED20131E83229E00BFFA01 /* DashboardDataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED20071E83229E00BFFA01 /* DashboardDataSourceTests.swift */; }; - A7ED20141E83229E00BFFA01 /* DashboardProjectsDrawerDataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED20081E83229E00BFFA01 /* DashboardProjectsDrawerDataSourceTests.swift */; }; A7ED20151E83229E00BFFA01 /* DiscoveryFiltersDataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED20091E83229E00BFFA01 /* DiscoveryFiltersDataSourceTests.swift */; }; A7ED20161E83229E00BFFA01 /* DiscoveryPagesDataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED200A1E83229E00BFFA01 /* DiscoveryPagesDataSourceTests.swift */; }; A7ED20171E83229E00BFFA01 /* DiscoveryProjectsDataSourceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED200B1E83229E00BFFA01 /* DiscoveryProjectsDataSourceTest.swift */; }; A7ED20181E83229E00BFFA01 /* FindFriendsDataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED200C1E83229E00BFFA01 /* FindFriendsDataSourceTests.swift */; }; - A7ED201B1E83229E00BFFA01 /* ProjectActivitiesDataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED200F1E83229E00BFFA01 /* ProjectActivitiesDataSourceTests.swift */; }; A7ED201D1E8322EC00BFFA01 /* TestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F461E831BA200BFFA01 /* TestCase.swift */; }; A7ED201E1E83231C00BFFA01 /* MockBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F451E831BA200BFFA01 /* MockBundle.swift */; }; A7ED201F1E83232A00BFFA01 /* XCTestCase+AppEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F471E831BA200BFFA01 /* XCTestCase+AppEnvironment.swift */; }; @@ -1138,10 +1064,8 @@ A7ED20421E8323E900BFFA01 /* SortPagerViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED20271E8323E900BFFA01 /* SortPagerViewControllerTests.swift */; }; A7ED20431E8323E900BFFA01 /* EmptyStatesViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED20281E8323E900BFFA01 /* EmptyStatesViewControllerTests.swift */; }; A7ED20441E8323E900BFFA01 /* ResetPasswordViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED20291E8323E900BFFA01 /* ResetPasswordViewControllerTests.swift */; }; - A7ED20451E8323E900BFFA01 /* DashboardViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED202A1E8323E900BFFA01 /* DashboardViewControllerTests.swift */; }; A7ED20481E8323E900BFFA01 /* LoginViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED202D1E8323E900BFFA01 /* LoginViewControllerTests.swift */; }; A7ED204D1E8323E900BFFA01 /* FindFriendsViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED20321E8323E900BFFA01 /* FindFriendsViewControllerTests.swift */; }; - A7ED204E1E8323E900BFFA01 /* ProjectActivityViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED20331E8323E900BFFA01 /* ProjectActivityViewControllerTests.swift */; }; A7ED20521E8323E900BFFA01 /* FacebookConfirmationViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED20371E8323E900BFFA01 /* FacebookConfirmationViewControllerTests.swift */; }; A7ED20551E8323E900BFFA01 /* LoginToutViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED203A1E8323E900BFFA01 /* LoginToutViewControllerTests.swift */; }; A7ED20571E8323E900BFFA01 /* ActivitiesViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED203C1E8323E900BFFA01 /* ActivitiesViewControllerTests.swift */; }; @@ -1151,7 +1075,6 @@ A7ED20621E83256700BFFA01 /* AppDelegateViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED205D1E83256700BFFA01 /* AppDelegateViewModelTests.swift */; }; A7ED20631E83256700BFFA01 /* HelpWebViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED205E1E83256700BFFA01 /* HelpWebViewModelTests.swift */; }; A7ED20641E83256700BFFA01 /* RootViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED205F1E83256700BFFA01 /* RootViewModelTests.swift */; }; - A7ED20651E83256700BFFA01 /* UpdatePreviewViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED20601E83256700BFFA01 /* UpdatePreviewViewModelTests.swift */; }; A7ED20661E83256700BFFA01 /* UpdateViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED20611E83256700BFFA01 /* UpdateViewModelTests.swift */; }; A7F441AD1D005A9400FE6FC5 /* ActivitiesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F4418E1D005A9400FE6FC5 /* ActivitiesViewModel.swift */; }; A7F441AF1D005A9400FE6FC5 /* ActivityFriendBackingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F4418F1D005A9400FE6FC5 /* ActivityFriendBackingViewModel.swift */; }; @@ -1665,15 +1588,11 @@ 0151AE871C8F60370067F1BE /* UIColor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIColor.swift; sourceTree = ""; }; 015572721E79C4FF005FB8CC /* BackerDashboardProjectCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackerDashboardProjectCellViewModel.swift; sourceTree = ""; }; 0156B4181D10B419000C4252 /* UIAlertController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIAlertController.swift; sourceTree = ""; }; - 0156B5231D1327A1000C4252 /* DashboardRewardsCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardRewardsCellViewModel.swift; sourceTree = ""; }; - 0156B5571D133BA0000C4252 /* DashboardRewardRowStackViewViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardRewardRowStackViewViewModel.swift; sourceTree = ""; }; 015706141E649DEE0087DD68 /* BackerDashboardViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackerDashboardViewController.swift; sourceTree = ""; }; 015706511E64A6C70087DD68 /* BackerDashboard.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = BackerDashboard.storyboard; sourceTree = ""; }; 015706541E64BFC80087DD68 /* BackerDashboardViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackerDashboardViewModel.swift; sourceTree = ""; }; 0157067C1E65F0420087DD68 /* BackerDashboardProjectsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackerDashboardProjectsViewController.swift; sourceTree = ""; }; 015706BA1E68DE580087DD68 /* ProfileSortBarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfileSortBarView.swift; sourceTree = ""; }; - 015A06F21D21914E007AE210 /* DashboardRewardRowStackView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardRewardRowStackView.swift; sourceTree = ""; }; - 015A06F51D219513007AE210 /* DashboardRewardsCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardRewardsCell.swift; sourceTree = ""; }; 0160E50E1EFAD7B2004F61D6 /* MessagesEmptyStateCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesEmptyStateCell.swift; sourceTree = ""; }; 0169F8C01D6CA27500C8D5C5 /* RootCategory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootCategory.swift; sourceTree = ""; }; 0169F9831D6E0B2000C8D5C5 /* DiscoveryFiltersStaticRowCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscoveryFiltersStaticRowCell.swift; sourceTree = ""; }; @@ -1681,20 +1600,12 @@ 0169F9871D6F51C400C8D5C5 /* DiscoveryFiltersViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscoveryFiltersViewModel.swift; sourceTree = ""; }; 017508151D67A4E300BB1863 /* DiscoveryNavigationHeaderViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscoveryNavigationHeaderViewController.swift; sourceTree = ""; }; 0176E13A1C9742FD009CA092 /* UIBarButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIBarButtonItem.swift; sourceTree = ""; }; - 018422841D2C47A400CA7566 /* DashboardTitleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardTitleView.swift; sourceTree = ""; }; - 018422B61D2C47D600CA7566 /* DashboardProjectsDrawerCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardProjectsDrawerCell.swift; sourceTree = ""; }; - 018422B81D2C482900CA7566 /* DashboardTitleViewViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardTitleViewViewModel.swift; sourceTree = ""; }; - 018422BF1D2C486900CA7566 /* DashboardProjectsDrawerCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardProjectsDrawerCellViewModel.swift; sourceTree = ""; }; - 018422C31D2C48AA00CA7566 /* DashboardProjectsDrawerDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardProjectsDrawerDataSource.swift; sourceTree = ""; }; 018F1F821C8E182200643DAA /* LoginViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = LoginViewController.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 01940B251D42DC1A0074FCE3 /* HelpViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HelpViewModel.swift; sourceTree = ""; }; 01940B271D467EC60074FCE3 /* HelpWebViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HelpWebViewController.swift; sourceTree = ""; }; 01940B2A1D46814E0074FCE3 /* HelpWebViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HelpWebViewModel.swift; sourceTree = ""; }; 01940B2D1D46A9AD0074FCE3 /* Help.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Help.storyboard; sourceTree = ""; }; - 0199545E1D2D818E00BC1390 /* DashboardProjectsDrawerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardProjectsDrawerViewModel.swift; sourceTree = ""; }; 019DDFEB1CB6FF4500BDC113 /* ResetPasswordViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ResetPasswordViewController.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; - 01A120D01D2D646300B42F73 /* DashboardProjectsDrawer.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = DashboardProjectsDrawer.storyboard; sourceTree = ""; }; - 01A120D21D2D6E6200B42F73 /* DashboardProjectsDrawerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardProjectsDrawerViewController.swift; sourceTree = ""; }; 01A7A4BF1C9690220036E553 /* UITextField+LocalizedPlaceholderKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITextField+LocalizedPlaceholderKey.swift"; sourceTree = ""; }; 01B3B02F1E78890800B8BF46 /* BackerDashboardPagesDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackerDashboardPagesDataSource.swift; sourceTree = ""; }; 01DEFB941CB44A5D003709C0 /* TwoFactorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = TwoFactorViewController.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; @@ -2032,15 +1943,8 @@ 47F4CA57267A7AD100356DBF /* RemoteConfigFeatureFlagToolsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteConfigFeatureFlagToolsViewModel.swift; sourceTree = ""; }; 47F85270272206840050DB39 /* ProjectFAQsCellViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectFAQsCellViewModelTests.swift; sourceTree = ""; }; 47F95ECF2672C587001365B2 /* ViewRepliesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewRepliesView.swift; sourceTree = ""; }; - 59019FB51D21A47700EAEC9D /* DashboardReferrerRowStackViewViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardReferrerRowStackViewViewModel.swift; sourceTree = ""; }; - 59019FB81D21ABD200EAEC9D /* DashboardReferrerRowStackView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardReferrerRowStackView.swift; sourceTree = ""; }; 59392BEB1D7094B0001C99A4 /* ProjectUpdatesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectUpdatesViewController.swift; sourceTree = ""; }; 59392C1D1D7095B3001C99A4 /* ProjectUpdatesViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectUpdatesViewModel.swift; sourceTree = ""; }; - 593AC5CE1D33F4BF002613F4 /* DashboardFundingCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardFundingCell.swift; sourceTree = ""; }; - 593AC6001D33F517002613F4 /* DashboardFundingCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardFundingCellViewModel.swift; sourceTree = ""; }; - 5955E64D1D21800300B4153D /* DashboardReferrersCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardReferrersCell.swift; sourceTree = ""; }; - 5955E6801D21805200B4153D /* DashboardReferrersCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardReferrersCellViewModel.swift; sourceTree = ""; }; - 595CDAB71D3537180051C816 /* FundingGraphView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FundingGraphView.swift; sourceTree = ""; }; 595F82631D679346008B8C56 /* DiscoveryPostcardViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscoveryPostcardViewModel.swift; sourceTree = ""; }; 59673C8A1D50EC920035AFD9 /* Video.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Video.storyboard; sourceTree = ""; }; 59673CBC1D50ED380035AFD9 /* VideoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoViewController.swift; sourceTree = ""; }; @@ -2060,13 +1964,7 @@ 598D96CA1D42AE85003F3F66 /* ActivitySampleProjectCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivitySampleProjectCellViewModel.swift; sourceTree = ""; }; 59AE35AF1D67631B00A310E6 /* DiscoveryPage.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = DiscoveryPage.storyboard; sourceTree = ""; }; 59AE35E11D67643100A310E6 /* DiscoveryPostcardCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscoveryPostcardCell.swift; sourceTree = ""; }; - 59B0DFC31D11AC850081D2DC /* DashboardDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardDataSource.swift; sourceTree = ""; }; - 59B0DFFC1D11B2E50081D2DC /* DashboardContextCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardContextCell.swift; sourceTree = ""; }; - 59B0E0021D1203970081D2DC /* DashboardActionCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardActionCell.swift; sourceTree = ""; }; - 59B0E0051D1207070081D2DC /* DashboardActionCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardActionCellViewModel.swift; sourceTree = ""; }; 59B0E07D1D147F340081D2DC /* DashboardStyles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardStyles.swift; sourceTree = ""; }; - 59D1E6241D1865AC00896A4C /* DashboardVideoCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardVideoCell.swift; sourceTree = ""; }; - 59D1E6571D1866F800896A4C /* DashboardVideoCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardVideoCellViewModel.swift; sourceTree = ""; }; 59E877371DC9419700BCD1F7 /* Newsletter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Newsletter.swift; sourceTree = ""; }; 6008632B29B8F64700B87B39 /* UserEmailFragment.graphql */ = {isa = PBXFileReference; lastKnownFileType = text; path = UserEmailFragment.graphql; sourceTree = ""; }; 6008632D29B8F66F00B87B39 /* FetchUserEmail.graphql */ = {isa = PBXFileReference; lastKnownFileType = text; path = FetchUserEmail.graphql; sourceTree = ""; }; @@ -2109,7 +2007,6 @@ 60C996EF2AC20314006BE4F4 /* CreateFlaggingInputTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateFlaggingInputTests.swift; sourceTree = ""; }; 60DA50E928B68990002E2DF1 /* SetYourPasswordViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetYourPasswordViewModelTests.swift; sourceTree = ""; }; 60DA50EF28B69534002E2DF1 /* SetYourPasswordViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetYourPasswordViewControllerTests.swift; sourceTree = ""; }; - 60DF50962A434E6B002C771F /* DashboardDeprecationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardDeprecationView.swift; sourceTree = ""; }; 701160D2291ECB250095BF24 /* LoadingBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingBarButtonItem.swift; sourceTree = ""; }; 7061848829BE4C11008F9941 /* MessageBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBannerView.swift; sourceTree = ""; }; 7061848C29BE577B008F9941 /* MessageBannerViewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBannerViewViewModel.swift; sourceTree = ""; }; @@ -2241,10 +2138,7 @@ 8001D4971D41568C009E6667 /* UpdateDraftStyles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateDraftStyles.swift; sourceTree = ""; }; 8016BFE71D0F582D00067956 /* String+Whitespace.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Whitespace.swift"; sourceTree = ""; }; 802800571C88F64D00141235 /* Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Base.xcconfig; path = Configs/Base.xcconfig; sourceTree = ""; }; - 803BDF731D11AF7C004A785A /* UpdateDraftViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateDraftViewController.swift; sourceTree = ""; }; 8053D3101D3848A3007B85DB /* Reachability.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Reachability.swift; sourceTree = ""; }; - 8072F41C1D46B75200999EF1 /* UpdatePreviewViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdatePreviewViewController.swift; sourceTree = ""; }; - 8072F44E1D46BAA400999EF1 /* UpdatePreviewViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdatePreviewViewModel.swift; sourceTree = ""; }; 809F8B651D08B4FF005BADD9 /* UpdateDraftViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateDraftViewModel.swift; sourceTree = ""; }; 80AB97B61D6281D60051C9D1 /* Debug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Debug.entitlements; sourceTree = ""; }; 80E26A121D500C6A007B3022 /* Navigation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Navigation.swift; sourceTree = ""; }; @@ -2454,28 +2348,10 @@ 94F4A95926125C8C000C21F9 /* TimeInterval+ISO8601Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+ISO8601Date.swift"; sourceTree = ""; }; 94F4A96126125EA0000C21F9 /* TimeInterval+ISO8601DateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+ISO8601DateTests.swift"; sourceTree = ""; }; 9D10B91A1D35407C008B8045 /* String+Truncate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Truncate.swift"; sourceTree = ""; }; - 9D14FFC51D135C12005F4ABB /* ProjectActivityUpdateCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectActivityUpdateCellViewModel.swift; sourceTree = ""; }; - 9D2546F71D23101E0053844D /* ProjectActivityCommentCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectActivityCommentCell.swift; sourceTree = ""; }; - 9D25472C1D23135F0053844D /* ProjectActivityCommentCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectActivityCommentCellViewModel.swift; sourceTree = ""; }; - 9D2F4BDF1D1AE02700B7C554 /* ProjectActivitySuccessCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectActivitySuccessCellViewModel.swift; sourceTree = ""; }; - 9D525F0E1D4158AC003CAE04 /* ProjectActivityDateCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectActivityDateCell.swift; sourceTree = ""; }; 9D7536CC1D78D78600A7623B /* SurveyResponseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SurveyResponseViewController.swift; sourceTree = ""; }; 9D7536CE1D78D88D00A7623B /* SurveyResponseViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SurveyResponseViewModel.swift; sourceTree = ""; }; - 9D8772121D19E84E003A4E96 /* ProjectActivityLaunchCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectActivityLaunchCellViewModel.swift; sourceTree = ""; }; - 9D9F57CB1D131AF200CE81DE /* ProjectActivityBackingCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectActivityBackingCell.swift; sourceTree = ""; }; - 9D9F57CC1D131AF200CE81DE /* ProjectActivityEmptyStateCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectActivityEmptyStateCell.swift; sourceTree = ""; }; - 9D9F57CD1D131AF200CE81DE /* ProjectActivityLaunchCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectActivityLaunchCell.swift; sourceTree = ""; }; - 9D9F57CE1D131AF200CE81DE /* ProjectActivityNegativeStateChangeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectActivityNegativeStateChangeCell.swift; sourceTree = ""; }; - 9D9F57D01D131AF200CE81DE /* ProjectActivitySuccessCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectActivitySuccessCell.swift; sourceTree = ""; }; - 9D9F57D11D131AF200CE81DE /* ProjectActivityUpdateCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectActivityUpdateCell.swift; sourceTree = ""; }; - 9D9F580C1D131B1200CE81DE /* ProjectActivitiesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectActivitiesViewController.swift; sourceTree = ""; }; - 9D9F580E1D131B4000CE81DE /* DashboardViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardViewModel.swift; sourceTree = ""; }; - 9D9F580F1D131B4000CE81DE /* ProjectActivitiesViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectActivitiesViewModel.swift; sourceTree = ""; }; - 9D9F58141D131D4A00CE81DE /* ProjectActivitiesDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectActivitiesDataSource.swift; sourceTree = ""; }; - 9DC204B61D1B46BD003C1636 /* ProjectActivityNegativeStateChangeCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectActivityNegativeStateChangeCellViewModel.swift; sourceTree = ""; }; 9DC572E41D36CA9800AE209C /* ProjectActivityStyles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectActivityStyles.swift; sourceTree = ""; }; 9DD1E3871D50035E00D4829E /* ProjectActivityData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectActivityData.swift; sourceTree = ""; }; - 9DEE3B241D1D81950020C2BE /* ProjectActivityBackingCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectActivityBackingCellViewModel.swift; sourceTree = ""; }; A707BAD31CFFAB9400653B2F /* HelpType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HelpType.swift; sourceTree = ""; }; A707BAD41CFFAB9400653B2F /* LoginIntent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginIntent.swift; sourceTree = ""; }; A707BAD51CFFAB9400653B2F /* Notifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = ""; }; @@ -2516,19 +2392,15 @@ A734A2661D21A1790080BBD5 /* WKNavigationActionData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WKNavigationActionData.swift; sourceTree = ""; }; A73923AB1D272242004524C3 /* Activity.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Activity.storyboard; sourceTree = ""; }; A73923AC1D272242004524C3 /* Thanks.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Thanks.storyboard; sourceTree = ""; }; - A73923AE1D272242004524C3 /* Dashboard.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Dashboard.storyboard; sourceTree = ""; }; A73923AF1D272242004524C3 /* Discovery.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Discovery.storyboard; sourceTree = ""; }; A73923B01D272242004524C3 /* Friends.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Friends.storyboard; sourceTree = ""; }; A73923B21D272242004524C3 /* Login.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Login.storyboard; sourceTree = ""; }; A73923B31D272242004524C3 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; A73923B41D272242004524C3 /* Messages.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Messages.storyboard; sourceTree = ""; }; - A73923B71D272242004524C3 /* ProjectActivity.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = ProjectActivity.storyboard; sourceTree = ""; }; A73923B81D272242004524C3 /* Search.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Search.storyboard; sourceTree = ""; }; A73923BA1D272242004524C3 /* Update.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Update.storyboard; sourceTree = ""; }; - A73923BB1D272242004524C3 /* UpdateDraft.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = UpdateDraft.storyboard; sourceTree = ""; }; A74382041D3458C900040A95 /* PaddingCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaddingCell.swift; sourceTree = ""; }; A745D0201CA897FF00C12802 /* SearchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = ""; }; - A745D0461CA8985B00C12802 /* DashboardViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardViewController.swift; sourceTree = ""; }; A747A8B81D45893100AF199A /* ProjectPamphletContentDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectPamphletContentDataSource.swift; sourceTree = ""; }; A74900171D00E23000BC3BE7 /* SignupViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignupViewModel.swift; sourceTree = ""; }; A749001D1D00E27100BC3BE7 /* SignupViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignupViewController.swift; sourceTree = ""; }; @@ -2573,7 +2445,6 @@ A775B51F1CA8705B00BBB587 /* RootTabBarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootTabBarViewController.swift; sourceTree = ""; }; A775B5451CA871D700BBB587 /* RootViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = RootViewModel.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; A77D7B061CBAAF5D0077586B /* Paginate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Paginate.swift; sourceTree = ""; }; - A78012641D2EEA620027396E /* ReferralChartView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferralChartView.swift; sourceTree = ""; }; A7808BBD1D6240B9001CF96A /* ProjectCreatorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectCreatorViewController.swift; sourceTree = ""; }; A7808BEF1D625C6A001CF96A /* ProjectCreatorViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectCreatorViewModel.swift; sourceTree = ""; }; A78355B81E85BD2D0021DA5A /* ActivityFriendFollowCellViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityFriendFollowCellViewModelTests.swift; sourceTree = ""; }; @@ -2663,35 +2534,21 @@ A7ED1F501E831C5C00BFFA01 /* ActivitySampleFollowCellViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivitySampleFollowCellViewModelTests.swift; sourceTree = ""; }; A7ED1F521E831C5C00BFFA01 /* FindFriendsViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FindFriendsViewModelTests.swift; sourceTree = ""; }; A7ED1F551E831C5C00BFFA01 /* ActivitySampleProjectCellViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivitySampleProjectCellViewModelTests.swift; sourceTree = ""; }; - A7ED1F561E831C5C00BFFA01 /* DashboardTitleViewViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardTitleViewViewModelTests.swift; sourceTree = ""; }; A7ED1F571E831C5C00BFFA01 /* ActivitySampleBackingCellViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivitySampleBackingCellViewModelTests.swift; sourceTree = ""; }; A7ED1F581E831C5C00BFFA01 /* MessagesSearchViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesSearchViewModelTests.swift; sourceTree = ""; }; A7ED1F5A1E831C5C00BFFA01 /* MessageCellViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageCellViewModelTests.swift; sourceTree = ""; }; A7ED1F5C1E831C5C00BFFA01 /* BackingCellViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackingCellViewModelTests.swift; sourceTree = ""; }; - A7ED1F5E1E831C5C00BFFA01 /* ProjectActivityCommentCellViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectActivityCommentCellViewModelTests.swift; sourceTree = ""; }; A7ED1F601E831C5C00BFFA01 /* FindFriendsFriendFollowCellViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FindFriendsFriendFollowCellViewModelTests.swift; sourceTree = ""; }; A7ED1F621E831C5C00BFFA01 /* ProjectNotificationCellViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectNotificationCellViewModelTests.swift; sourceTree = ""; }; A7ED1F631E831C5C00BFFA01 /* SortPagerViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SortPagerViewModelTests.swift; sourceTree = ""; }; - A7ED1F681E831C5C00BFFA01 /* ProjectActivitySuccessCellViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectActivitySuccessCellViewModelTests.swift; sourceTree = ""; }; A7ED1F6A1E831C5C00BFFA01 /* SurveyResponseViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SurveyResponseViewModelTests.swift; sourceTree = ""; }; - A7ED1F6B1E831C5C00BFFA01 /* DashboardFundingCellViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardFundingCellViewModelTests.swift; sourceTree = ""; }; A7ED1F6C1E831C5C00BFFA01 /* MessageThreadsViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageThreadsViewModelTests.swift; sourceTree = ""; }; A7ED1F6D1E831C5C00BFFA01 /* ActivitiesViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivitiesViewModelTests.swift; sourceTree = ""; }; - A7ED1F6E1E831C5C00BFFA01 /* DashboardProjectsDrawerViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardProjectsDrawerViewModelTests.swift; sourceTree = ""; }; A7ED1F6F1E831C5C00BFFA01 /* ActivityFriendBackingViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityFriendBackingViewModelTests.swift; sourceTree = ""; }; - A7ED1F701E831C5C00BFFA01 /* ProjectActivityBackingCellViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectActivityBackingCellViewModelTests.swift; sourceTree = ""; }; - A7ED1F711E831C5C00BFFA01 /* ProjectActivityUpdateCellViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectActivityUpdateCellViewModelTests.swift; sourceTree = ""; }; - A7ED1F721E831C5C00BFFA01 /* DashboardReferrerRowStackViewViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardReferrerRowStackViewViewModelTests.swift; sourceTree = ""; }; - A7ED1F741E831C5C00BFFA01 /* DashboardActionCellViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardActionCellViewModelTests.swift; sourceTree = ""; }; A7ED1F761E831C5C00BFFA01 /* LoginToutViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginToutViewModelTests.swift; sourceTree = ""; }; - A7ED1F781E831C5C00BFFA01 /* DashboardRewardRowStackViewViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardRewardRowStackViewViewModelTests.swift; sourceTree = ""; }; A7ED1F791E831C5C00BFFA01 /* FindFriendsStatsCellViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FindFriendsStatsCellViewModelTests.swift; sourceTree = ""; }; A7ED1F7A1E831C5C00BFFA01 /* ActivityUpdateViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityUpdateViewModelTests.swift; sourceTree = ""; }; A7ED1F7B1E831C5C00BFFA01 /* SearchEmptyStateCellViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchEmptyStateCellViewModelTests.swift; sourceTree = ""; }; - A7ED1F7C1E831C5C00BFFA01 /* DashboardRewardsCellViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardRewardsCellViewModelTests.swift; sourceTree = ""; }; - A7ED1F7D1E831C5C00BFFA01 /* ProjectActivitiesViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectActivitiesViewModelTests.swift; sourceTree = ""; }; - A7ED1F7F1E831C5C00BFFA01 /* DashboardViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardViewModelTests.swift; sourceTree = ""; }; - A7ED1F801E831C5C00BFFA01 /* DashboardVideoCellViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardVideoCellViewModelTests.swift; sourceTree = ""; }; A7ED1F811E831C5C00BFFA01 /* ShareViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShareViewModelTests.swift; sourceTree = ""; }; A7ED1F821E831C5C00BFFA01 /* DiscoveryExpandableRowCellViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscoveryExpandableRowCellViewModelTests.swift; sourceTree = ""; }; A7ED1F831E831C5C00BFFA01 /* DiscoveryFiltersViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscoveryFiltersViewModelTests.swift; sourceTree = ""; }; @@ -2704,13 +2561,9 @@ A7ED1F8B1E831C5C00BFFA01 /* FacebookConfirmationViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FacebookConfirmationViewModelTests.swift; sourceTree = ""; }; A7ED1F8C1E831C5C00BFFA01 /* FindFriendsHeaderCellViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FindFriendsHeaderCellViewModelTests.swift; sourceTree = ""; }; A7ED1F8E1E831C5C00BFFA01 /* HelpViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HelpViewModelTests.swift; sourceTree = ""; }; - A7ED1F8F1E831C5C00BFFA01 /* DashboardReferrersCellViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardReferrersCellViewModelTests.swift; sourceTree = ""; }; - A7ED1F911E831C5C00BFFA01 /* DashboardProjectsDrawerCellViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardProjectsDrawerCellViewModelTests.swift; sourceTree = ""; }; - A7ED1F921E831C5C00BFFA01 /* ProjectActivityNegativeStateChangeCellViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectActivityNegativeStateChangeCellViewModelTests.swift; sourceTree = ""; }; A7ED1F931E831C5C00BFFA01 /* LoginViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginViewModelTests.swift; sourceTree = ""; }; A7ED1F941E831C5C00BFFA01 /* MessagesViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesViewModelTests.swift; sourceTree = ""; }; A7ED1F951E831C5C00BFFA01 /* MessageThreadCellViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageThreadCellViewModelTests.swift; sourceTree = ""; }; - A7ED1F961E831C5C00BFFA01 /* ProjectActivityLaunchCellViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectActivityLaunchCellViewModelTests.swift; sourceTree = ""; }; A7ED1F971E831C5C00BFFA01 /* ProjectCreatorViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectCreatorViewModelTests.swift; sourceTree = ""; }; A7ED1F981E831C5C00BFFA01 /* ProjectDescriptionViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectDescriptionViewModelTests.swift; sourceTree = ""; }; A7ED1F9B1E831C5C00BFFA01 /* ResetPasswordViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResetPasswordViewModelTests.swift; sourceTree = ""; }; @@ -2726,13 +2579,10 @@ A7ED1FA71E831C5C00BFFA01 /* TwoFactorViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TwoFactorViewModelTests.swift; sourceTree = ""; }; A7ED1FA81E831C5C00BFFA01 /* UpdateDraftViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateDraftViewModelTests.swift; sourceTree = ""; }; A7ED20051E83229E00BFFA01 /* ActivitiesDataSourceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivitiesDataSourceTests.swift; sourceTree = ""; }; - A7ED20071E83229E00BFFA01 /* DashboardDataSourceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardDataSourceTests.swift; sourceTree = ""; }; - A7ED20081E83229E00BFFA01 /* DashboardProjectsDrawerDataSourceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardProjectsDrawerDataSourceTests.swift; sourceTree = ""; }; A7ED20091E83229E00BFFA01 /* DiscoveryFiltersDataSourceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscoveryFiltersDataSourceTests.swift; sourceTree = ""; }; A7ED200A1E83229E00BFFA01 /* DiscoveryPagesDataSourceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscoveryPagesDataSourceTests.swift; sourceTree = ""; }; A7ED200B1E83229E00BFFA01 /* DiscoveryProjectsDataSourceTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscoveryProjectsDataSourceTest.swift; sourceTree = ""; }; A7ED200C1E83229E00BFFA01 /* FindFriendsDataSourceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FindFriendsDataSourceTests.swift; sourceTree = ""; }; - A7ED200F1E83229E00BFFA01 /* ProjectActivitiesDataSourceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectActivitiesDataSourceTests.swift; sourceTree = ""; }; A7ED20101E83229E00BFFA01 /* ProjectPamphletContentDataSourceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectPamphletContentDataSourceTests.swift; sourceTree = ""; }; A7ED20211E83237F00BFFA01 /* Combos.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Combos.swift; sourceTree = ""; }; A7ED20221E83237F00BFFA01 /* TraitController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TraitController.swift; sourceTree = ""; }; @@ -2740,12 +2590,10 @@ A7ED20271E8323E900BFFA01 /* SortPagerViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SortPagerViewControllerTests.swift; sourceTree = ""; }; A7ED20281E8323E900BFFA01 /* EmptyStatesViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyStatesViewControllerTests.swift; sourceTree = ""; }; A7ED20291E8323E900BFFA01 /* ResetPasswordViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResetPasswordViewControllerTests.swift; sourceTree = ""; }; - A7ED202A1E8323E900BFFA01 /* DashboardViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardViewControllerTests.swift; sourceTree = ""; }; A7ED202D1E8323E900BFFA01 /* LoginViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginViewControllerTests.swift; sourceTree = ""; }; A7ED202E1E8323E900BFFA01 /* DiscoveryPageViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscoveryPageViewControllerTests.swift; sourceTree = ""; }; A7ED20301E8323E900BFFA01 /* DiscoveryNavigationHeaderViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscoveryNavigationHeaderViewControllerTests.swift; sourceTree = ""; }; A7ED20321E8323E900BFFA01 /* FindFriendsViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FindFriendsViewControllerTests.swift; sourceTree = ""; }; - A7ED20331E8323E900BFFA01 /* ProjectActivityViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProjectActivityViewControllerTests.swift; sourceTree = ""; }; A7ED20371E8323E900BFFA01 /* FacebookConfirmationViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FacebookConfirmationViewControllerTests.swift; sourceTree = ""; }; A7ED20381E8323E900BFFA01 /* SearchViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchViewControllerTests.swift; sourceTree = ""; }; A7ED203A1E8323E900BFFA01 /* LoginToutViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginToutViewControllerTests.swift; sourceTree = ""; }; @@ -2753,11 +2601,9 @@ A7ED203D1E8323E900BFFA01 /* SignupViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignupViewControllerTests.swift; sourceTree = ""; }; A7ED203E1E8323E900BFFA01 /* MessageThreadsViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageThreadsViewControllerTests.swift; sourceTree = ""; }; A7ED203F1E8323E900BFFA01 /* TwoFactorViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TwoFactorViewControllerTests.swift; sourceTree = ""; }; - A7ED205B1E83240D00BFFA01 /* FundingGraphViewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FundingGraphViewTests.swift; sourceTree = ""; }; A7ED205D1E83256700BFFA01 /* AppDelegateViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegateViewModelTests.swift; sourceTree = ""; }; A7ED205E1E83256700BFFA01 /* HelpWebViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HelpWebViewModelTests.swift; sourceTree = ""; }; A7ED205F1E83256700BFFA01 /* RootViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootViewModelTests.swift; sourceTree = ""; }; - A7ED20601E83256700BFFA01 /* UpdatePreviewViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdatePreviewViewModelTests.swift; sourceTree = ""; }; A7ED20611E83256700BFFA01 /* UpdateViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateViewModelTests.swift; sourceTree = ""; }; A7F4418E1D005A9400FE6FC5 /* ActivitiesViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivitiesViewModel.swift; sourceTree = ""; }; A7F4418F1D005A9400FE6FC5 /* ActivityFriendBackingViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityFriendBackingViewModel.swift; sourceTree = ""; }; @@ -4735,9 +4581,6 @@ 1937A6E028C92B1D00DD732D /* CreatePassword */, 19A97CD428C79BF60031B857 /* Comments */, 1937A6E128C92BBD00DD732D /* CuratedProjects */, - 19A97CF728C7E40C0031B857 /* Dashboard */, - 19A97CFD28C7E6460031B857 /* DashboardProjects */, - 60DF50952A434E30002C771F /* DashboardDeprecationBanner */, 1937A6E228C92BD400DD732D /* DebugPushNotifications */, 6049D00E2A9CF6530015BB0D /* DesignSystem */, 19A97D0328C7E7350031B857 /* DiscoveryFilters */, @@ -4763,7 +4606,6 @@ 19A97D3928C802230031B857 /* PledgePaymentMethods */, 1937A70428C9392600DD732D /* PledgeShippingLocation */, 1937A70528C9399D00DD732D /* PledgeView */, - 19A97D3F28C8049E0031B857 /* ProjectActivities */, 1965436E28C810CC00457EC6 /* ProjectNotifications */, 1965437528C812F100457EC6 /* ProjectPamphletContentDataSource_DEPRECATED_09_06_2022 */, 1965437628C8141700457EC6 /* ProjectPage */, @@ -5079,126 +4921,6 @@ path = Datasource; sourceTree = ""; }; - 19A97CF728C7E40C0031B857 /* Dashboard */ = { - isa = PBXGroup; - children = ( - 19A97CF828C7E5570031B857 /* Views */, - A751A51A1C85EAD8009C5DEA /* ViewModel */, - 19A97CFA28C7E56F0031B857 /* Controller */, - 19A97CFB28C7E5780031B857 /* Storyboard */, - 19A97CFC28C7E5810031B857 /* Datasource */, - ); - path = Dashboard; - sourceTree = ""; - }; - 19A97CF828C7E5570031B857 /* Views */ = { - isa = PBXGroup; - children = ( - A78012641D2EEA620027396E /* ReferralChartView.swift */, - 59019FB81D21ABD200EAEC9D /* DashboardReferrerRowStackView.swift */, - 015A06F21D21914E007AE210 /* DashboardRewardRowStackView.swift */, - 018422841D2C47A400CA7566 /* DashboardTitleView.swift */, - 19A97CF928C7E5650031B857 /* Cells */, - ); - path = Views; - sourceTree = ""; - }; - 19A97CF928C7E5650031B857 /* Cells */ = { - isa = PBXGroup; - children = ( - 59D1E6241D1865AC00896A4C /* DashboardVideoCell.swift */, - 59B0E0021D1203970081D2DC /* DashboardActionCell.swift */, - 59B0DFFC1D11B2E50081D2DC /* DashboardContextCell.swift */, - 5955E64D1D21800300B4153D /* DashboardReferrersCell.swift */, - 015A06F51D219513007AE210 /* DashboardRewardsCell.swift */, - 593AC5CE1D33F4BF002613F4 /* DashboardFundingCell.swift */, - ); - path = Cells; - sourceTree = ""; - }; - 19A97CFA28C7E56F0031B857 /* Controller */ = { - isa = PBXGroup; - children = ( - A745D0461CA8985B00C12802 /* DashboardViewController.swift */, - A7ED202A1E8323E900BFFA01 /* DashboardViewControllerTests.swift */, - 803BDF731D11AF7C004A785A /* UpdateDraftViewController.swift */, - 8072F41C1D46B75200999EF1 /* UpdatePreviewViewController.swift */, - ); - path = Controller; - sourceTree = ""; - }; - 19A97CFB28C7E5780031B857 /* Storyboard */ = { - isa = PBXGroup; - children = ( - A73923BB1D272242004524C3 /* UpdateDraft.storyboard */, - A73923AE1D272242004524C3 /* Dashboard.storyboard */, - ); - path = Storyboard; - sourceTree = ""; - }; - 19A97CFC28C7E5810031B857 /* Datasource */ = { - isa = PBXGroup; - children = ( - 59B0DFC31D11AC850081D2DC /* DashboardDataSource.swift */, - A7ED20071E83229E00BFFA01 /* DashboardDataSourceTests.swift */, - ); - path = Datasource; - sourceTree = ""; - }; - 19A97CFD28C7E6460031B857 /* DashboardProjects */ = { - isa = PBXGroup; - children = ( - 19A97CFE28C7E6E30031B857 /* Storyboard */, - 19A97CFF28C7E6EA0031B857 /* Views */, - 19A97D0128C7E6F60031B857 /* Datasource */, - 19A97D0228C7E6FE0031B857 /* Controller */, - ); - path = DashboardProjects; - sourceTree = ""; - }; - 19A97CFE28C7E6E30031B857 /* Storyboard */ = { - isa = PBXGroup; - children = ( - 01A120D01D2D646300B42F73 /* DashboardProjectsDrawer.storyboard */, - ); - path = Storyboard; - sourceTree = ""; - }; - 19A97CFF28C7E6EA0031B857 /* Views */ = { - isa = PBXGroup; - children = ( - 595CDAB71D3537180051C816 /* FundingGraphView.swift */, - A7ED205B1E83240D00BFFA01 /* FundingGraphViewTests.swift */, - 19A97D0028C7E6F00031B857 /* Cells */, - ); - path = Views; - sourceTree = ""; - }; - 19A97D0028C7E6F00031B857 /* Cells */ = { - isa = PBXGroup; - children = ( - 018422B61D2C47D600CA7566 /* DashboardProjectsDrawerCell.swift */, - ); - path = Cells; - sourceTree = ""; - }; - 19A97D0128C7E6F60031B857 /* Datasource */ = { - isa = PBXGroup; - children = ( - 018422C31D2C48AA00CA7566 /* DashboardProjectsDrawerDataSource.swift */, - A7ED20081E83229E00BFFA01 /* DashboardProjectsDrawerDataSourceTests.swift */, - ); - path = Datasource; - sourceTree = ""; - }; - 19A97D0228C7E6FE0031B857 /* Controller */ = { - isa = PBXGroup; - children = ( - 01A120D21D2D6E6200B42F73 /* DashboardProjectsDrawerViewController.swift */, - ); - path = Controller; - sourceTree = ""; - }; 19A97D0328C7E7350031B857 /* DiscoveryFilters */ = { isa = PBXGroup; children = ( @@ -5689,66 +5411,6 @@ path = Controller; sourceTree = ""; }; - 19A97D3F28C8049E0031B857 /* ProjectActivities */ = { - isa = PBXGroup; - children = ( - 19A97D4428C8059F0031B857 /* Controller */, - 19A97D4328C805970031B857 /* Storyboard */, - 19A97D4228C8058E0031B857 /* Views */, - 19A97D4028C8057E0031B857 /* Datasource */, - ); - path = ProjectActivities; - sourceTree = ""; - }; - 19A97D4028C8057E0031B857 /* Datasource */ = { - isa = PBXGroup; - children = ( - 9D9F58141D131D4A00CE81DE /* ProjectActivitiesDataSource.swift */, - A7ED200F1E83229E00BFFA01 /* ProjectActivitiesDataSourceTests.swift */, - ); - path = Datasource; - sourceTree = ""; - }; - 19A97D4128C805870031B857 /* Cells */ = { - isa = PBXGroup; - children = ( - 9D9F57CB1D131AF200CE81DE /* ProjectActivityBackingCell.swift */, - 9D2546F71D23101E0053844D /* ProjectActivityCommentCell.swift */, - 9D525F0E1D4158AC003CAE04 /* ProjectActivityDateCell.swift */, - 9D9F57CC1D131AF200CE81DE /* ProjectActivityEmptyStateCell.swift */, - 9D9F57CD1D131AF200CE81DE /* ProjectActivityLaunchCell.swift */, - 9D9F57CE1D131AF200CE81DE /* ProjectActivityNegativeStateChangeCell.swift */, - 9D9F57D01D131AF200CE81DE /* ProjectActivitySuccessCell.swift */, - 9D9F57D11D131AF200CE81DE /* ProjectActivityUpdateCell.swift */, - ); - path = Cells; - sourceTree = ""; - }; - 19A97D4228C8058E0031B857 /* Views */ = { - isa = PBXGroup; - children = ( - 19A97D4128C805870031B857 /* Cells */, - ); - path = Views; - sourceTree = ""; - }; - 19A97D4328C805970031B857 /* Storyboard */ = { - isa = PBXGroup; - children = ( - A73923B71D272242004524C3 /* ProjectActivity.storyboard */, - ); - path = Storyboard; - sourceTree = ""; - }; - 19A97D4428C8059F0031B857 /* Controller */ = { - isa = PBXGroup; - children = ( - 9D9F580C1D131B1200CE81DE /* ProjectActivitiesViewController.swift */, - A7ED20331E8323E900BFFA01 /* ProjectActivityViewControllerTests.swift */, - ); - path = Controller; - sourceTree = ""; - }; 19C8E56729A923F4007C3504 /* ViewModifiers */ = { isa = PBXGroup; children = ( @@ -6161,15 +5823,6 @@ path = Styles; sourceTree = ""; }; - A751A51A1C85EAD8009C5DEA /* ViewModel */ = { - isa = PBXGroup; - children = ( - 8072F44E1D46BAA400999EF1 /* UpdatePreviewViewModel.swift */, - A7ED20601E83256700BFFA01 /* UpdatePreviewViewModelTests.swift */, - ); - path = ViewModel; - sourceTree = ""; - }; A751A51B1C85EAD8009C5DEA /* SharedViews */ = { isa = PBXGroup; children = ( @@ -6570,28 +6223,6 @@ D6B6876D21937182005F5DA7 /* CreditCardCellViewModelTests.swift */, D660078B2416D66C00AC1EDB /* CuratedProjectsViewModel.swift */, D660078F241A7C0000AC1EDB /* CuratedProjectsViewModelTests.swift */, - 59B0E0051D1207070081D2DC /* DashboardActionCellViewModel.swift */, - A7ED1F741E831C5C00BFFA01 /* DashboardActionCellViewModelTests.swift */, - 593AC6001D33F517002613F4 /* DashboardFundingCellViewModel.swift */, - A7ED1F6B1E831C5C00BFFA01 /* DashboardFundingCellViewModelTests.swift */, - 018422BF1D2C486900CA7566 /* DashboardProjectsDrawerCellViewModel.swift */, - A7ED1F911E831C5C00BFFA01 /* DashboardProjectsDrawerCellViewModelTests.swift */, - 0199545E1D2D818E00BC1390 /* DashboardProjectsDrawerViewModel.swift */, - A7ED1F6E1E831C5C00BFFA01 /* DashboardProjectsDrawerViewModelTests.swift */, - 59019FB51D21A47700EAEC9D /* DashboardReferrerRowStackViewViewModel.swift */, - A7ED1F721E831C5C00BFFA01 /* DashboardReferrerRowStackViewViewModelTests.swift */, - 5955E6801D21805200B4153D /* DashboardReferrersCellViewModel.swift */, - A7ED1F8F1E831C5C00BFFA01 /* DashboardReferrersCellViewModelTests.swift */, - 0156B5571D133BA0000C4252 /* DashboardRewardRowStackViewViewModel.swift */, - A7ED1F781E831C5C00BFFA01 /* DashboardRewardRowStackViewViewModelTests.swift */, - 0156B5231D1327A1000C4252 /* DashboardRewardsCellViewModel.swift */, - A7ED1F7C1E831C5C00BFFA01 /* DashboardRewardsCellViewModelTests.swift */, - 018422B81D2C482900CA7566 /* DashboardTitleViewViewModel.swift */, - A7ED1F561E831C5C00BFFA01 /* DashboardTitleViewViewModelTests.swift */, - 59D1E6571D1866F800896A4C /* DashboardVideoCellViewModel.swift */, - A7ED1F801E831C5C00BFFA01 /* DashboardVideoCellViewModelTests.swift */, - 9D9F580E1D131B4000CE81DE /* DashboardViewModel.swift */, - A7ED1F7F1E831C5C00BFFA01 /* DashboardViewModelTests.swift */, 06BD75C726C431A000A12D4E /* CommentDialogViewModel.swift */, 06BD75C826C431A000A12D4E /* CommentDialogViewModelTests.swift */, D703478F1DBAABC30099C668 /* DiscoveryExpandableRowCellViewModel.swift */, @@ -6708,20 +6339,6 @@ D710ADFC2441172100DC7199 /* PledgeViewCTAContainerViewModelTests.swift */, 37DEC1E62257C9F30051EF9B /* PledgeViewModel.swift */, 37DEC21F2257CA0A0051EF9B /* PledgeViewModelTests.swift */, - 9D9F580F1D131B4000CE81DE /* ProjectActivitiesViewModel.swift */, - A7ED1F7D1E831C5C00BFFA01 /* ProjectActivitiesViewModelTests.swift */, - 9DEE3B241D1D81950020C2BE /* ProjectActivityBackingCellViewModel.swift */, - A7ED1F701E831C5C00BFFA01 /* ProjectActivityBackingCellViewModelTests.swift */, - 9D25472C1D23135F0053844D /* ProjectActivityCommentCellViewModel.swift */, - A7ED1F5E1E831C5C00BFFA01 /* ProjectActivityCommentCellViewModelTests.swift */, - 9D8772121D19E84E003A4E96 /* ProjectActivityLaunchCellViewModel.swift */, - A7ED1F961E831C5C00BFFA01 /* ProjectActivityLaunchCellViewModelTests.swift */, - 9DC204B61D1B46BD003C1636 /* ProjectActivityNegativeStateChangeCellViewModel.swift */, - A7ED1F921E831C5C00BFFA01 /* ProjectActivityNegativeStateChangeCellViewModelTests.swift */, - 9D2F4BDF1D1AE02700B7C554 /* ProjectActivitySuccessCellViewModel.swift */, - A7ED1F681E831C5C00BFFA01 /* ProjectActivitySuccessCellViewModelTests.swift */, - 9D14FFC51D135C12005F4ABB /* ProjectActivityUpdateCellViewModel.swift */, - A7ED1F711E831C5C00BFFA01 /* ProjectActivityUpdateCellViewModelTests.swift */, A7808BEF1D625C6A001CF96A /* ProjectCreatorViewModel.swift */, A7ED1F971E831C5C00BFFA01 /* ProjectCreatorViewModelTests.swift */, A7C93FB51D44142900C2DF9B /* ProjectDescriptionViewModel.swift */, @@ -7531,7 +7148,6 @@ 77A3C502219B5E4900824FC1 /* Assets.xcassets in Resources */, D6B6766320FE849F0082717D /* SettingsNewsletters.storyboard in Resources */, 77FA6CAB20F3E3C200809E31 /* SettingsTableViewCell.xib in Resources */, - A73923CC1D272242004524C3 /* UpdateDraft.storyboard in Resources */, 77752F97219B3D7300E7DA7D /* SettingsAccountWarningCell.xib in Resources */, 066A518526C598390009898C /* CommentsDialog.storyboard in Resources */, A7986C9D1D6B85840027030D /* Thanks.storyboard in Resources */, @@ -7541,10 +7157,8 @@ D6C3845C210B9AC400ADB671 /* SettingsNewslettersTopCell.xib in Resources */, 77FD8B44216D6167000A95AC /* LoadingBarButtonItemView.xib in Resources */, D6A7AB401FDAF3EC007B20AE /* ThanksCategoryCell.xib in Resources */, - 01A120D11D2D646300B42F73 /* DashboardProjectsDrawer.storyboard in Resources */, 59673C8B1D50EC920035AFD9 /* Video.storyboard in Resources */, 77F9A9B22135FC9B0082A11E /* FindFriendsCell.xib in Resources */, - A73923BF1D272242004524C3 /* Dashboard.storyboard in Resources */, 77A7B64321026CAE008D12C1 /* SettingsNotificationCell.xib in Resources */, D63BBCDB217E7802007E01F0 /* CreditCardCell.xib in Resources */, 774527DE21B1E0480072E590 /* MessageBannerViewController.xib in Resources */, @@ -7554,7 +7168,6 @@ 01940B2E1D46A9AD0074FCE3 /* Help.storyboard in Resources */, 014D62FF1E7211220033D2BD /* BackerDashboardProjectCell.xib in Resources */, 77C5E21A214182A2002E1670 /* SettingsPrivacySwitchCell.xib in Resources */, - A73923C81D272242004524C3 /* ProjectActivity.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -7698,7 +7311,6 @@ 014D629B1E6E31790033D2BD /* BackerDashboardProjectsViewModel.swift in Sources */, D78E038E229305E90043E92F /* PledgeStateCTAType.swift in Sources */, A73379451D0E36A600C91445 /* Colors.swift in Sources */, - 01C7CDB11D13462500D9E0D1 /* DashboardRewardRowStackViewViewModel.swift in Sources */, D04AAC32218BB70D00CF713E /* MessageBannerViewModel.swift in Sources */, A73378FE1D0AE36400C91445 /* LoginStyles.swift in Sources */, 0154A93B1CA1A17800DB9BA4 /* UIColor.swift in Sources */, @@ -7745,7 +7357,6 @@ 473DE012273C502F0033331D /* ProjectRisksCellViewModel.swift in Sources */, A755115F1C8642C3005355CF /* Format.swift in Sources */, 598D96CB1D42AE85003F3F66 /* ActivitySampleProjectCellViewModel.swift in Sources */, - 01C7CDB31D13462A00D9E0D1 /* DashboardRewardsCellViewModel.swift in Sources */, A7D121091D15B08200F364FD /* CountBadgeView.swift in Sources */, 77216CE220EFE7F70061BE82 /* FacebookConnectionType.swift in Sources */, D7ADDFE722E0DAFA00157D83 /* RewardCellProjectBackingStateType.swift in Sources */, @@ -7794,7 +7405,6 @@ 4791BDE6271762E600DFE5D5 /* ProjectFAQsCellViewModel.swift in Sources */, 0634C2F927CFEEC2003A6D6E /* ExternalSourceViewElementCellViewModel.swift in Sources */, 94BA16E426698C8B0034CC3F /* CommentTableViewFooterViewModel.swift in Sources */, - 59B0E0061D1207070081D2DC /* DashboardActionCellViewModel.swift in Sources */, 77D19FF5240813240058FC8E /* NavigationController.swift in Sources */, 8A8099F422E2142C00373E66 /* RewardCardContainerViewModel.swift in Sources */, A75511631C8642C3005355CF /* LocalizedString.swift in Sources */, @@ -7806,7 +7416,6 @@ A734A2671D21A1790080BBD5 /* WKNavigationActionData.swift in Sources */, A7C93FB61D44142900C2DF9B /* ProjectDescriptionViewModel.swift in Sources */, 8AFB8C97233E9977006779B5 /* CreatePaymentSourceInput+Constructor.swift in Sources */, - 9D8772131D19E84E003A4E96 /* ProjectActivityLaunchCellViewModel.swift in Sources */, 774F8D5D22B1B14100A1ACD5 /* FeatureFlagToolsViewModel.swift in Sources */, A7F441CF1D005A9400FE6FC5 /* MessageThreadCellViewModel.swift in Sources */, 194154D128D8FBAA004648C8 /* CreatePaymentSourceSetupIntentClientSecret+Constructor.swift in Sources */, @@ -7820,7 +7429,6 @@ 3757D0CE228E51F800241AE6 /* UIFont.swift in Sources */, 778F891C22D3E37300D095C5 /* Feature+Helpers.swift in Sources */, 37C7B81523187BA400C78278 /* ShippingRuleCellViewModel.swift in Sources */, - 9D9F581B1D1324E200CE81DE /* DashboardViewModel.swift in Sources */, 77C9122923C4FC0B00F3D2C9 /* ApplePayCapabilities.swift in Sources */, D796867C20FE655300E54C61 /* SettingsFollowCellViewModel.swift in Sources */, D79A01A42242E8CD004BC6A8 /* AppEnvironmentType.swift in Sources */, @@ -7837,7 +7445,6 @@ D093B49C21A86FD800910962 /* PushRegistration.swift in Sources */, A7F441B31D005A9400FE6FC5 /* ActivityProjectStatusViewModel.swift in Sources */, 370BE71622541C8100B44DB2 /* UIViewController+URL.swift in Sources */, - 59D1E6581D1866F800896A4C /* DashboardVideoCellViewModel.swift in Sources */, A75511641C8642C3005355CF /* NSBundleType.swift in Sources */, D04AAC2D218BB70D00CF713E /* SettingsNotificationPickerViewModel.swift in Sources */, 776B1A0F24192CA900B03098 /* CategoryPillCellViewModel.swift in Sources */, @@ -7847,7 +7454,6 @@ 064B007A27A469C8007B21FE /* HTMLViewElementStyles.swift in Sources */, 8A213CEF239EAEA400BBB4C7 /* KSRAnalytics.swift in Sources */, D70347901DBAABC30099C668 /* DiscoveryExpandableRowCellViewModel.swift in Sources */, - 0199545F1D2D818E00BC1390 /* DashboardProjectsDrawerViewModel.swift in Sources */, D79970B423EB79A600584911 /* ThanksCategoryCellViewModel.swift in Sources */, D67DF564232ABB960051D207 /* ManagePledgeViewModel.swift in Sources */, D0A787BF2204D975006AE4F4 /* UITableView+AutoLayoutHeaderView.swift in Sources */, @@ -7862,7 +7468,6 @@ 597073521D05FE6B00B00444 /* ProjectNotificationsViewModel.swift in Sources */, 8A45168D24B3D02700D8CAEF /* RewardAddOnSelectionViewModel.swift in Sources */, 77F6E73721222E97005A5C55 /* SettingsCellType.swift in Sources */, - 59019FB61D21A47700EAEC9D /* DashboardReferrerRowStackViewViewModel.swift in Sources */, D703FC6B20F7E3F8004A272D /* SettingsPrivacyViewModel.swift in Sources */, 8A4DDAB52373429300ADE31D /* PledgeStatusLabelViewModel.swift in Sources */, 0169F9881D6F51C400C8D5C5 /* DiscoveryFiltersViewModel.swift in Sources */, @@ -7872,7 +7477,6 @@ A7F441E91D005A9400FE6FC5 /* TwoFactorViewModel.swift in Sources */, 8A417DF025AE37D200A2C406 /* Segment.swift in Sources */, D6534D3C22E7898B00E9D279 /* PledgePaymentMethodsViewModel.swift in Sources */, - 9DEE3B561D1D819D0020C2BE /* ProjectActivityBackingCellViewModel.swift in Sources */, A78537F81CB5803B00385B73 /* NSHTTPCookieStorageType.swift in Sources */, D63BBD37217FC224007E01F0 /* CreditCardCellViewModel.swift in Sources */, 8A13D16124955A2E007E2C0B /* PledgeExpandableRewardsHeaderViewModel.swift in Sources */, @@ -7892,7 +7496,6 @@ D04AAC33218BB70D00CF713E /* SettingsPrivacySwitchCellViewModel.swift in Sources */, A7169BF61DDD064200480C0D /* UIScrollView+Extensions.swift in Sources */, 597582E31D5D12AE008765DE /* SettingsStyles.swift in Sources */, - 9D14FFC61D135C12005F4ABB /* ProjectActivityUpdateCellViewModel.swift in Sources */, A7F441AF1D005A9400FE6FC5 /* ActivityFriendBackingViewModel.swift in Sources */, 8AAA9BD822F49EC200F12976 /* UIColor+Mixing.swift in Sources */, 8AC3E13A269F781D00168BF8 /* ErrorEnvelope+LocalizedDescription.swift in Sources */, @@ -7908,7 +7511,6 @@ A7CC13D91D00E6CF00035C52 /* FindFriendsStatsCellViewModel.swift in Sources */, D08CD1FF21913166009F89F0 /* WatchProjectViewModel.swift in Sources */, D6B4EFC6210686270079159D /* SettingsNewslettersViewModel.swift in Sources */, - 018422BB1D2C483000CA7566 /* DashboardTitleViewViewModel.swift in Sources */, D04AAC1F218BB70D00CF713E /* SettingsNotificationCellViewModel.swift in Sources */, A72AFFDA1CD7ED6B008F052B /* Keyboard.swift in Sources */, 1996AA352A5F4ADB00AE2ED0 /* PaymentSheetPaymentOptionsDisplayData.swift in Sources */, @@ -7916,12 +7518,10 @@ D04AAC2F218BB70D00CF713E /* LoadingBarButtonItemViewModel.swift in Sources */, A718885D1DE0DDCE0094856D /* ShortcutItem.swift in Sources */, 8AF91CDD22EF5AF8005F9C90 /* Feature.swift in Sources */, - 9D25472D1D23135F0053844D /* ProjectActivityCommentCellViewModel.swift in Sources */, 473DE016273C56E50033331D /* ProjectRisksDisclaimerCellViewModel.swift in Sources */, 20338E262656AAD900F2C43A /* CommentComposerViewModel.swift in Sources */, A755116A1C8642C3005355CF /* UILabel+LocalizedKey.swift in Sources */, A755116B1C8642C3005355CF /* UILabel+SimpleHTML.swift in Sources */, - 5955E6811D21805200B4153D /* DashboardReferrersCellViewModel.swift in Sources */, D66850A1236CA44C00EE9AC2 /* ProjectPamphletCreatorHeaderCellViewModel.swift in Sources */, A75C81201D210C4700B5AD03 /* UpdateActivityItemProvider.swift in Sources */, 9D7536CF1D78D88D00A7623B /* SurveyResponseViewModel.swift in Sources */, @@ -7958,15 +7558,12 @@ 778215E820F6922100F3D09F /* HelpType.swift in Sources */, 01940B261D42DC1A0074FCE3 /* HelpViewModel.swift in Sources */, D6B6766520FE85010082717D /* SettingsNewslettersCellViewModel.swift in Sources */, - 593AC6011D33F517002613F4 /* DashboardFundingCellViewModel.swift in Sources */, 373AB25D222A0D8900769FC2 /* PasswordValidation.swift in Sources */, D710ADF7243CCE7E00DC7199 /* ISO8601DateFormatter.swift in Sources */, - 9D2F4BE01D1AE02700B7C554 /* ProjectActivitySuccessCellViewModel.swift in Sources */, D69C5529239ED10E00B0987A /* ErroredBackingViewViewModel.swift in Sources */, A707BADA1CFFAB9400653B2F /* Notifications.swift in Sources */, 77556F5420A099B3008CEA57 /* ExperimentName+Helpers.swift in Sources */, A76126BB1C90C94000EDCCB9 /* UITableView-Extensions.swift in Sources */, - 9D9F58191D13243900CE81DE /* ProjectActivitiesViewModel.swift in Sources */, 77AA2B3B233C0A1B008BBCB8 /* CreateBackingInput+Constructor.swift in Sources */, 6067BCEC293E49F00036ABB1 /* FacebookResetPasswordViewModel.swift in Sources */, 77C26957240D711A009AD91E /* CategoryCollectionViewSectionHeaderViewModel.swift in Sources */, @@ -7992,7 +7589,6 @@ D04AAC25218BB70D00CF713E /* FindFriendsCellViewModel.swift in Sources */, 3706408D22A9BE8100889CBD /* DateFormatter.swift in Sources */, D04AABF8218BB2FD00CF713E /* SettingsNotificationsViewModel.swift in Sources */, - 9DC204B71D1B46BD003C1636 /* ProjectActivityNegativeStateChangeCellViewModel.swift in Sources */, A7698B2A1D00602800953FD3 /* LoginViewModel.swift in Sources */, A7F441B51D005A9400FE6FC5 /* ActivityUpdateViewModel.swift in Sources */, A75C811C1D210C4700B5AD03 /* SafariActivity.swift in Sources */, @@ -8013,7 +7609,6 @@ 8A297910234EA8FD0039396D /* PledgeAmountSummaryViewModel.swift in Sources */, 941710B526557A6A00F5954E /* CommentCellStyles.swift in Sources */, 77D7A739214187A9003F258C /* SettingsSwitchCellType.swift in Sources */, - 018422C01D2C486900CA7566 /* DashboardProjectsDrawerCellViewModel.swift in Sources */, 8A213CEB239EAEA400BBB4C7 /* TrackingClientType.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -8035,26 +7630,21 @@ A7ED1F491E831BA200BFFA01 /* DispatchTimeInterval-Extensions.swift in Sources */, 77AA2B3D233D10D3008BBCB8 /* CreateBackingConstructorTests.swift in Sources */, 8AD9CF1424C8D46800F77223 /* PledgeShippingSummaryViewModelTests.swift in Sources */, - A7ED1FD91E831C5C00BFFA01 /* ProjectActivitiesViewModelTests.swift in Sources */, 06DC4152266FFE81005205F7 /* RootCommentCellViewModelTests.swift in Sources */, - A7ED1FDB1E831C5C00BFFA01 /* DashboardViewModelTests.swift in Sources */, 77D19FF22406D5A40058FC8E /* CategorySelectionViewModelTests.swift in Sources */, 3706408822A8A6F200889CBD /* PledgeShippingLocationViewModelTests.swift in Sources */, 8AFB8C99233E9A1F006779B5 /* CreatePaymentSourceInput+ConstructorTests.swift in Sources */, A7ED1F2B1E830FDC00BFFA01 /* IsValidEmailTests.swift in Sources */, - A7ED1FEB1E831C5C00BFFA01 /* DashboardReferrersCellViewModelTests.swift in Sources */, 37C7B81723187BAC00C78278 /* ShippingRuleCellViewModelTests.swift in Sources */, 6008633F29BF750700B87B39 /* MockAppTrackingTransparency.swift in Sources */, 37FDAFAD2273BA4B00662CC8 /* UIStackView+Tests.swift in Sources */, 8A13D16E24985411007E2C0B /* PledgeExpandableHeaderRewardCellViewModelTests.swift in Sources */, D04AACA7218BB72100CF713E /* DiscoveryProjectCategoryViewModelTests.swift in Sources */, - A7ED1FD41E831C5C00BFFA01 /* DashboardRewardRowStackViewViewModelTests.swift in Sources */, A7ED1F331E830FDC00BFFA01 /* RefTagTests.swift in Sources */, D68F0B1A243FB688009FC948 /* SignInWithAppleEnvelopeTests.swift in Sources */, A7ED1FDE1E831C5C00BFFA01 /* DiscoveryExpandableRowCellViewModelTests.swift in Sources */, 20338E1F26566F9E00F2C43A /* CommentComposerViewModelTests.swift in Sources */, 77D19FFA240824D60058FC8E /* PillCellViewModelTests.swift in Sources */, - A7ED1FC41E831C5C00BFFA01 /* ProjectActivitySuccessCellViewModelTests.swift in Sources */, 3777F2F92343C7AB0030BEF5 /* ManageViewPledgeRewardReceivedViewModelTests.swift in Sources */, 47AC2139264AD5430090AEDF /* CommentsViewModelTests.swift in Sources */, A7ED1FE31E831C5C00BFFA01 /* FindFriendsFaceookConnectCellViewModelTests.swift in Sources */, @@ -8103,7 +7693,6 @@ D612A21922F0ED5A007F7FD9 /* CreditCard+UtilsTests.swift in Sources */, A7ED1F3D1E831B4F00BFFA01 /* ValueCellDataSourceTests.swift in Sources */, 77C9123123C637FD00F3D2C9 /* DoubleCurrencyTests.swift in Sources */, - A7ED1FEE1E831C5C00BFFA01 /* ProjectActivityNegativeStateChangeCellViewModelTests.swift in Sources */, A7ED1F4C1E831BA200BFFA01 /* XCTestCase+AppEnvironment.swift in Sources */, 4746FFF5272C588900EC3429 /* ProjectTabDisclaimerCellViewModelTests.swift in Sources */, D6B6876E21937182005F5DA7 /* CreditCardCellViewModelTests.swift in Sources */, @@ -8113,6 +7702,7 @@ D09362B0225D803600E1411A /* UIViewController+URLTests.swift in Sources */, 77319C7E2469B5C70051B755 /* DiscoveryProjectCardViewModelTests.swift in Sources */, A7ED1FDF1E831C5C00BFFA01 /* DiscoveryFiltersViewModelTests.swift in Sources */, + 191E60282A953FAB001413B2 /* ProjectTabQuestionAnswerCellViewModelTests.swift in Sources */, A7ED1FDC1E831C5C00BFFA01 /* DashboardVideoCellViewModelTests.swift in Sources */, 8A5CB28424C11819003113D4 /* RewardAddOnSelectionContinueCTAViewModelTests.swift in Sources */, 3706409022A9BE9400889CBD /* DateFormatterTests.swift in Sources */, @@ -8136,7 +7726,6 @@ 1996AA322A5F3A3200AE2ED0 /* Stripe+PaymentMethod.swift in Sources */, D69C552F23A0609600B0987A /* ErroredBackingViewViewModelTests.swift in Sources */, 373AB25B222A063500769FC2 /* CreatePasswordViewModelTests.swift in Sources */, - A7ED1FCE1E831C5C00BFFA01 /* DashboardReferrerRowStackViewViewModelTests.swift in Sources */, A7ED1FF11E831C5C00BFFA01 /* MessageThreadCellViewModelTests.swift in Sources */, 8A49395F24B53D1900C3C3CE /* RewardAddOnSelectionViewModelTests.swift in Sources */, D72370922118FEFA001EA4CA /* SettingsPrivacyViewModelTests.swift in Sources */, @@ -8146,9 +7735,7 @@ A7ED1FAA1E831C5C00BFFA01 /* DiscoveryPageViewModelTests.swift in Sources */, D7180BA222EF9DE900EB0110 /* RewardCellProjectBackingStateTypeTests.swift in Sources */, D72370542118C19B001EA4CA /* SettingsRecommendationsCellViewModelTests.swift in Sources */, - A7ED1FCC1E831C5C00BFFA01 /* ProjectActivityBackingCellViewModelTests.swift in Sources */, 0602002A27D271B900909500 /* ExternalSourceViewElementCellViewModelTests.swift in Sources */, - A7ED1FED1E831C5C00BFFA01 /* DashboardProjectsDrawerCellViewModelTests.swift in Sources */, A7ED1FE01E831C5C00BFFA01 /* DiscoveryNavigationHeaderViewModelTests.swift in Sources */, A7ED1F481E831BA200BFFA01 /* AlertError+Equatable.swift in Sources */, 7720BA7022E0DBD10071FDA1 /* InstantiableTests.swift in Sources */, @@ -8158,17 +7745,14 @@ A7ED1FFA1E831C5C00BFFA01 /* ProjectPamphletContentViewModelTests.swift in Sources */, A7ED1F381E830FDC00BFFA01 /* UIColorTests.swift in Sources */, 77C7B654226E0E54001101AC /* RewardsCollectionViewModelTests.swift in Sources */, - A7ED1FBA1E831C5C00BFFA01 /* ProjectActivityCommentCellViewModelTests.swift in Sources */, D62B14B02212184500AC05C8 /* DeletePaymentMethodEnvelopeTests.swift in Sources */, 6067BCF2293FC3520036ABB1 /* FacebookResetPasswordViewModelTests.swift in Sources */, 771F388A2422C961009036A0 /* PersonalizationCellViewModelTests.swift in Sources */, 94BA16E926698EF00034CC3F /* CommentTableViewFooterViewModelTests.swift in Sources */, D660078A2410310400AC1EDB /* CategorySelectionHeaderViewModelTests.swift in Sources */, - A7ED1FD01E831C5C00BFFA01 /* DashboardActionCellViewModelTests.swift in Sources */, A7ED1FE21E831C5C00BFFA01 /* DiscoveryViewModelTests.swift in Sources */, A7ED1FF81E831C5C00BFFA01 /* VideoViewModelTests.swift in Sources */, 06AF787A2710E5E1009587F1 /* ProjectPageViewModelTests.swift in Sources */, - A7ED1FCD1E831C5C00BFFA01 /* ProjectActivityUpdateCellViewModelTests.swift in Sources */, 477239792710FABB00D26CDA /* ProjectNavigationSelectorViewModelTests.swift in Sources */, A7A6261B1E85B936004C931A /* BackerDashboardProjectsViewModelTests.swift in Sources */, 8A213CF7239EB22900BBB4C7 /* MockTrackingClient.swift in Sources */, @@ -8178,7 +7762,6 @@ A7A6261A1E85B936004C931A /* BackerDashboardViewModelTests.swift in Sources */, 37EB3E4E228CF4FB00076E4C /* NumberFormatterTests.swift in Sources */, 8AE09C3E24463C4C00036043 /* PledgeDisclaimerViewModelTests.swift in Sources */, - A7ED1FB21E831C5C00BFFA01 /* DashboardTitleViewViewModelTests.swift in Sources */, 06BD75CB26C431B400A12D4E /* CommentDialogViewModelTests.swift in Sources */, A7ED1F3A1E830FDC00BFFA01 /* UILabel+SimpleHTMLTests.swift in Sources */, D6ED1B37216D0C64007F7547 /* ChangeEmailViewModelTests.swift in Sources */, @@ -8198,7 +7781,6 @@ A7ED1F2A1E830FDC00BFFA01 /* FormatTests.swift in Sources */, 94BE15CE25E970A3007CD9A4 /* TrackingHelpersTests.swift in Sources */, 3767EDB322CFFF380088E8E4 /* ShippingRulesViewModelTests.swift in Sources */, - A7ED1FF21E831C5C00BFFA01 /* ProjectActivityLaunchCellViewModelTests.swift in Sources */, A7ED1FEF1E831C5C00BFFA01 /* LoginViewModelTests.swift in Sources */, A7ED1FF31E831C5C00BFFA01 /* ProjectCreatorViewModelTests.swift in Sources */, 473DE01A273C757D0033331D /* ProjectRisksDisclaimerCellViewModelTests.swift in Sources */, @@ -8246,7 +7828,6 @@ D6C9A20E1F755FE200981E64 /* GraphSchemaTests.swift in Sources */, D6600790241A7C0000AC1EDB /* CuratedProjectsViewModelTests.swift in Sources */, A7ED1F4A1E831BA200BFFA01 /* MockBundle.swift in Sources */, - A7ED1FD81E831C5C00BFFA01 /* DashboardRewardsCellViewModelTests.swift in Sources */, D04AACA5218BB72100CF713E /* BetaToolsViewModelTests.swift in Sources */, 1996AA332A5F477B00AE2ED0 /* PaymentMethodsViewModelTests.swift in Sources */, 473DE018273C74BA0033331D /* ProjectRisksCellViewModelTests.swift in Sources */, @@ -8255,7 +7836,6 @@ 8A4DDAB92374DF9F00ADE31D /* String+AttributedTests.swift in Sources */, D65BF353233023E500B15B25 /* ManagePledgeSummaryViewModelTests.swift in Sources */, 064E10BF27CF038300A9A75F /* AudioVideoViewElementCellViewModelTests.swift in Sources */, - A7ED1FC71E831C5C00BFFA01 /* DashboardFundingCellViewModelTests.swift in Sources */, A7ED1FD51E831C5C00BFFA01 /* FindFriendsStatsCellViewModelTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -8269,7 +7849,6 @@ 4791BDE42717604C00DFE5D5 /* ProjectFAQsCell.swift in Sources */, 019DDFED1CB6FF4500BDC113 /* ResetPasswordViewController.swift in Sources */, 20ABFEE4264EE06400AA48EC /* CommentInputTextView.swift in Sources */, - 018422C51D2C48AA00CA7566 /* DashboardProjectsDrawerDataSource.swift in Sources */, 7061848B29BE4CD8008F9941 /* MessageBannerView.swift in Sources */, D0237C1522BC2B640092C792 /* PledgeSummaryViewController.swift in Sources */, 015706BB1E68DE580087DD68 /* ProfileSortBarView.swift in Sources */, @@ -8281,7 +7860,6 @@ 0169F9861D6F4E1D00C8D5C5 /* DiscoveryFiltersViewController.swift in Sources */, A7CC14421D00E74F00035C52 /* FindFriendsStatsCell.swift in Sources */, 77A3C53B219CCEBF00824FC1 /* SettingsAccountDataSource.swift in Sources */, - 015A06F71D219513007AE210 /* DashboardRewardsCell.swift in Sources */, A7E315C61C88AAA8000DD85A /* DiscoveryProjectsDataSource.swift in Sources */, 7758A8372097BADF0018B96D /* DiscoveryProjectCategoryView.swift in Sources */, 59673CBD1D50ED380035AFD9 /* VideoViewController.swift in Sources */, @@ -8304,10 +7882,7 @@ 8AA3DB30250AB4AB009AC8EA /* UIAlertController.swift in Sources */, 776B6F70215183A400AB0652 /* ChangeEmailViewController.swift in Sources */, 47AC212F264AD2590090AEDF /* CommentsViewController.swift in Sources */, - 0170E7701D25C55200E2CCE4 /* ProjectActivityCommentCell.swift in Sources */, - 9D14FF901D133351005F4ABB /* ProjectActivityNegativeStateChangeCell.swift in Sources */, 3742036D22DD41BA007AA86E /* PillLayout.swift in Sources */, - 015A07461D247564007AE210 /* UpdateDraftViewController.swift in Sources */, 06E3296B270E39B300216306 /* ProjectPageNavigationBarView.swift in Sources */, 7720BA6E22DE79BF0071FDA1 /* UIViewController+Presentation.swift in Sources */, 701160D4291ECB9F0095BF24 /* LoadingBarButtonItem.swift in Sources */, @@ -8326,7 +7901,6 @@ A74FFDF01CE3E33300C7BCB9 /* MessageDialogViewController.swift in Sources */, 8A2A2E2E238B3CBE002C62E0 /* UIView+ShimmerLoading.swift in Sources */, 01515F8C1E1D6E0C00FDECB6 /* MessageThreadEmptyStateCell.swift in Sources */, - 9D525F101D4158BA003CAE04 /* ProjectActivityDateCell.swift in Sources */, 77636FC1243CFE25007F67B9 /* ProcessingView.swift in Sources */, 0157067D1E65F0420087DD68 /* BackerDashboardProjectsViewController.swift in Sources */, 6031A8462AB2485800F8184D /* ReportProjectInfoView.swift in Sources */, @@ -8334,7 +7908,6 @@ 8A4DDAB32373427000ADE31D /* PledgeStatusLabelView.swift in Sources */, 60368E662AC334B9005EE9A5 /* ReportProjectFormView.swift in Sources */, D6AE3F9220EA977D00DB212F /* SettingsNotificationsViewController.swift in Sources */, - 59019FBA1D21ABD200EAEC9D /* DashboardReferrerRowStackView.swift in Sources */, 8A8099F822E2156E00373E66 /* RewardPledgeNavigationController.swift in Sources */, 8A45169324B3E50600D8CAEF /* RewardAddOnSelectionDataSource.swift in Sources */, 015102AD1F1947C50006C0FC /* MessageThreadsDataSource.swift in Sources */, @@ -8342,18 +7915,14 @@ 3742036F22DD45A2007AA86E /* PillCell.swift in Sources */, 59AE35E21D67643100A310E6 /* DiscoveryPostcardCell.swift in Sources */, 37096C3422BC24DD003D1F40 /* UIFeedbackGeneratorType.swift in Sources */, - 59B0DFC51D11AC850081D2DC /* DashboardDataSource.swift in Sources */, - A757EABD1D19FAEE00A5C978 /* ProjectActivitiesViewController.swift in Sources */, 37CA16B123304DCA006044F9 /* ToggleViewController.swift in Sources */, A745D0221CA897FF00C12802 /* SearchViewController.swift in Sources */, A72C3AAA1D00F96C0075227E /* DiscoveryPageViewController.swift in Sources */, 4751A679272B56C400F81DD5 /* ProjectTabDisclaimerCell.swift in Sources */, - A745D0481CA8985B00C12802 /* DashboardViewController.swift in Sources */, 94114D602653135E0063E8F6 /* CommentRemovedCell.swift in Sources */, D6CCD8A523A94C6200CFD5FF /* ActivityErroredBackingsCell.swift in Sources */, D6B6766B20FF8F890082717D /* SettingsNewslettersCell.swift in Sources */, 77320F8A24411459004C631E /* SettingsAccountHeaderView.swift in Sources */, - 015A06F41D219156007AE210 /* DashboardRewardRowStackView.swift in Sources */, 4724A0A4268F6123000EB7A9 /* ViewMoreRepliesCell.swift in Sources */, 379CFFFE2242DAF400F6F0C2 /* Storyboard.swift in Sources */, A72C3AA41D00F7170075227E /* DiscoveryPagesDataSource.swift in Sources */, @@ -8363,7 +7932,6 @@ 8AA3DB33250AE40C009AC8EA /* SettingsViewModel.swift in Sources */, 771E630B23425977005967E8 /* CancelPledgeViewController.swift in Sources */, 06DC4144266FF184005205F7 /* RootCommentCell.swift in Sources */, - 60DF50982A434E75002C771F /* DashboardDeprecationView.swift in Sources */, A75AB2231C8A85D1002FC3E6 /* ActivityUpdateCell.swift in Sources */, 777C60A6241FECD800820C59 /* PersonalizationCell.swift in Sources */, 370C8B64234FCC6F00DE75DD /* LoadingButton.swift in Sources */, @@ -8399,12 +7967,10 @@ 60AE9F062ABB897900FB3A96 /* ReportProjectInfoListItem.swift in Sources */, 47F4CA63267A7B2300356DBF /* RemoteConfigFeatureFlagToolsViewController.swift in Sources */, A7CA8BB71D8F14260086A3E9 /* ProjectPamphletMainCell.swift in Sources */, - 593AC5CF1D33F4BF002613F4 /* DashboardFundingCell.swift in Sources */, A71003E31CDD077200B4F4D7 /* MessageCell.swift in Sources */, 01B3B0301E78890800B8BF46 /* BackerDashboardPagesDataSource.swift in Sources */, 8A29790C234EA7A70039396D /* PledgeAmountSummaryViewController.swift in Sources */, 8A13D169249817BF007E2C0B /* PledgeExpandableHeaderRewardCell.swift in Sources */, - 8072F44F1D46BAA400999EF1 /* UpdatePreviewViewModel.swift in Sources */, A7C795B31C873AC90081977F /* ActivitiesViewController.swift in Sources */, A731BF8D1D1EE44E00A734AC /* UpdateViewController.swift in Sources */, 7727494923FC8C2A0065E9F2 /* CategorySelectionDataSource.swift in Sources */, @@ -8431,7 +7997,6 @@ 473DE014273C551C0033331D /* ProjectRisksDisclaimerCell.swift in Sources */, D6C3845B210B9AC400ADB671 /* SettingsNewslettersTopCell.swift in Sources */, D6534D3822E784B500E9D279 /* PledgePaymentMethodCell.swift in Sources */, - 595CDAB81D3537180051C816 /* FundingGraphView.swift in Sources */, 777C67BB220A0FA5009F8D42 /* SettingsTableViewHeader.swift in Sources */, A72C3AB01D00F9C10075227E /* DiscoveryFiltersDataSource.swift in Sources */, A757EBB31D1B084F00A5C978 /* DiscoveryOnboardingCell.swift in Sources */, @@ -8439,23 +8004,17 @@ 19462F6A29D4892300868694 /* ChangeEmailView.swift in Sources */, 8A08B521249C3352009CF74E /* EasyButton.swift in Sources */, 20C186ED26726612008046D8 /* CommentsDataSource.swift in Sources */, - 018422BD1D2C484200CA7566 /* DashboardProjectsDrawerCell.swift in Sources */, - 9D14FF931D133351005F4ABB /* ProjectActivityUpdateCell.swift in Sources */, 3767EDA722CFFE6C0088E8E4 /* ShippingRulesDataSource.swift in Sources */, - 01A120D31D2D6E6200B42F73 /* DashboardProjectsDrawerViewController.swift in Sources */, 770E441321137E7400396D46 /* SettingsNotificationPickerCell.swift in Sources */, 06232D392795D26600A81755 /* TextViewElementCell.swift in Sources */, - 59B0E0041D1203970081D2DC /* DashboardActionCell.swift in Sources */, D703FC6920F7E2EC004A272D /* SettingsPrivacyViewController.swift in Sources */, 37D6722F221E61D300B6D415 /* SettingsFooterView.swift in Sources */, 0148EF911CDD2879000DEFF8 /* ThanksViewController.swift in Sources */, 77D19FFD24085A170058FC8E /* CategoryCollectionViewSectionHeaderView.swift in Sources */, D65BF34F232C1A1C00B15B25 /* ManagePledgeSummaryViewController.swift in Sources */, 8A4841B22487147200246223 /* ManagePledgeDataSource.swift in Sources */, - 8072F41D1D46B75200999EF1 /* UpdatePreviewViewController.swift in Sources */, 015706161E649DEE0087DD68 /* BackerDashboardViewController.swift in Sources */, A749001F1D00E27100BC3BE7 /* SignupViewController.swift in Sources */, - 59B0DFFE1D11B2E50081D2DC /* DashboardContextCell.swift in Sources */, 0634C2F727CFEE40003A6D6E /* ExternalSourceViewElementCell.swift in Sources */, 47F95ED72672C594001365B2 /* ViewRepliesView.swift in Sources */, 37CA16AD23300376006044F9 /* ManageViewPledgeRewardReceivedViewController.swift in Sources */, @@ -8472,7 +8031,6 @@ D79F0F3721028C2600D3B32C /* SettingsPrivacyRecommendationCell.swift in Sources */, D6B6766920FF8D850082717D /* SettingsNewslettersDataSource.swift in Sources */, 4751A675272B317500F81DD5 /* ProjectTabCategoryDescriptionCell.swift in Sources */, - 59D1E6261D1865AC00896A4C /* DashboardVideoCell.swift in Sources */, 06EB4E3127B5D32000D8BFCC /* PinchToZoom.swift in Sources */, 60C996E42ABCA5E5006BE4F4 /* ReportProjectLabelView.swift in Sources */, 60C996E42ABCA5E5006BE4F4 /* ReportProjectLabelView.swift in Sources */, @@ -8483,11 +8041,9 @@ 77C5E252214182CA002E1670 /* SettingsPrivacySwitchCell.swift in Sources */, A7C795B41C873AC90081977F /* DiscoveryViewController.swift in Sources */, 3767EDAB22CFFED40088E8E4 /* ShippingRuleCell.swift in Sources */, - 5955E64F1D21800300B4153D /* DashboardReferrersCell.swift in Sources */, 47C500712696481300BB4BF2 /* CommentViewMoreRepliesFailedCell.swift in Sources */, 8ACB32A824ABC2DB00A03968 /* RewardAddOnSelectionViewController.swift in Sources */, D63BBCD0217E5460007E01F0 /* PaymentMethodsViewController.swift in Sources */, - 9D14FF8D1D133351005F4ABB /* ProjectActivityBackingCell.swift in Sources */, 379CFFFC2242DAE800F6F0C2 /* Nib.swift in Sources */, A7CC14451D00E75F00035C52 /* FindFriendsDataSource.swift in Sources */, A7808BBE1D6240B9001CF96A /* ProjectCreatorViewController.swift in Sources */, @@ -8496,7 +8052,6 @@ 19C8E56929A9249D007C3504 /* TextInputFieldModifiers.swift in Sources */, D6B6766720FF8D3C0082717D /* SettingsNewslettersViewController.swift in Sources */, 77E6440320F65074005F6B38 /* HelpViewController.swift in Sources */, - A757EAEE1D19FAFA00A5C978 /* ProjectActivitiesDataSource.swift in Sources */, 8ACD29F7243E48260044BC17 /* PledgePaymentMethodsDataSource.swift in Sources */, 77EFBAA62268D32000DA5C3C /* RewardsCollectionViewController.swift in Sources */, 8A13D15F249559CB007E2C0B /* PledgeExpandableRewardsHeaderViewController.swift in Sources */, @@ -8509,8 +8064,8 @@ 597073A01D07277100B00444 /* ProjectNotificationsDataSource.swift in Sources */, A757E9EF1D19C37F00A5C978 /* ActivitySurveyResponseCell.swift in Sources */, D63BBCDA217E7802007E01F0 /* CreditCardCell.swift in Sources */, - 9D14FF921D133351005F4ABB /* ProjectActivitySuccessCell.swift in Sources */, 473632A426EBAC6A001B0D39 /* RiskMessagingViewController.swift in Sources */, + 191E601F2A93F318001413B2 /* ProjectTabQuestionAnswerCell.swift in Sources */, 191E601F2A93F318001413B2 /* ProjectTabAIGenerationCell.swift in Sources */, 018422BE1D2C484700CA7566 /* DashboardTitleView.swift in Sources */, 5970739A1D06346700B00444 /* ProjectNotificationsViewController.swift in Sources */, @@ -8520,13 +8075,11 @@ 8A07CF1226604E8F00426B1C /* CommentRepliesViewController.swift in Sources */, 77EFBAE52268E01400DA5C3C /* RewardCell.swift in Sources */, 77FD8B46216D6245000A95AC /* LoadingBarButtonItemView.swift in Sources */, - 9D14FF8E1D133351005F4ABB /* ProjectActivityEmptyStateCell.swift in Sources */, 017508161D67A4E300BB1863 /* DiscoveryNavigationHeaderViewController.swift in Sources */, 064B007627A462AA007B21FE /* ImageViewElementCell.swift in Sources */, 8AD486352359F34700A1463E /* STPPaymentHandler+StripePaymentHandlerActionStatus.swift in Sources */, D69C553123A17A0700B0987A /* ActivityErroredBackingsCellHeader.swift in Sources */, 014D625B1E6E20BB0033D2BD /* BackerDashboardProjectsDataSource.swift in Sources */, - A78012651D2EEA620027396E /* ReferralChartView.swift in Sources */, 597073B21D07281800B00444 /* ProjectNotificationCell.swift in Sources */, 8A49396B24B7735100C3C3CE /* RewardAddOnCardView.swift in Sources */, 01DEFB961CB44A5D003709C0 /* TwoFactorViewController.swift in Sources */, @@ -8564,7 +8117,6 @@ 8A8C6134243F99AB0092B682 /* ContentSizeTableView.swift in Sources */, 8A45169524B3E8FB00D8CAEF /* RewardAddOnCell.swift in Sources */, 8A13D163249566B8007E2C0B /* PledgeExpandableHeaderRewardHeaderCell.swift in Sources */, - 9D14FF8F1D133351005F4ABB /* ProjectActivityLaunchCell.swift in Sources */, 8A13D1672495687E007E2C0B /* PledgeExpandableRewardsHeaderDataSource.swift in Sources */, 8A32FE0424D2336E00F79C72 /* EmpyStateView.swift in Sources */, 379CFFFB2242DADB00F6F0C2 /* MFMailComposeViewController.swift in Sources */, @@ -8594,8 +8146,6 @@ A783560A1E85BE890021DA5A /* BackerDashboardProjectsViewControllerTests.swift in Sources */, 77941D2E216FCB0100398B89 /* ChangePasswordViewControllerTests.swift in Sources */, A7ED20661E83256700BFFA01 /* UpdateViewModelTests.swift in Sources */, - A7ED204E1E8323E900BFFA01 /* ProjectActivityViewControllerTests.swift in Sources */, - A7ED201B1E83229E00BFFA01 /* ProjectActivitiesDataSourceTests.swift in Sources */, 3767EDAF22CFFF010088E8E4 /* ShippingRulesTableViewControllerTests.swift in Sources */, 37DEC2242257CB650051EF9B /* PledgeViewControllerTests.swift in Sources */, A7ED20161E83229E00BFFA01 /* DiscoveryPagesDataSourceTests.swift in Sources */, @@ -8619,9 +8169,7 @@ 3751E42C2335E7D400047E9A /* TraitCollection.swift in Sources */, 3708DD45220A76FE00F8E569 /* CreatePasswordTableControllerTests.swift in Sources */, D72370942119139D001EA4CA /* SettingsPrivacyViewControllerTests.swift in Sources */, - A7ED20651E83256700BFFA01 /* UpdatePreviewViewModelTests.swift in Sources */, 77891BDE20CEB6DB00B46D5D /* ThanksProjectsDataSourceTests.swift in Sources */, - 1937A72628C959DD00DD732D /* FundingGraphViewTests.swift in Sources */, 19450C2628C8225200C60F97 /* SearchViewControllerTests.swift in Sources */, 209B0E3A265801D70006DB7D /* CommentComposerViewTests.swift in Sources */, 3767EDA922CFFE770088E8E4 /* ShippingRulesDataSourceTests.swift in Sources */, @@ -8661,10 +8209,8 @@ 776989AA242E747200AAC48D /* CuratedProjectsViewControllerTests.swift in Sources */, 19A97D1A28C7F0EC0031B857 /* DiscoveryNavigationHeaderViewControllerTests.swift in Sources */, 8A23EF0822F11470001262E1 /* RewardCardContainerViewTests.swift in Sources */, - A7ED20451E8323E900BFFA01 /* DashboardViewControllerTests.swift in Sources */, 1965436D28C807FB00457EC6 /* PledgePaymentMethodsViewControllerTests.swift in Sources */, D764377C224174B700DAFC9E /* SharedFunctionsTests.swift in Sources */, - A7ED20141E83229E00BFFA01 /* DashboardProjectsDrawerDataSourceTests.swift in Sources */, A7ED20431E8323E900BFFA01 /* EmptyStatesViewControllerTests.swift in Sources */, 70B1889429A521000004E293 /* FacebookResetPasswordViewControllerTests.swift in Sources */, A7ED20421E8323E900BFFA01 /* SortPagerViewControllerTests.swift in Sources */, @@ -8686,7 +8232,6 @@ 77C9122C23C5082800F3D2C9 /* MockApplePayCapable.swift in Sources */, D60C8CE12149A65000D96152 /* SettingsPrivacyDataSourceTests.swift in Sources */, 776B1A132419407700B03098 /* CategorySelectionDataSourceTests.swift in Sources */, - A7ED20131E83229E00BFFA01 /* DashboardDataSourceTests.swift in Sources */, A7ED20481E8323E900BFFA01 /* LoginViewControllerTests.swift in Sources */, 77CD8981217FA01B003066DA /* ProjectPageViewControllerConversionTests.swift in Sources */, 47CFFFFC27346034009B9510 /* ProjectPageViewControllerDataSourceTests.swift in Sources */, diff --git a/KsApi/models/Activity.swift b/KsApi/models/Activity.swift index 0e3d0aabf0..a11d6ccdfc 100644 --- a/KsApi/models/Activity.swift +++ b/KsApi/models/Activity.swift @@ -13,10 +13,6 @@ public struct Activity { public enum Category: String { case backing - case backingAmount = "backing-amount" - case backingCanceled = "backing-canceled" - case backingDropped = "backing-dropped" - case backingReward = "backing-reward" case cancellation case commentPost = "comment-post" case commentProject = "comment-project" diff --git a/Library/Navigation.swift b/Library/Navigation.swift index a789395ca0..6797a356c5 100644 --- a/Library/Navigation.swift +++ b/Library/Navigation.swift @@ -3,8 +3,6 @@ import KsApi public enum Navigation: Equatable { case checkout(Int, Navigation.Checkout) - case creatorMessages(Param, messageThreadId: Int) - case projectActivity(Param) case emailClick case messages(messageThreadId: Int) case profile(Profile) @@ -30,7 +28,6 @@ public enum Navigation: Equatable { case discovery([String: String]?) case search case activity - case dashboard(project: Param?) case login case me } @@ -138,7 +135,6 @@ private let allRoutes: [String: (RouteParamsDecoded) -> Navigation?] = [ "/projects/:creator_param/:project_param/checkouts/:checkout_param/thanks": thanks, "/projects/:creator_param/:project_param/comments": projectComments, "/projects/:creator_param/:project_param/creator_bio": creatorBio, - "/projects/:creator_param/:project_param/dashboard": dashboard, "/projects/:creator_param/:project_param/description": project, "/projects/:creator_param/:project_param/faqs": faqs, "/projects/:creator_param/:project_param/friends": friends, @@ -170,7 +166,6 @@ private let deepLinkRoutes: [String: (RouteParamsDecoded) -> Navigation?] = allR "/profile/verify_email", "/projects/:creator_param/:project_param", "/projects/:creator_param/:project_param/comments", - "/projects/:creator_param/:project_param/dashboard", "/projects/:creator_param/:project_param/posts", "/projects/:creator_param/:project_param/posts/:update_param", "/projects/:creator_param/:project_param/posts/:update_param/comments", @@ -335,15 +330,6 @@ private func creatorBio(_ params: RouteParamsDecoded) -> Navigation? { return nil } -private func dashboard(_ params: RouteParamsDecoded) -> Navigation? { - if let projectParam = params.projectParam() { - let dashboard = Navigation.Tab.dashboard(project: projectParam) - return .tab(dashboard) - } - - return nil -} - private func faqs(_ params: RouteParamsDecoded) -> Navigation? { if let projectParam = params.projectParam() { let refTag = params.refTag() diff --git a/Library/NavigationTests.swift b/Library/NavigationTests.swift index 96f1a8b78b..6b6f9049f0 100644 --- a/Library/NavigationTests.swift +++ b/Library/NavigationTests.swift @@ -208,11 +208,6 @@ public final class NavigationTests: XCTestCase { "/projects/creator/project/messages/new" ) - KSRAssertMatch( - .tab(.dashboard(project: .slug("project"))), - "/projects/creator/project/dashboard" - ) - KSRAssertMatch( .user(.slug("self"), .survey(3)), "/users/self/surveys/3" diff --git a/Library/RefTag.swift b/Library/RefTag.swift index e9e10cd508..931d97b94d 100644 --- a/Library/RefTag.swift +++ b/Library/RefTag.swift @@ -7,7 +7,6 @@ public enum RefTag { case categoryFeatured case categoryWithSort(DiscoveryParams.Sort) case city - case dashboard case dashboardActivity case discovery case discoveryWithSort(DiscoveryParams.Sort) @@ -52,7 +51,6 @@ public enum RefTag { case "category_newest": self = .categoryWithSort(.newest) case "category_popular": self = .categoryWithSort(.popular) case "city": self = .city - case "dashboard": self = .dashboard case "dashboard_activity": self = .dashboardActivity case "discovery": self = .discovery case "discovery_ending_soon": self = .discoveryWithSort(.endingSoon) @@ -110,8 +108,6 @@ public enum RefTag { return "category" + sortRefTagSuffix(sort) case .city: return "city" - case .dashboard: - return "dashboard" case .dashboardActivity: return "dashboard_activity" case .discovery: diff --git a/Library/RefTagTests.swift b/Library/RefTagTests.swift index ae999bfc6a..7a4a99345d 100644 --- a/Library/RefTagTests.swift +++ b/Library/RefTagTests.swift @@ -10,7 +10,6 @@ public final class RefTagTests: XCTestCase { XCTAssertEqual("category", RefTag.category.stringTag) XCTAssertEqual("category_featured", RefTag.categoryFeatured.stringTag) XCTAssertEqual("city", RefTag.city.stringTag) - XCTAssertEqual("dashboard", RefTag.dashboard.stringTag) XCTAssertEqual("dashboard_activity", RefTag.dashboardActivity.stringTag) XCTAssertEqual("discovery", RefTag.discovery.stringTag) XCTAssertEqual("ksr_email_backer_failed_transaction", RefTag.emailBackerFailedTransaction.stringTag) @@ -38,7 +37,6 @@ public final class RefTagTests: XCTestCase { XCTAssertEqual(RefTag.category, RefTag.category) XCTAssertEqual(RefTag.categoryFeatured, RefTag.categoryFeatured) XCTAssertEqual(RefTag.city, RefTag.city) - XCTAssertEqual(RefTag.dashboard, RefTag.dashboard) XCTAssertEqual(RefTag.dashboardActivity, RefTag.dashboardActivity) XCTAssertEqual(RefTag.discovery, RefTag.discovery) XCTAssertEqual(RefTag.messageThread, RefTag.messageThread) @@ -101,7 +99,6 @@ public final class RefTagTests: XCTestCase { ) XCTAssertEqual(RefTag.city, RefTag(code: RefTag.city.stringTag)) - XCTAssertEqual(RefTag.dashboard, RefTag(code: RefTag.dashboard.stringTag)) XCTAssertEqual(RefTag.dashboardActivity, RefTag(code: RefTag.dashboardActivity.stringTag)) XCTAssertEqual(RefTag.discovery, RefTag(code: RefTag.discovery.stringTag)) diff --git a/Library/RemoteConfig/RemoteConfigFeature+Helpers.swift b/Library/RemoteConfig/RemoteConfigFeature+Helpers.swift index 4b8719d522..cab4531859 100644 --- a/Library/RemoteConfig/RemoteConfigFeature+Helpers.swift +++ b/Library/RemoteConfig/RemoteConfigFeature+Helpers.swift @@ -7,13 +7,6 @@ public func featureConsentManagementDialogEnabled() -> Bool { .isFeatureEnabled(featureKey: RemoteConfigFeature.consentManagementDialogEnabled) ?? false) } -public func featureCreatorDashboardEnabled() -> Bool { - return AppEnvironment.current.userDefaults - .remoteConfigFeatureFlags[RemoteConfigFeature.creatorDashboardEnabled.rawValue] ?? - (AppEnvironment.current.remoteConfigClient? - .isFeatureEnabled(featureKey: RemoteConfigFeature.creatorDashboardEnabled) ?? false) -} - public func featureFacebookLoginInterstitialEnabled() -> Bool { return AppEnvironment.current.userDefaults .remoteConfigFeatureFlags[RemoteConfigFeature.facebookLoginInterstitialEnabled.rawValue] ?? diff --git a/Library/RemoteConfig/RemoteConfigFeature+HelpersTests.swift b/Library/RemoteConfig/RemoteConfigFeature+HelpersTests.swift index 6520960ff1..4f3998e3fe 100644 --- a/Library/RemoteConfig/RemoteConfigFeature+HelpersTests.swift +++ b/Library/RemoteConfig/RemoteConfigFeature+HelpersTests.swift @@ -39,24 +39,6 @@ final class RemoteConfigFeatureHelpersTests: TestCase { } } - func testCreatorDashboard_RemoteConfig_FeatureFlag_True() { - let mockRemoteConfigClient = MockRemoteConfigClient() - |> \.features .~ [RemoteConfigFeature.creatorDashboardEnabled.rawValue: true] - - withEnvironment(remoteConfigClient: mockRemoteConfigClient) { - XCTAssertTrue(featureCreatorDashboardEnabled()) - } - } - - func testCreatorDashboard_RemoteConfig_FeatureFlag_False() { - let mockRemoteConfigClient = MockRemoteConfigClient() - |> \.features .~ [RemoteConfigFeature.creatorDashboardEnabled.rawValue: false] - - withEnvironment(remoteConfigClient: mockRemoteConfigClient) { - XCTAssertFalse(featureCreatorDashboardEnabled()) - } - } - func testReportThisProject_RemoteConfig_FeatureFlag_True() { let mockRemoteConfigClient = MockRemoteConfigClient() |> \.features .~ [RemoteConfigFeature.reportThisProjectEnabled.rawValue: true] diff --git a/Library/RemoteConfig/RemoteConfigFeature.swift b/Library/RemoteConfig/RemoteConfigFeature.swift index 8d8466e037..67e63d61d0 100644 --- a/Library/RemoteConfig/RemoteConfigFeature.swift +++ b/Library/RemoteConfig/RemoteConfigFeature.swift @@ -2,7 +2,6 @@ import Foundation public enum RemoteConfigFeature: String, CaseIterable { case consentManagementDialogEnabled = "consent_management_dialog" - case creatorDashboardEnabled = "creator_dashboard" case facebookLoginInterstitialEnabled = "facebook_interstitial" case reportThisProjectEnabled = "report_this_project" case useOfAIProjectTab = "use_of_ai_project_tab" @@ -12,7 +11,6 @@ extension RemoteConfigFeature: CustomStringConvertible { public var description: String { switch self { case .consentManagementDialogEnabled: return "Consent Management Dialog" - case .creatorDashboardEnabled: return "Creator Dashboard" case .facebookLoginInterstitialEnabled: return "Facebook Login Interstitial" case .reportThisProjectEnabled: return "Report This Project" case .useOfAIProjectTab: return "Use of AI Project Tab" diff --git a/Library/ShareContext.swift b/Library/ShareContext.swift index aa39054d0c..57907894e2 100644 --- a/Library/ShareContext.swift +++ b/Library/ShareContext.swift @@ -3,14 +3,12 @@ import KsApi /** An enumeration of all the place a share flow can be started. - - creatorDashboard: Sharing a creator's project from their dashboard. - discovery: Sharing a project from the discovery page. - project: Sharing a project from the project screen. - thanks: Sharing a project from the checkout-thanks screen. - update: Sharing an update from the update screen. */ public enum ShareContext { - case creatorDashboard(Project) case discovery(Project) case project(Project) case thanks(Project) @@ -32,7 +30,6 @@ public enum ShareContext { public var project: Project { switch self { - case let .creatorDashboard(project): return project case let .discovery(project): return project case let .project(project): return project case let .thanks(project): return project @@ -51,8 +48,6 @@ public enum ShareContext { extension ShareContext: Equatable { public static func == (lhs: ShareContext, rhs: ShareContext) -> Bool { switch (lhs, rhs) { - case let (.creatorDashboard(lhs), .creatorDashboard(rhs)): - return lhs == rhs case let (.discovery(lhs), .discovery(rhs)): return lhs == rhs case let (.project(lhs), .project(rhs)): @@ -61,7 +56,7 @@ extension ShareContext: Equatable { return lhs == rhs case let (.update(lhsProject, lhsUpdate), .update(rhsProject, rhsUpdate)): return lhsProject == rhsProject && lhsUpdate == rhsUpdate - case (.creatorDashboard, _), (.discovery, _), (.project, _), (.thanks, _), (.update, _): + case (.discovery, _), (.project, _), (.thanks, _), (.update, _): return false } } diff --git a/Library/ShortcutItem.swift b/Library/ShortcutItem.swift index 88a7bd466f..3cb9fc0533 100644 --- a/Library/ShortcutItem.swift +++ b/Library/ShortcutItem.swift @@ -1,13 +1,10 @@ public enum ShortcutItem { - case creatorDashboard case projectsWeLove case recommendedForYou case search public init?(typeString: String) { switch typeString { - case "creator_dashboard": - self = .creatorDashboard case "projects_we_love": self = .projectsWeLove case "recommended_for_you": @@ -21,8 +18,6 @@ public enum ShortcutItem { public var typeString: String { switch self { - case .creatorDashboard: - return "creator_dashboard" case .projectsWeLove: return "projects_we_love" case .recommendedForYou: @@ -36,8 +31,9 @@ public enum ShortcutItem { extension ShortcutItem: Equatable {} public func == (lhs: ShortcutItem, rhs: ShortcutItem) -> Bool { switch (lhs, rhs) { - case (.creatorDashboard, .creatorDashboard), (.projectsWeLove, .projectsWeLove), - (.recommendedForYou, .recommendedForYou), (.search, .search): + case (.projectsWeLove, .projectsWeLove), + (.recommendedForYou, .recommendedForYou), + (.search, .search): return true default: return false diff --git a/Library/Styles/TabBarItemStyles.swift b/Library/Styles/TabBarItemStyles.swift index 7118b58c22..96d6ac4e37 100644 --- a/Library/Styles/TabBarItemStyles.swift +++ b/Library/Styles/TabBarItemStyles.swift @@ -11,13 +11,11 @@ private let paddingY: CGFloat = 6.0 private let baseTabBarItemStyle = UITabBarItem.lens.title .~ nil -public func activityTabBarItemStyle(isMember _: Bool) -> (UITabBarItem) -> UITabBarItem { - return baseTabBarItemStyle - <> UITabBarItem.lens.title %~ { _ in Strings.tabbar_activity() } - <> UITabBarItem.lens.image .~ image(named: "tabbar-icon-activity") - <> UITabBarItem.lens.selectedImage .~ image(named: "tabbar-icon-activity-selected") - <> UITabBarItem.lens.accessibilityLabel %~ { _ in Strings.tabbar_activity() } -} +public let activityTabBarItemStyle = baseTabBarItemStyle + <> UITabBarItem.lens.title %~ { _ in Strings.tabbar_activity() } + <> UITabBarItem.lens.image .~ image(named: "tabbar-icon-activity") + <> UITabBarItem.lens.selectedImage .~ image(named: "tabbar-icon-activity-selected") + <> UITabBarItem.lens.accessibilityLabel %~ { _ in Strings.tabbar_activity() } public let dashboardTabBarItemStyle = baseTabBarItemStyle <> UITabBarItem.lens.title %~ { _ in Strings.tabbar_dashboard() } @@ -25,15 +23,13 @@ public let dashboardTabBarItemStyle = baseTabBarItemStyle <> UITabBarItem.lens.selectedImage .~ image(named: "tabbar-icon-dashboard-selected") <> UITabBarItem.lens.accessibilityLabel %~ { _ in Strings.tabbar_dashboard() } -public func homeTabBarItemStyle(isMember _: Bool) -> (UITabBarItem) -> UITabBarItem { - return baseTabBarItemStyle - <> UITabBarItem.lens.title %~ { _ in Strings.Explore() } - <> UITabBarItem.lens.image .~ image(named: "tabbar-icon-home") - <> UITabBarItem.lens.selectedImage .~ image(named: "tabbar-icon-home-selected") - <> UITabBarItem.lens.accessibilityLabel %~ { _ in Strings.Explore() } -} +public let homeTabBarItemStyle = baseTabBarItemStyle + <> UITabBarItem.lens.title %~ { _ in Strings.Explore() } + <> UITabBarItem.lens.image .~ image(named: "tabbar-icon-home") + <> UITabBarItem.lens.selectedImage .~ image(named: "tabbar-icon-home-selected") + <> UITabBarItem.lens.accessibilityLabel %~ { _ in Strings.Explore() } -public func profileTabBarItemStyle(isLoggedIn: Bool, isMember _: Bool) -> (UITabBarItem) -> UITabBarItem { +public func profileTabBarItemStyle(isLoggedIn: Bool) -> (UITabBarItem) -> UITabBarItem { let imageName = isLoggedIn ? "tabbar-icon-profile-logged-in" : "tabbar-icon-profile-logged-out" let accLabel = isLoggedIn ? Strings.tabbar_profile() : Strings.tabbar_login() diff --git a/Library/Tracking/KSRAnalytics.swift b/Library/Tracking/KSRAnalytics.swift index 52af915a44..52a6edeff1 100644 --- a/Library/Tracking/KSRAnalytics.swift +++ b/Library/Tracking/KSRAnalytics.swift @@ -768,65 +768,6 @@ public final class KSRAnalytics { ) } - // MARK: - Creator Dashboard Events - - /** - Call when the creator dashboard page is viewed and the first page is loaded. - */ - - public func trackCreatorDashboardPageViewed() { - let props = contextProperties( - ctaContext: .creatorDashboard, - page: .creatorDashboard, - sectionContext: .dashboard - ) - self.track(event: SegmentEvent.pageViewed.rawValue, properties: props) - } - - /** - Call when a project is switched via dropdown at the top of the creator dashboard - - - parameter project: The `Project` that the creator switched to. - - parameter refTag: The ref tag used when switching projects. - */ - - public func trackCreatorDashboardSwitchProjectClicked(project: Project, refTag: RefTag?) { - let props = projectProperties(from: project, loggedInUser: self.loggedInUser) - .withAllValuesFrom(contextProperties( - ctaContext: .projectSelect, - page: .creatorDashboard, - sectionContext: .dashboard - )) - - self.track( - event: SegmentEvent.ctaClicked.rawValue, - properties: props, - refTag: refTag?.stringTag - ) - } - - /** - Call when 'Post Update' is tapped in the creator dashboard - - - parameter project: The `Project` corresponding to the update. - - parameter refTag: The ref tag used when switching projects. - */ - - public func trackCreatorDashboardPostUpdateClicked(project: Project, refTag: RefTag?) { - let props = projectProperties(from: project, loggedInUser: self.loggedInUser) - .withAllValuesFrom(contextProperties( - ctaContext: .projectUpdateCreateDraft, - page: .creatorDashboard, - sectionContext: .dashboard - )) - - self.track( - event: SegmentEvent.ctaClicked.rawValue, - properties: props, - refTag: refTag?.stringTag - ) - } - // MARK: - Pledge Events public func trackAddOnsContinueButtonClicked( @@ -1528,9 +1469,6 @@ private func properties( result["share_type"] = shareActivityType.flatMap(shareTypeProperty) switch shareContext { - case let .creatorDashboard(project): - result = result.withAllValuesFrom(projectProperties(from: project, loggedInUser: loggedInUser)) - result["context"] = "creator_dashboard" case let .discovery(project): result = result.withAllValuesFrom(projectProperties(from: project, loggedInUser: loggedInUser)) result["context"] = "discovery" diff --git a/Library/Tracking/KSRAnalyticsTests.swift b/Library/Tracking/KSRAnalyticsTests.swift index 2761b8292e..a05e61dfd3 100644 --- a/Library/Tracking/KSRAnalyticsTests.swift +++ b/Library/Tracking/KSRAnalyticsTests.swift @@ -482,54 +482,6 @@ final class KSRAnalyticsTests: TestCase { XCTAssertEqual("Art", segmentClientProperties?["project_category"] as? String) } - // MARK: - Creator Dashboard Properties Tests - - func testCreatorDashboardPageViewed() { - let segmentClient = MockTrackingClient() - let loggedInUser = User.template |> \.id .~ 23 - let ksrAnalytics = KSRAnalytics( - loggedInUser: loggedInUser, - segmentClient: segmentClient, appTrackingTransparency: self.appTrackingTransparency - ) - - ksrAnalytics.trackCreatorDashboardPageViewed() - - XCTAssertEqual(["Page Viewed"], segmentClient.events) - XCTAssertEqual(["creator_dashboard"], segmentClient.properties(forKey: "context_page")) - XCTAssertEqual(["dashboard"], segmentClient.properties(forKey: "context_section")) - } - - func testCreatorDashboardSwitchProjectClicked() { - let segmentClient = MockTrackingClient() - let ksrAnalytics = KSRAnalytics( - segmentClient: segmentClient, - appTrackingTransparency: self.appTrackingTransparency - ) - - ksrAnalytics.trackCreatorDashboardSwitchProjectClicked(project: .template, refTag: RefTag.dashboard) - - XCTAssertEqual(["CTA Clicked"], segmentClient.events) - XCTAssertEqual(["creator_project_select"], segmentClient.properties(forKey: "context_cta")) - XCTAssertEqual(["creator_dashboard"], segmentClient.properties(forKey: "context_page")) - XCTAssertEqual(["dashboard"], segmentClient.properties(forKey: "context_section")) - XCTAssertEqual(["The Project"], segmentClient.properties(forKey: "project_name")) - } - - func testCreatorDashboardPostUpdateClicked() { - let segmentClient = MockTrackingClient() - let ksrAnalytics = KSRAnalytics( - segmentClient: segmentClient, - appTrackingTransparency: self.appTrackingTransparency - ) - - ksrAnalytics.trackCreatorDashboardPostUpdateClicked(project: .template, refTag: RefTag.dashboard) - - XCTAssertEqual(["CTA Clicked"], segmentClient.events) - XCTAssertEqual(["creator_project_update_create_draft"], segmentClient.properties(forKey: "context_cta")) - XCTAssertEqual(["creator_dashboard"], segmentClient.properties(forKey: "context_page")) - XCTAssertEqual(["dashboard"], segmentClient.properties(forKey: "context_section")) - } - // MARK: - Discovery Properties Tests func testDiscoveryProperties() { diff --git a/Library/ViewModels/DashboardActionCellViewModel.swift b/Library/ViewModels/DashboardActionCellViewModel.swift deleted file mode 100644 index 5d929dd9aa..0000000000 --- a/Library/ViewModels/DashboardActionCellViewModel.swift +++ /dev/null @@ -1,160 +0,0 @@ -import KsApi -import ReactiveExtensions -import ReactiveSwift - -public protocol DashboardActionCellViewModelInputs { - /// Call when the activity button is tapped. - func activityTapped() - - /// Call to configure cell with project value. - func configureWith(project: Project) - - /// Call when the messages button is tapped. - func messagesTapped() - - /// Call when the post update button is tapped. - func postUpdateTapped() -} - -public protocol DashboardActionCellViewModelOutputs { - /// Emits the activity button label and unseen activities count to be read by VoiceOver. - var activityButtonAccessibilityLabel: Signal { get } - - /// Emits a boolean that determines if the activity row is hidden. - var activityRowHidden: Signal { get } - - /// Emits with the project when should go to activity screen. - var goToActivity: Signal { get } - - /// Emits with the project when should go to messages screen. - var goToMessages: Signal<(), Never> { get } - - /// Emits with the project when should go to post update screen. - var goToPostUpdate: Signal { get } - - /// Emits the last update published time to display. - var lastUpdatePublishedAt: Signal { get } - - /// Emits a boolean that determines if the last update published label should be hidden. - var lastUpdatePublishedLabelHidden: Signal { get } - - /// Emits the messages button label and unread messages count to be read by VoiceOver. - var messagesButtonAccessibilityLabel: Signal { get } - - /// Emits a boolean that determines if the messages row should be hidden. - var messagesRowHidden: Signal { get } - - /// Emits a boolean that determines if the post update button should be hidden - var postUpdateButtonHidden: Signal { get } - - /// Emits the last update published at value to be read by VoiceOver. - var postUpdateButtonAccessibilityValue: Signal { get } - - /// Emits the count of unread messages to be displayed. - var unreadMessagesCount: Signal { get } - - /// Emits a boolean that determines if the unread messages indicator should be hidden. - var unreadMessagesCountHidden: Signal { get } - - /// Emits the count of unseen activities to be displayed. - var unseenActivitiesCount: Signal { get } - - /// Emits a boolean that determines if the unseen activities indicator should be hidden. - var unseenActivitiesCountHidden: Signal { get } -} - -public protocol DashboardActionCellViewModelType { - var inputs: DashboardActionCellViewModelInputs { get } - var outputs: DashboardActionCellViewModelOutputs { get } -} - -public final class DashboardActionCellViewModel: DashboardActionCellViewModelInputs, - DashboardActionCellViewModelOutputs, DashboardActionCellViewModelType { - public init() { - let project = self.projectProperty.signal.skipNil() - - self.goToActivity = project.takeWhen(self.activityTappedProperty.signal) - - self.goToMessages = project.ignoreValues().takeWhen(self.messagesTappedProperty.signal) - - self.goToPostUpdate = project.takeWhen(self.postUpdateTappedProperty.signal) - - self.lastUpdatePublishedAt = project - .map { project in - if let lastUpdatePublishedAt = project.memberData.lastUpdatePublishedAt { - return Strings.dashboard_post_update_button_subtitle_last_updated_on_date( - date: Format.date(secondsInUTC: lastUpdatePublishedAt, timeStyle: .none) - ) - } - - if .some(project.creator) == AppEnvironment.current.currentUser { - return Strings.dashboard_post_update_button_subtitle_you_have_not_posted_an_update_yet() - } else { - return Strings.No_one_has_posted_an_update_yet() - } - } - - self.postUpdateButtonAccessibilityValue = self.lastUpdatePublishedAt - - self.unreadMessagesCount = project.map { Format.wholeNumber($0.memberData.unreadMessagesCount ?? 0) } - self.unreadMessagesCountHidden = project.map { ($0.memberData.unreadMessagesCount ?? 0) == 0 } - self.unseenActivitiesCount = project.map { Format.wholeNumber($0.memberData.unseenActivityCount ?? 0) } - self.unseenActivitiesCountHidden = project.map { ($0.memberData.unseenActivityCount ?? 0) == 0 } - - self.activityButtonAccessibilityLabel = self.unseenActivitiesCount - .map { - Strings.activity_navigation_title_activity() + ", " + $0 + " unseen" - } - - self.messagesButtonAccessibilityLabel = self.unreadMessagesCount - .map { - Strings.profile_buttons_messages() + ", " + $0 + " unread" - } - - self.lastUpdatePublishedLabelHidden = project.map { !$0.memberData.permissions.contains(.post) } - self.postUpdateButtonHidden = self.lastUpdatePublishedLabelHidden - - self.messagesRowHidden = project.map { $0.creator != AppEnvironment.current.currentUser } - - self.activityRowHidden = project.map { !$0.memberData.permissions.contains(.viewPledges) } - } - - fileprivate let activityTappedProperty = MutableProperty(()) - public func activityTapped() { - self.activityTappedProperty.value = () - } - - fileprivate let messagesTappedProperty = MutableProperty(()) - public func messagesTapped() { - self.messagesTappedProperty.value = () - } - - fileprivate let postUpdateTappedProperty = MutableProperty(()) - public func postUpdateTapped() { - self.postUpdateTappedProperty.value = () - } - - fileprivate let projectProperty = MutableProperty(nil) - public func configureWith(project: Project) { - self.projectProperty.value = project - } - - public let activityButtonAccessibilityLabel: Signal - public let activityRowHidden: Signal - public let goToActivity: Signal - public let goToMessages: Signal<(), Never> - public let goToPostUpdate: Signal - public let lastUpdatePublishedAt: Signal - public let lastUpdatePublishedLabelHidden: Signal - public let messagesButtonAccessibilityLabel: Signal - public let messagesRowHidden: Signal - public let postUpdateButtonAccessibilityValue: Signal - public let postUpdateButtonHidden: Signal - public let unreadMessagesCount: Signal - public let unreadMessagesCountHidden: Signal - public let unseenActivitiesCount: Signal - public let unseenActivitiesCountHidden: Signal - - public var inputs: DashboardActionCellViewModelInputs { return self } - public var outputs: DashboardActionCellViewModelOutputs { return self } -} diff --git a/Library/ViewModels/DashboardActionCellViewModelTests.swift b/Library/ViewModels/DashboardActionCellViewModelTests.swift deleted file mode 100644 index 91f6e217b7..0000000000 --- a/Library/ViewModels/DashboardActionCellViewModelTests.swift +++ /dev/null @@ -1,171 +0,0 @@ -@testable import KsApi -@testable import Library -import Prelude -import ReactiveExtensions -import ReactiveExtensions_TestHelpers -import ReactiveSwift -import XCTest - -internal final class DashboardActionCellViewModelTests: TestCase { - fileprivate let vm = DashboardActionCellViewModel() - - fileprivate let activityButtonAccessibilityLabel = TestObserver() - fileprivate let activityRowHidden = TestObserver() - fileprivate let goToActivity = TestObserver() - fileprivate let goToMessages = TestObserver<(), Never>() - fileprivate let goToPostUpdate = TestObserver() - fileprivate let lastUpdatePublishedAt = TestObserver() - fileprivate let lastUpdatePublishedLabelHidden = TestObserver() - fileprivate let messagesButtonAccessibilityLabel = TestObserver() - fileprivate let messagesRowHidden = TestObserver() - fileprivate let postUpdateButtonAccessibilityValue = TestObserver() - fileprivate let postUpdateButtonHidden = TestObserver() - - internal override func setUp() { - super.setUp() - - self.vm.outputs.activityButtonAccessibilityLabel.observe(self.activityButtonAccessibilityLabel.observer) - self.vm.outputs.activityRowHidden.observe(self.activityRowHidden.observer) - self.vm.outputs.goToActivity.observe(self.goToActivity.observer) - self.vm.outputs.goToMessages.observe(self.goToMessages.observer) - self.vm.outputs.goToPostUpdate.observe(self.goToPostUpdate.observer) - self.vm.outputs.lastUpdatePublishedAt.observe(self.lastUpdatePublishedAt.observer) - self.vm.outputs.lastUpdatePublishedLabelHidden.observe(self.lastUpdatePublishedLabelHidden.observer) - self.vm.outputs.messagesButtonAccessibilityLabel.observe(self.messagesButtonAccessibilityLabel.observer) - self.vm.outputs.messagesRowHidden.observe(self.messagesRowHidden.observer) - self.vm.outputs.postUpdateButtonAccessibilityValue - .observe(self.postUpdateButtonAccessibilityValue.observer) - self.vm.outputs.postUpdateButtonHidden.observe(self.postUpdateButtonHidden.observer) - } - - func testAccessibilityElements() { - let date = Date().timeIntervalSince1970 - let formattedDate = Format.date(secondsInUTC: date, timeStyle: .none) - let project = .template - |> Project.lens.memberData.lastUpdatePublishedAt .~ date - |> Project.lens.memberData.unreadMessagesCount .~ 10 - |> Project.lens.memberData.unseenActivityCount .~ 7 - - self.vm.inputs.configureWith(project: project) - - self.activityButtonAccessibilityLabel.assertValues(["Activity, 7 unseen"]) - self.messagesButtonAccessibilityLabel.assertValues(["Messages, 10 unread"]) - self.postUpdateButtonAccessibilityValue.assertValues(["Last updated on \(formattedDate)."]) - } - - func testActivityRowHidden_WithViewPledgePermissions() { - self.vm.inputs.configureWith( - project: .template |> Project.lens.memberData.permissions .~ [.viewPledges] - ) - - self.activityRowHidden.assertValues([false]) - } - - func testActivityRowHidden_WithoutViewPledgePermissions() { - self.vm.inputs.configureWith( - project: .template |> Project.lens.memberData.permissions .~ [] - ) - - self.activityRowHidden.assertValues([true]) - } - - func testGoToScreens() { - let project = Project.template - self.vm.inputs.configureWith(project: project) - - self.vm.inputs.activityTapped() - self.goToActivity.assertValues([project], "Go to activity screen.") - - self.vm.inputs.messagesTapped() - self.goToMessages.assertValueCount(1, "Go to messages screen.") - - self.vm.inputs.postUpdateTapped() - self.goToPostUpdate.assertValues([project], "Go to post update screen.") - } - - func testLastUpdatePublishedAtEmits() { - let date = Date().timeIntervalSince1970 - let formattedDate = Format.date(secondsInUTC: date, timeStyle: .none) - let project = .template - |> Project.lens.memberData.lastUpdatePublishedAt .~ date - - self.vm.inputs.configureWith(project: project) - self.lastUpdatePublishedAt.assertValues(["Last updated on \(formattedDate)."]) - } - - func testLastUpdatePublishedAtEmits_CollaboratorNoUpdates() { - let collaborator = User.template - |> \.id .~ 9 - let project = Project.template - - withEnvironment(currentUser: collaborator) { - vm.inputs.configureWith(project: project) - - self.lastUpdatePublishedAt.assertValues(["No one has posted an update yet."]) - } - } - - func testLastUpdatePublishedAtEmits_CreatorNoUpdates() { - let creator = User.template |> \.id .~ 42 - let project = .template |> Project.lens.creator .~ creator - - withEnvironment(currentUser: creator) { - vm.inputs.configureWith(project: project) - - self.lastUpdatePublishedAt.assertValues( - [Strings.dashboard_post_update_button_subtitle_you_have_not_posted_an_update_yet()] - ) - } - } - - func testPermissionsWithCreator() { - let creator = User.template |> \.id .~ 42 - let project = .template - |> Project.lens.creator .~ creator - |> Project.lens.memberData.permissions .~ [.post] - - withEnvironment(currentUser: creator) { - self.vm.inputs.configureWith(project: project) - - self.lastUpdatePublishedLabelHidden.assertValues([false], "Last update label is not hidden.") - self.messagesRowHidden.assertValues([false], "Messages row is not hidden.") - self.postUpdateButtonHidden.assertValues([false], "Post update button is not hidden.") - } - } - - func testPermissionsWithCollaborator() { - let creator = User.template |> \.id .~ 42 - let collaborator = User.template |> \.id .~ 99 - let project = .template - |> Project.lens.creator .~ creator - |> Project.lens.memberData.permissions .~ [.post] - - withEnvironment(currentUser: collaborator) { - self.vm.inputs.configureWith(project: project) - - self.lastUpdatePublishedLabelHidden.assertValues([false], "Last update label is not hidden.") - self.messagesRowHidden.assertValues([true], "Messages row is hidden for non-creator.") - self.postUpdateButtonHidden.assertValues([false], "Post update button is not hidden.") - } - } - - func testPermissionsWithCollaboratorWithoutPostPermission() { - let creator = User.template |> \.id .~ 42 - let collaborator = User.template |> \.id .~ 99 - let project = .template - |> Project.lens.creator .~ creator - |> Project.lens.memberData.permissions .~ [] - - withEnvironment(currentUser: collaborator) { - self.vm.inputs.configureWith(project: project) - - self.lastUpdatePublishedLabelHidden - .assertValues([true], "Last update label is hidden without post permissions.") - self.messagesRowHidden.assertValues([true], "Messages row is hidden for non-creator.") - self.postUpdateButtonHidden.assertValues( - [true], - "Post update button is hidden without post permissions." - ) - } - } -} diff --git a/Library/ViewModels/DashboardFundingCellViewModel.swift b/Library/ViewModels/DashboardFundingCellViewModel.swift deleted file mode 100644 index 28d4823b5b..0000000000 --- a/Library/ViewModels/DashboardFundingCellViewModel.swift +++ /dev/null @@ -1,194 +0,0 @@ -import KsApi -import Prelude -import ReactiveExtensions -import ReactiveSwift -import UIKit - -public struct FundingGraphData { - public let project: Project - public let stats: [ProjectStatsEnvelope.FundingDateStats] - public let yAxisTickSize: CGFloat -} - -extension FundingGraphData: Equatable {} -public func == (lhs: FundingGraphData, rhs: FundingGraphData) -> Bool { - return - lhs.project == rhs.project && - lhs.stats == rhs.stats && - lhs.yAxisTickSize == rhs.yAxisTickSize -} - -public protocol DashboardFundingCellViewModelInputs { - /// Call to configure cell with funding stats and project data. - func configureWith( - fundingDateStats stats: [ProjectStatsEnvelope.FundingDateStats], - project: Project - ) -} - -public protocol DashboardFundingCellViewModelOutputs { - /// Emits the backers count text to be displayed. - var backersText: Signal { get } - - /// Emits the relevant cell information to be spoken on VoiceOver. - var cellAccessibilityValue: Signal { get } - - /// Emits the deadline date text (e.g. Jul 26) to be displayed. - var deadlineDateText: Signal { get } - - /// Emits the disparate funding data to be displayed in the funding graph. - var graphData: Signal { get } - - /// Emits the pledged of goal text to be displayed. - var goalText: Signal { get } - - /// Emits the launch date text (e.g. Jun 26) to be displayed. - var launchDateText: Signal { get } - - /// Emits the amount pledged text to be displayed. - var pledgedText: Signal { get } - - /// Emits the time remaining (units) text to be displayed. - var timeRemainingSubtitleText: Signal { get } - - /// Emits the time remaining (value) text to be displayed. - var timeRemainingTitleText: Signal { get } - - /// Emits the bottom y axis pledge interval label to be displayed. - var graphYAxisBottomLabelText: Signal { get } - - /// Emits the middle y axis pledge interval label to be displayed. - var graphYAxisMiddleLabelText: Signal { get } - - /// Emits the top y axis pledge interval label to be displayed. - var graphYAxisTopLabelText: Signal { get } -} - -public protocol DashboardFundingCellViewModelType { - var inputs: DashboardFundingCellViewModelInputs { get } - var outputs: DashboardFundingCellViewModelOutputs { get } -} - -public final class DashboardFundingCellViewModel: DashboardFundingCellViewModelInputs, - DashboardFundingCellViewModelOutputs, DashboardFundingCellViewModelType { - public static let tickCount = 4 - - public init() { - let statsProject = self.statsProjectProperty.signal.skipNil() - - self.backersText = statsProject.map { _, project in Format.wholeNumber(project.stats.backersCount) } - - self.deadlineDateText = statsProject.map { _, project in - guard let deadline = project.dates.deadline else { - return "" - } - - return Format.date(secondsInUTC: deadline, dateStyle: .short, timeStyle: .none) - } - - self.goalText = statsProject.map { _, project in - Strings.discovery_baseball_card_stats_pledged_of_goal( - goal: Format.currency(project.stats.goal, country: project.country) - ) - } - - self.graphData = statsProject - .map { stats, project in - let maxPledged = stats.map { $0.cumulativePledged }.max() ?? 0 - let range = Double(maxPledged > project.stats.goal ? maxPledged : project.stats.goal) - - return FundingGraphData( - project: project, - stats: stats, - yAxisTickSize: tickSize(DashboardFundingCellViewModel.tickCount, range: range) - ) - } - - self.graphYAxisBottomLabelText = self.graphData - .map { data in Format.currency(Int(data.yAxisTickSize), country: data.project.country) } - - self.graphYAxisMiddleLabelText = self.graphData - .map { data in Format.currency(Int(data.yAxisTickSize * 2), country: data.project.country) } - - self.graphYAxisTopLabelText = self.graphData - .map { data in Format.currency(Int(data.yAxisTickSize * 3), country: data.project.country) } - - self.launchDateText = statsProject - .map { _, project in - guard let launchedAt = project.dates.launchedAt else { - return "" - } - - return Format.date(secondsInUTC: launchedAt, dateStyle: .short, timeStyle: .none) - } - - self.pledgedText = statsProject - .map { _, project in Format.currency(project.stats.pledged, country: project.country) } - - let timeRemaining = statsProject.map { _, project -> (String, String) in - guard let deadline = project.dates.deadline else { - return ("", "") - } - - return Format.duration(secondsInUTC: deadline, useToGo: true) - } - - self.timeRemainingTitleText = timeRemaining.map(first) - self.timeRemainingSubtitleText = timeRemaining.map(second) - - self.cellAccessibilityValue = statsProject - .map { _, project in - - let pledged = Format.currency(project.stats.pledged, country: project.country) - let goal = Format.currency(project.stats.goal, country: project.country) - let backersCount = project.stats.backersCount - var (time, unit) = ("", "") - - if let deadline = project.dates.deadline { - (time, unit) = Format.duration(secondsInUTC: deadline, useToGo: false) - } - - let timeLeft = time + " " + unit - - return project.state == .live ? - Strings.dashboard_graphs_funding_accessibility_live_stat_value( - pledged: pledged, goal: goal, backers_count: backersCount, time_left: timeLeft - ) : - Strings.dashboard_graphs_funding_accessibility_non_live_stat_value( - pledged: pledged, goal: goal, backers_count: backersCount, time_left: timeLeft - ) - } - } - - private let statsProjectProperty = MutableProperty<([ProjectStatsEnvelope.FundingDateStats], Project)?>(nil) - public func configureWith( - fundingDateStats stats: [ProjectStatsEnvelope.FundingDateStats], - project: Project - ) { - self.statsProjectProperty.value = (stats, project) - } - - public let backersText: Signal - public let cellAccessibilityValue: Signal - public let deadlineDateText: Signal - public let goalText: Signal - public let graphData: Signal - public let graphYAxisBottomLabelText: Signal - public let graphYAxisMiddleLabelText: Signal - public let graphYAxisTopLabelText: Signal - public let launchDateText: Signal - public let pledgedText: Signal - public let timeRemainingSubtitleText: Signal - public let timeRemainingTitleText: Signal - - public var inputs: DashboardFundingCellViewModelInputs { return self } - public var outputs: DashboardFundingCellViewModelOutputs { return self } -} - -// Returns the tick size relative to the number of ticks in a range. -private func tickSize(_ tickCount: Int, range: Double) -> CGFloat { - let unroundedTickSize = range / (Double(tickCount) - 1.0) - let exponent = ceil(log10(unroundedTickSize) - 1.0) - let power = pow(10.0, exponent) - return CGFloat(ceil(unroundedTickSize / power) * power) -} diff --git a/Library/ViewModels/DashboardFundingCellViewModelTests.swift b/Library/ViewModels/DashboardFundingCellViewModelTests.swift deleted file mode 100644 index cd59fb49a7..0000000000 --- a/Library/ViewModels/DashboardFundingCellViewModelTests.swift +++ /dev/null @@ -1,148 +0,0 @@ -@testable import KsApi -@testable import Library -import Prelude -import ReactiveExtensions -import ReactiveExtensions_TestHelpers -import ReactiveSwift -import XCTest - -internal final class DashboardFundingCellViewModelTests: TestCase { - internal let vm = DashboardFundingCellViewModel() - internal let backersText = TestObserver() - internal let cellAccessibilityValue = TestObserver() - internal let deadlineDateText = TestObserver() - internal let goalText = TestObserver() - internal let launchDateText = TestObserver() - internal let pledgedText = TestObserver() - internal let project = TestObserver() - internal let stats = TestObserver<[ProjectStatsEnvelope.FundingDateStats], Never>() - internal let timeRemainingSubtitleText = TestObserver() - internal let timeRemainingTitleText = TestObserver() - internal let yAxisTickSize = TestObserver() - - internal override func setUp() { - super.setUp() - self.vm.outputs.backersText.observe(self.backersText.observer) - self.vm.outputs.cellAccessibilityValue.observe(self.cellAccessibilityValue.observer) - self.vm.outputs.deadlineDateText.observe(self.deadlineDateText.observer) - self.vm.outputs.launchDateText.observe(self.launchDateText.observer) - self.vm.outputs.goalText.observe(self.goalText.observer) - self.vm.outputs.graphData.map { data in data.project }.observe(self.project.observer) - self.vm.outputs.graphData.map { data in data.stats }.observe(self.stats.observer) - self.vm.outputs.graphData.map { data in data.yAxisTickSize }.observe(self.yAxisTickSize.observer) - self.vm.outputs.pledgedText.observe(self.pledgedText.observer) - self.vm.outputs.timeRemainingSubtitleText.observe(self.timeRemainingSubtitleText.observer) - self.vm.outputs.timeRemainingTitleText.observe(self.timeRemainingTitleText.observer) - } - - func testCellAccessibility() { - let liveProject = .template - |> Project.lens.stats.backersCount .~ 5 - |> Project.lens.stats.pledged .~ 50 - |> Project.lens.stats.goal .~ 10_000 - |> Project.lens.dates.deadline .~ (Date().timeIntervalSince1970 + 60.0 * 60.0 * 24.0 * 3.0) - |> Project.lens.country .~ .us - - let stats = [ProjectStatsEnvelope.FundingDateStats.template] - let deadline = liveProject.dates.deadline! - - self.vm.inputs.configureWith(fundingDateStats: stats, project: liveProject) - - self.cellAccessibilityValue.assertValues( - [ - Strings.dashboard_graphs_funding_accessibility_live_stat_value( - pledged: Format.currency(liveProject.stats.pledged, country: liveProject.country), - goal: Format.currency(liveProject.stats.goal, country: liveProject.country), - backers_count: liveProject.stats.backersCount, - time_left: Format.duration(secondsInUTC: deadline).time + " " + - Format.duration(secondsInUTC: deadline).unit - ) - ], - "Live project stats value emits." - ) - - let nonLiveProject = .template |> Project.lens.state .~ .successful - let nonLiveDeadline = nonLiveProject.dates.deadline! - - self.vm.inputs.configureWith(fundingDateStats: stats, project: nonLiveProject) - - self.cellAccessibilityValue.assertValues( - [ - Strings.dashboard_graphs_funding_accessibility_live_stat_value( - pledged: Format.currency(liveProject.stats.pledged, country: liveProject.country), - goal: Format.currency(liveProject.stats.goal, country: liveProject.country), - backers_count: liveProject.stats.backersCount, - time_left: Format.duration(secondsInUTC: deadline).time + " " + - Format.duration(secondsInUTC: deadline).unit - ), - Strings.dashboard_graphs_funding_accessibility_non_live_stat_value( - pledged: Format.currency(nonLiveProject.stats.pledged, country: nonLiveProject.country), - goal: Format.currency(nonLiveProject.stats.goal, country: nonLiveProject.country), - backers_count: nonLiveProject.stats.backersCount, - time_left: Format.duration(secondsInUTC: nonLiveDeadline).time + " " + - Format.duration(secondsInUTC: nonLiveDeadline).unit - ) - ], - "Non live project stats value emits." - ) - } - - func testFundingGraphDataEmits() { - let now = self.dateType.init() - - let stat1 = .template - |> ProjectStatsEnvelope.FundingDateStats.lens.date .~ (now.timeIntervalSince1970 - 60 * 60 * 24 * 4) - |> ProjectStatsEnvelope.FundingDateStats.lens.cumulativePledged .~ 500 - - let stat2 = .template - |> ProjectStatsEnvelope.FundingDateStats.lens.date .~ (now.timeIntervalSince1970 - 60 * 60 * 24 * 3) - |> ProjectStatsEnvelope.FundingDateStats.lens.cumulativePledged .~ 700 - - let stat3 = .template - |> ProjectStatsEnvelope.FundingDateStats.lens.date .~ (now.timeIntervalSince1970 - 60 * 60 * 24 * 2) - |> ProjectStatsEnvelope.FundingDateStats.lens.cumulativePledged .~ 1_500 - - let stat4 = .template - |> ProjectStatsEnvelope.FundingDateStats.lens.date .~ (now.timeIntervalSince1970 - 60 * 60 * 24 * 1) - |> ProjectStatsEnvelope.FundingDateStats.lens.cumulativePledged .~ 2_200 - - let stat5 = .template - |> ProjectStatsEnvelope.FundingDateStats.lens.date .~ now.timeIntervalSince1970 - |> ProjectStatsEnvelope.FundingDateStats.lens.cumulativePledged .~ 3_500 - - let fundingDateStats = [stat1, stat2, stat3, stat4, stat5] - - let project = .template - |> Project.lens.dates.deadline .~ now.timeIntervalSince1970 - |> Project.lens.dates.launchedAt .~ (now.timeIntervalSince1970 - 60 * 60 * 24 * 5) - |> Project.lens.dates.stateChangedAt .~ now.timeIntervalSince1970 - - self.vm.inputs.configureWith(fundingDateStats: fundingDateStats, project: project) - - self.project.assertValues([project]) - self.stats.assertValues([[stat1, stat2, stat3, stat4, stat5]]) - self.yAxisTickSize.assertValueCount(1) - } - - func testProjectDataEmits() { - let now = self.dateType.init() - - let fundingDateStats = [ProjectStatsEnvelope.FundingDateStats.template] - - let project = .template - |> Project.lens.stats.backersCount .~ 2_000 - |> Project.lens.dates.deadline .~ (now.timeIntervalSince1970 + 60.0 * 60.0 * 24.0) - |> Project.lens.stats.goal .~ 50_000 - |> Project.lens.stats.pledged .~ 5_000 - - self.vm.inputs.configureWith(fundingDateStats: fundingDateStats, project: project) - - self.backersText.assertValues(["2,000"]) - self.deadlineDateText.assertValueCount(1) - self.goalText.assertValues(["pledged of $50,000"]) - self.launchDateText.assertValueCount(1) - self.pledgedText.assertValues(["$5,000"]) - self.timeRemainingSubtitleText.assertValues(["hours to go"]) - self.timeRemainingTitleText.assertValues(["24"]) - } -} diff --git a/Library/ViewModels/DashboardProjectsDrawerCellViewModel.swift b/Library/ViewModels/DashboardProjectsDrawerCellViewModel.swift deleted file mode 100644 index be77d57f24..0000000000 --- a/Library/ViewModels/DashboardProjectsDrawerCellViewModel.swift +++ /dev/null @@ -1,66 +0,0 @@ -import KsApi -import Prelude -import ReactiveExtensions -import ReactiveSwift - -public protocol DashboardProjectsDrawerCellViewModelInputs { - /// Call when configuring cell with Project and order of creation - func configureWith(project: Project, indexNum: Int, isChecked: Bool) -} - -public protocol DashboardProjectsDrawerCellViewModelOutputs { - /// Emits label for accessibility. - var cellAccessibilityLabel: Signal { get } - - /// Emits value for accessibility. - var cellAccessibilityValue: Signal { get } - - /// Emits whether should show checkmark or not. - var isCheckmarkHidden: Signal { get } - - /// Emits with project name label text. - var projectNameText: Signal { get } - - /// Emits with project number label text. - var projectNumberText: Signal { get } -} - -public protocol DashboardProjectsDrawerCellViewModelType { - var inputs: DashboardProjectsDrawerCellViewModelInputs { get } - var outputs: DashboardProjectsDrawerCellViewModelOutputs { get } -} - -public final class DashboardProjectsDrawerCellViewModel: DashboardProjectsDrawerCellViewModelType, - DashboardProjectsDrawerCellViewModelInputs, DashboardProjectsDrawerCellViewModelOutputs { - public init() { - self.projectNameText = self.projectProperty.signal.skipNil().map { $0.name } - - self.projectNumberText = self.orderNumProperty.signal.map { - Strings.dashboard_switcher_project_number(current_project_index: "\($0 + 1)") - } - - let isChecked = self.isCheckedProperty.signal - self.isCheckmarkHidden = isChecked.map(negate) - - self.cellAccessibilityLabel = self.projectNameText - self.cellAccessibilityValue = isChecked.map { $0 ? "Selected" : "Unselected" } - } - - public var inputs: DashboardProjectsDrawerCellViewModelInputs { return self } - public var outputs: DashboardProjectsDrawerCellViewModelOutputs { return self } - - public let cellAccessibilityLabel: Signal - public let cellAccessibilityValue: Signal - public let projectNameText: Signal - public let projectNumberText: Signal - public let isCheckmarkHidden: Signal - - fileprivate let projectProperty = MutableProperty(nil) - fileprivate let orderNumProperty = MutableProperty(0) - fileprivate let isCheckedProperty = MutableProperty(false) - public func configureWith(project: Project, indexNum: Int, isChecked: Bool) { - self.projectProperty.value = project - self.orderNumProperty.value = indexNum - self.isCheckedProperty.value = isChecked - } -} diff --git a/Library/ViewModels/DashboardProjectsDrawerCellViewModelTests.swift b/Library/ViewModels/DashboardProjectsDrawerCellViewModelTests.swift deleted file mode 100644 index 9c8bf11ef3..0000000000 --- a/Library/ViewModels/DashboardProjectsDrawerCellViewModelTests.swift +++ /dev/null @@ -1,43 +0,0 @@ -@testable import KsApi -@testable import Library -import Prelude -import ReactiveExtensions_TestHelpers -import ReactiveSwift -import XCTest - -internal final class DashboardProjectsDrawerCellViewModelTests: TestCase { - internal let vm: DashboardProjectsDrawerCellViewModelType = DashboardProjectsDrawerCellViewModel() - - let isCheckmarkHidden = TestObserver() - let projectNameText = TestObserver() - let projectNumberText = TestObserver() - - internal override func setUp() { - super.setUp() - - self.vm.outputs.isCheckmarkHidden.observe(self.isCheckmarkHidden.observer) - self.vm.outputs.projectNameText.observe(self.projectNameText.observer) - self.vm.outputs.projectNumberText.observe(self.projectNumberText.observer) - } - - func testConfigureWith() { - let project = .template |> Project.lens.name .~ "Fart Patrol" - let project2 = .template |> Project.lens.name .~ "Cat Detectives" - - self.isCheckmarkHidden.assertValueCount(0) - self.projectNameText.assertValueCount(0) - self.projectNumberText.assertValueCount(0) - - self.vm.inputs.configureWith(project: project, indexNum: 2, isChecked: true) - - self.isCheckmarkHidden.assertValues([false]) - self.projectNameText.assertValues(["Fart Patrol"]) - self.projectNumberText.assertValues(["Project #3"]) - - self.vm.inputs.configureWith(project: project2, indexNum: 1, isChecked: false) - - self.isCheckmarkHidden.assertValues([false, true]) - self.projectNameText.assertValues(["Fart Patrol", "Cat Detectives"]) - self.projectNumberText.assertValues(["Project #3", "Project #2"]) - } -} diff --git a/Library/ViewModels/DashboardProjectsDrawerViewModel.swift b/Library/ViewModels/DashboardProjectsDrawerViewModel.swift deleted file mode 100644 index 84e51a512b..0000000000 --- a/Library/ViewModels/DashboardProjectsDrawerViewModel.swift +++ /dev/null @@ -1,102 +0,0 @@ -import KsApi -import Prelude -import ReactiveExtensions -import ReactiveSwift - -public protocol DashboardProjectsDrawerViewModelInputs { - /// Call when the view has completed animating in. - func animateInCompleted() - - /// Call when the view has completed animating out. - func animateOutCompleted() - - /// Call when the background is tapped to dismiss the drawer. - func backgroundTapped() - - /// Call to configure the datasource with projects. - func configureWith(data: [ProjectsDrawerData]) - - /// Call when a project cell is tapped with the project. - func projectCellTapped(_ project: Project) - - /// Call when the view loads. - func viewDidLoad() -} - -public protocol DashboardProjectsDrawerViewModelOutputs { - /// Emits when to shift screen reader focus on first project cell. - var focusScreenReaderOnFirstProject: Signal<(), Never> { get } - - /// Emits projects to display in tableview. - var projectsDrawerData: Signal<[ProjectsDrawerData], Never> { get } - - /// Emits to notify delegate to close the drawer on background tap. - var notifyDelegateToCloseDrawer: Signal<(), Never> { get } - - /// Emits to notify delegate when view controller completed animating out. - var notifyDelegateDidAnimateOut: Signal<(), Never> { get } - - /// Emits to notify delegate when project cell was tapped with project. - var notifyDelegateProjectCellTapped: Signal { get } -} - -public protocol DashboardProjectsDrawerViewModelType { - var inputs: DashboardProjectsDrawerViewModelInputs { get } - var outputs: DashboardProjectsDrawerViewModelOutputs { get } -} - -public final class DashboardProjectsDrawerViewModel: DashboardProjectsDrawerViewModelType, - DashboardProjectsDrawerViewModelInputs, DashboardProjectsDrawerViewModelOutputs { - public init() { - self.projectsDrawerData = self.projectsDrawerDataProperty.signal.skipNil() - .takeWhen(self.viewDidLoadProperty.signal) - - self.notifyDelegateToCloseDrawer = self.backgroundTappedProperty.signal - - self.notifyDelegateProjectCellTapped = self.projectCellTappedProperty.signal.skipNil() - - self.notifyDelegateDidAnimateOut = self.animateOutCompletedProperty.signal - - self.focusScreenReaderOnFirstProject = self.animateInCompletedProperty.signal - .filter { AppEnvironment.current.isVoiceOverRunning() } - } - - public var inputs: DashboardProjectsDrawerViewModelInputs { return self } - public var outputs: DashboardProjectsDrawerViewModelOutputs { return self } - - public let focusScreenReaderOnFirstProject: Signal<(), Never> - public let projectsDrawerData: Signal<[ProjectsDrawerData], Never> - public let notifyDelegateDidAnimateOut: Signal<(), Never> - public var notifyDelegateToCloseDrawer: Signal<(), Never> - public let notifyDelegateProjectCellTapped: Signal - - fileprivate let animateInCompletedProperty = MutableProperty(()) - public func animateInCompleted() { - self.animateInCompletedProperty.value = () - } - - fileprivate let animateOutCompletedProperty = MutableProperty(()) - public func animateOutCompleted() { - self.animateOutCompletedProperty.value = () - } - - fileprivate let backgroundTappedProperty = MutableProperty(()) - public func backgroundTapped() { - self.backgroundTappedProperty.value = () - } - - fileprivate let projectsDrawerDataProperty = MutableProperty<[ProjectsDrawerData]?>(nil) - public func configureWith(data: [ProjectsDrawerData]) { - self.projectsDrawerDataProperty.value = data - } - - fileprivate let projectCellTappedProperty = MutableProperty(nil) - public func projectCellTapped(_ project: Project) { - self.projectCellTappedProperty.value = project - } - - fileprivate let viewDidLoadProperty = MutableProperty(()) - public func viewDidLoad() { - self.viewDidLoadProperty.value = () - } -} diff --git a/Library/ViewModels/DashboardProjectsDrawerViewModelTests.swift b/Library/ViewModels/DashboardProjectsDrawerViewModelTests.swift deleted file mode 100644 index 591b17777d..0000000000 --- a/Library/ViewModels/DashboardProjectsDrawerViewModelTests.swift +++ /dev/null @@ -1,102 +0,0 @@ -@testable import KsApi -@testable import Library -import Prelude -import ReactiveExtensions_TestHelpers -import ReactiveSwift -import XCTest - -internal final class DashboardProjectsDrawerViewModelTests: TestCase { - internal let vm: DashboardProjectsDrawerViewModelType = DashboardProjectsDrawerViewModel() - - let projectsDrawerData = TestObserver<[ProjectsDrawerData], Never>() - let notifyDelegateToCloseDrawer = TestObserver<(), Never>() - let notifyDelegateDidAnimateOut = TestObserver<(), Never>() - let notifyDelegateProjectCellTapped = TestObserver() - let focusScreenReaderOnFirstProject = TestObserver<(), Never>() - - let project1 = .template |> Project.lens.id .~ 4 - let project2 = .template |> Project.lens.id .~ 6 - let data1 = [ProjectsDrawerData(project: .template |> Project.lens.id .~ 4, indexNum: 0, isChecked: true)] - let data2 = [ - ProjectsDrawerData( - project: .template |> Project.lens.id .~ 4, - indexNum: 0, - isChecked: true - ), - ProjectsDrawerData( - project: .template |> Project.lens.id .~ 6, - indexNum: 1, - isChecked: false - ) - ] - - internal override func setUp() { - super.setUp() - - self.vm.outputs.projectsDrawerData.observe(self.projectsDrawerData.observer) - self.vm.outputs.notifyDelegateToCloseDrawer.observe(self.notifyDelegateToCloseDrawer.observer) - self.vm.outputs.notifyDelegateDidAnimateOut.observe(self.notifyDelegateDidAnimateOut.observer) - self.vm.outputs.notifyDelegateProjectCellTapped.observe(self.notifyDelegateProjectCellTapped.observer) - self.vm.outputs.focusScreenReaderOnFirstProject.observe(self.focusScreenReaderOnFirstProject.observer) - } - - func testConfigureWith() { - self.vm.inputs.configureWith(data: self.data1) - - self.projectsDrawerData.assertValueCount(0) - - self.vm.inputs.viewDidLoad() - - self.projectsDrawerData.assertValues([self.data1]) - - self.vm.inputs.configureWith(data: self.data2) - - self.projectsDrawerData.assertValueCount(1) - - self.vm.inputs.viewDidLoad() - - self.projectsDrawerData.assertValues([self.data1, self.data2]) - } - - func testProjectTapped() { - self.vm.inputs.configureWith(data: self.data1) - self.vm.inputs.viewDidLoad() - - self.notifyDelegateProjectCellTapped.assertValueCount(0) - - self.vm.inputs.projectCellTapped(self.project1) - - self.notifyDelegateProjectCellTapped.assertValues([self.project1]) - } - - func testAnimateOut_OnBackgroundTapped() { - self.vm.inputs.configureWith(data: self.data1) - self.vm.inputs.viewDidLoad() - self.vm.inputs.animateInCompleted() - - self.notifyDelegateToCloseDrawer.assertValueCount(0) - - self.vm.inputs.backgroundTapped() - - self.notifyDelegateToCloseDrawer.assertValueCount(1) - self.notifyDelegateDidAnimateOut.assertValueCount(0) - - self.vm.inputs.animateOutCompleted() - - self.notifyDelegateToCloseDrawer.assertValueCount(1, "Drawer close does not emit") - self.notifyDelegateDidAnimateOut.assertValueCount(1, "Notify delegate animate out complete emits") - } - - func testAnimateIn_FocusOnFirstProject() { - withEnvironment(isVoiceOverRunning: { true }) { - self.vm.inputs.configureWith(data: data1) - self.vm.inputs.viewDidLoad() - - self.focusScreenReaderOnFirstProject.assertValueCount(0) - - self.vm.inputs.animateInCompleted() - - self.focusScreenReaderOnFirstProject.assertValueCount(1) - } - } -} diff --git a/Library/ViewModels/DashboardReferrerRowStackViewViewModel.swift b/Library/ViewModels/DashboardReferrerRowStackViewViewModel.swift deleted file mode 100644 index caf5101d08..0000000000 --- a/Library/ViewModels/DashboardReferrerRowStackViewViewModel.swift +++ /dev/null @@ -1,71 +0,0 @@ -import KsApi -import Prelude -import ReactiveExtensions -import ReactiveSwift -import UIKit - -public protocol DashboardReferrerRowStackViewViewModelInputs { - /// Call to configure cell with referrer data. - func configureWith(country: Project.Country, referrer: ProjectStatsEnvelope.ReferrerStats) -} - -public protocol DashboardReferrerRowStackViewViewModelOutputs { - /// Emits the number of backers to be displayed. - var backersText: Signal { get } - - /// Emits the amount pledged and percentage to be displayed. - var pledgedText: Signal { get } - - /// Emits the referrer source to be displayed. - var sourceText: Signal { get } - - /// Emits the text color of the row labels to be displayed. - var textColor: Signal { get } -} - -public protocol DashboardReferrerRowStackViewViewModelType { - var inputs: DashboardReferrerRowStackViewViewModelInputs { get } - var outputs: DashboardReferrerRowStackViewViewModelOutputs { get } -} - -public final class DashboardReferrerRowStackViewViewModel: DashboardReferrerRowStackViewViewModelInputs, - DashboardReferrerRowStackViewViewModelOutputs, DashboardReferrerRowStackViewViewModelType { - public init() { - let countryReferrer = self.countryReferrerProperty.signal.skipNil() - - self.backersText = countryReferrer.map { _, referrer in Format.wholeNumber(referrer.backersCount) } - - self.pledgedText = countryReferrer - .map { country, referrer in - Format.currency(Int(referrer.pledged), country: country) + " (" - + Format.percentage(referrer.percentageOfDollars) + ")" - } - - self.sourceText = countryReferrer.map { _, referrer in referrer.referrerName } - - self.textColor = countryReferrer.map { _, referrer in - switch referrer.referrerType { - case .internal: - return .ksr_create_700 - case .external: - return .ksr_celebrate_500 - default: - return .ksr_trust_500 - } - } - } - - fileprivate let countryReferrerProperty = - MutableProperty<(Project.Country, ProjectStatsEnvelope.ReferrerStats)?>(nil) - public func configureWith(country: Project.Country, referrer: ProjectStatsEnvelope.ReferrerStats) { - self.countryReferrerProperty.value = (country, referrer) - } - - public let backersText: Signal - public let pledgedText: Signal - public let sourceText: Signal - public let textColor: Signal - - public var inputs: DashboardReferrerRowStackViewViewModelInputs { return self } - public var outputs: DashboardReferrerRowStackViewViewModelOutputs { return self } -} diff --git a/Library/ViewModels/DashboardReferrerRowStackViewViewModelTests.swift b/Library/ViewModels/DashboardReferrerRowStackViewViewModelTests.swift deleted file mode 100644 index 8ecb8e5e63..0000000000 --- a/Library/ViewModels/DashboardReferrerRowStackViewViewModelTests.swift +++ /dev/null @@ -1,39 +0,0 @@ -@testable import KsApi -@testable import Library -import Prelude -import ReactiveExtensions -import ReactiveExtensions_TestHelpers -import ReactiveSwift -import XCTest - -internal final class DashboardReferrersRowStackViewViewModelTests: TestCase { - internal let vm = DashboardReferrerRowStackViewViewModel() - internal let backersText = TestObserver() - internal let pledgedText = TestObserver() - internal let sourceText = TestObserver() - internal let textColor = TestObserver() - - internal override func setUp() { - super.setUp() - self.vm.outputs.backersText.observe(self.backersText.observer) - self.vm.outputs.pledgedText.observe(self.pledgedText.observer) - self.vm.outputs.sourceText.observe(self.sourceText.observer) - self.vm.outputs.textColor.observe(self.textColor.observer) - } - - func testReferrerRowDataEmits() { - let referrer = .template - |> ProjectStatsEnvelope.ReferrerStats.lens.backersCount .~ 50 - |> ProjectStatsEnvelope.ReferrerStats.lens.percentageOfDollars .~ 0.125 - |> ProjectStatsEnvelope.ReferrerStats.lens.pledged .~ 100.0 - |> ProjectStatsEnvelope.ReferrerStats.lens.referrerName .~ "search" - |> ProjectStatsEnvelope.ReferrerStats.lens.referrerType .~ .internal - let country = Project.Country.us - - self.vm.inputs.configureWith(country: country, referrer: referrer) - self.backersText.assertValues(["50"]) - self.pledgedText.assertValues(["$100 (12%)"]) - self.sourceText.assertValues(["search"]) - self.textColor.assertValues([.ksr_create_700]) - } -} diff --git a/Library/ViewModels/DashboardReferrersCellViewModel.swift b/Library/ViewModels/DashboardReferrersCellViewModel.swift deleted file mode 100644 index a7358522f9..0000000000 --- a/Library/ViewModels/DashboardReferrersCellViewModel.swift +++ /dev/null @@ -1,249 +0,0 @@ -import KsApi -import Prelude -import ReactiveExtensions -import ReactiveSwift - -public struct ReferrersRowData { - public let country: Project.Country - public let referrers: [ProjectStatsEnvelope.ReferrerStats] -} - -extension ReferrersRowData: Equatable {} -public func == (lhs: ReferrersRowData, rhs: ReferrersRowData) -> Bool { - return lhs.country == rhs.country && lhs.referrers == rhs.referrers -} - -public protocol DashboardReferrersCellViewModelInputs { - /// Call when cell is loaded. - func awakeFromNib() - - /// Call when the Backers button is tapped. - func backersButtonTapped() - - /// Call to configure cell with cumulative and referral stats. - func configureWith( - cumulative: ProjectStatsEnvelope.CumulativeStats, - project: Project, - referralAggregates: ProjectStatsEnvelope.ReferralAggregateStats, - referrers: [ProjectStatsEnvelope.ReferrerStats] - ) - - /// Call when the Percent button is tapped. - func percentButtonTapped() - - /// Call when the Pledged button is tapped. - func pledgedButtonTapped() - - /// Call when the Show more referrers button is tapped. - func showMoreReferrersTapped() - - /// Call when the Source button is tapped. - func sourceButtonTapped() -} - -public protocol DashboardReferrersCellViewModelOutputs { - /// Emits the average pledge text to be displayed. - var averagePledgeText: Signal { get } - - /// Emits the custom percent text to be displayed. - var customPercentText: Signal { get } - - /// Emits the pledged via custom text to be displayed. - var customPledgedText: Signal { get } - - /// Emits the external pledge percentage to be displayed in a chart. - var externalPercentage: Signal { get } - - /// Emits the percent pledged via external text to be displayed. - var externalPercentText: Signal { get } - - /// Emits the pledged via external text to be displayed. - var externalPledgedText: Signal { get } - - /// Emits the internal pledge percentage to be displayed in a chart. - var internalPercentage: Signal { get } - - /// Emits the percent pledged via internal text to be displayed. - var internalPercentText: Signal { get } - - /// Emits the pledged via internal text to be displayed. - var internalPledgedText: Signal { get } - - /// Emits when should notify the delegate that referrer rows have been added to the stack view. - var notifyDelegateAddedReferrerRows: Signal { get } - - /// Emits the referrers row data to be displayed in each referrers stack view row. - var referrersRowData: Signal { get } - - /// Emits a boolean to determine when the show more referrers button should be hidden. - var showMoreReferrersButtonHidden: Signal { get } -} - -public protocol DashboardReferrersCellViewModelType { - var inputs: DashboardReferrersCellViewModelInputs { get } - var outputs: DashboardReferrersCellViewModelOutputs { get } -} - -public final class DashboardReferrersCellViewModel: DashboardReferrersCellViewModelInputs, - DashboardReferrersCellViewModelOutputs, DashboardReferrersCellViewModelType { - public init() { - let cumulativeProjectStats = self.cumulativeProjectStatsProperty.signal.skipNil() - - let country = cumulativeProjectStats.map { _, project, _, _ in project.country } - - let referralAggregates = cumulativeProjectStats.map { _, _, aggregates, _ in aggregates } - - let referrers = cumulativeProjectStats.map { _, _, _, stats in stats } - - self.averagePledgeText = cumulativeProjectStats - .map { cumulative, project, _, _ in - Format.currency(cumulative.averagePledge, country: project.country) - } - - let customPledgedAmount = referralAggregates - .map { $0.custom } - - let externalPledgedAmount = referralAggregates - .map { $0.external } - - let internalPledgedAmount = referralAggregates - .map { $0.kickstarter } - - let pledge = cumulativeProjectStats - .map { cumulative, _, _, _ in cumulative.pledged } - - self.customPercentText = Signal.combineLatest(customPledgedAmount, pledge) - .map { customAmount, pledged in - pledged == 0 ? Format.percentage(0.0) : Format.percentage(customAmount / Double(pledged)) - } - - self.customPledgedText = Signal.combineLatest(customPledgedAmount, country) - .map { pledged, country in Format.currency(Int(pledged), country: country) } - - self.externalPercentage = Signal.combineLatest(externalPledgedAmount, pledge) - .map { externalAmount, pledged in pledged == 0 ? 0.0 : externalAmount / Double(pledged) } - - self.externalPercentText = self.externalPercentage.map { Format.percentage($0) } - - self.externalPledgedText = Signal.combineLatest(externalPledgedAmount, country) - .map { pledged, country in Format.currency(Int(pledged), country: country) } - - self.internalPercentage = Signal.combineLatest(internalPledgedAmount, pledge) - .map { internalAmount, pledged in pledged == 0 ? 0.0 : internalAmount / Double(pledged) } - - self.internalPercentText = self.internalPercentage.map { Format.percentage($0) } - - self.internalPledgedText = Signal.combineLatest(internalPledgedAmount, country) - .map { pledged, country in Format.currency(Int(pledged), country: country) } - - let sortedByPledgedOrPercent = referrers.sort { $0.pledged > $1.pledged } - - let initialSort = sortedByPledgedOrPercent - - let sortedByBackers = referrers - .takeWhen(self.backersButtonTappedProperty.signal) - .sort { $0.backersCount > $1.backersCount } - - let sortedByPercent = sortedByPledgedOrPercent - .takeWhen(self.percentButtonTappedProperty.signal) - - let sortedByPledged = sortedByPledgedOrPercent - .takeWhen(self.pledgedButtonTappedProperty.signal) - - let sortedBySource = referrers - .takeWhen(self.sourceButtonTappedProperty.signal) - .sort { $0.referrerName.lowercased() < $1.referrerName.lowercased() } - - let allReferrers = Signal.merge( - initialSort, - sortedByBackers, - sortedByPercent, - sortedByPledged, - sortedBySource - ) - - let allReferrersRowData = Signal.combineLatest(country, allReferrers) - .map(ReferrersRowData.init) - - let showMoreReferrersButtonIsHidden = Signal.merge( - referrers.map { $0.count < 6 }, - self.showMoreReferrersTappedProperty.signal.mapConst(true) - ) - - self.showMoreReferrersButtonHidden = showMoreReferrersButtonIsHidden.skipRepeats() - - self.referrersRowData = Signal.combineLatest(allReferrersRowData, showMoreReferrersButtonIsHidden) - .map { rowData, isHidden in - let refCount = rowData.referrers.count - let maxReferrers = isHidden ? rowData.referrers : - Array(rowData.referrers[0..(nil) - public func configureWith( - cumulative: ProjectStatsEnvelope.CumulativeStats, - project: Project, - referralAggregates: ProjectStatsEnvelope.ReferralAggregateStats, - referrers: [ProjectStatsEnvelope.ReferrerStats] - ) { - self.cumulativeProjectStatsProperty.value = (cumulative, project, referralAggregates, referrers) - } - - fileprivate let percentButtonTappedProperty = MutableProperty(()) - public func percentButtonTapped() { - self.percentButtonTappedProperty.value = () - } - - fileprivate let pledgedButtonTappedProperty = MutableProperty(()) - public func pledgedButtonTapped() { - self.pledgedButtonTappedProperty.value = () - } - - fileprivate let showMoreReferrersTappedProperty = MutableProperty(()) - public func showMoreReferrersTapped() { - self.showMoreReferrersTappedProperty.value = () - } - - fileprivate let sourceButtonTappedProperty = MutableProperty(()) - public func sourceButtonTapped() { - self.sourceButtonTappedProperty.value = () - } - - public let averagePledgeText: Signal - public let customPercentText: Signal - public let customPledgedText: Signal - public let externalPercentage: Signal - public let externalPercentText: Signal - public let externalPledgedText: Signal - public let internalPercentage: Signal - public let internalPercentText: Signal - public let internalPledgedText: Signal - public let notifyDelegateAddedReferrerRows: Signal - public let referrersRowData: Signal - public let showMoreReferrersButtonHidden: Signal - - public var inputs: DashboardReferrersCellViewModelInputs { return self } - public var outputs: DashboardReferrersCellViewModelOutputs { return self } -} diff --git a/Library/ViewModels/DashboardReferrersCellViewModelTests.swift b/Library/ViewModels/DashboardReferrersCellViewModelTests.swift deleted file mode 100644 index ae79f68227..0000000000 --- a/Library/ViewModels/DashboardReferrersCellViewModelTests.swift +++ /dev/null @@ -1,219 +0,0 @@ -@testable import KsApi -@testable import Library -import Prelude -import ReactiveExtensions -import ReactiveExtensions_TestHelpers -import ReactiveSwift -import XCTest - -internal final class DashboardReferrersCellViewModelTests: TestCase { - internal let vm = DashboardReferrersCellViewModel() - internal let averagePledgeText = TestObserver() - internal let customPercentText = TestObserver() - internal let customPledgedText = TestObserver() - internal let externalPercentText = TestObserver() - internal let externalPledgedText = TestObserver() - internal let internalPercentText = TestObserver() - internal let internalPledgedText = TestObserver() - internal let notifyDelegateAddedReferrerRows = TestObserver() - internal let referrersRowCountry = TestObserver() - internal let referrersRowReferrers = TestObserver<[ProjectStatsEnvelope.ReferrerStats], Never>() - internal let showMoreReferrersButtonHidden = TestObserver() - - internal override func setUp() { - super.setUp() - self.vm.outputs.averagePledgeText.observe(self.averagePledgeText.observer) - self.vm.outputs.customPercentText.observe(self.customPercentText.observer) - self.vm.outputs.customPledgedText.observe(self.customPledgedText.observer) - self.vm.outputs.externalPercentText.observe(self.externalPercentText.observer) - self.vm.outputs.externalPledgedText.observe(self.externalPledgedText.observer) - self.vm.outputs.internalPercentText.observe(self.internalPercentText.observer) - self.vm.outputs.internalPledgedText.observe(self.internalPledgedText.observer) - self.vm.outputs.notifyDelegateAddedReferrerRows.observe(self.notifyDelegateAddedReferrerRows.observer) - self.vm.outputs.referrersRowData.map { $0.country }.observe(self.referrersRowCountry.observer) - self.vm.outputs.referrersRowData.map { $0.referrers }.observe(self.referrersRowReferrers.observer) - self.vm.outputs.showMoreReferrersButtonHidden.observe(self.showMoreReferrersButtonHidden.observer) - } - - func testAccumulatedReferrerDataEmits() { - let country = Project.Country.us - let cumulative = .template - |> ProjectStatsEnvelope.CumulativeStats.lens.pledged .~ 300 - let project = .template |> Project.lens.country .~ country - let referrers = [ProjectStatsEnvelope.ReferrerStats.template] - - let referralAggregates = .template - |> ProjectStatsEnvelope.ReferralAggregateStats.lens.kickstarter .~ 100.00 - |> ProjectStatsEnvelope.ReferralAggregateStats.lens.external .~ 100.00 - |> ProjectStatsEnvelope.ReferralAggregateStats.lens.custom .~ 100.00 - - self.vm.inputs.configureWith( - cumulative: cumulative, project: project, - referralAggregates: referralAggregates, referrers: referrers - ) - - self.externalPledgedText.assertValues(["$100"]) - self.externalPercentText.assertValues(["33%"]) - self.customPledgedText.assertValues(["$100"]) - self.customPercentText.assertValues(["33%"]) - self.internalPledgedText.assertValues(["$100"]) - self.internalPercentText.assertValues(["33%"]) - } - - func testZeroPledges() { - let country = Project.Country.us - let cumulative = .template - |> ProjectStatsEnvelope.CumulativeStats.lens.pledged .~ 0 - let project = .template |> Project.lens.country .~ country - let referrers = [ProjectStatsEnvelope.ReferrerStats.template] - - let referralAggregates = .template - |> ProjectStatsEnvelope.ReferralAggregateStats.lens.kickstarter .~ 0.0 - |> ProjectStatsEnvelope.ReferralAggregateStats.lens.external .~ 0.0 - |> ProjectStatsEnvelope.ReferralAggregateStats.lens.custom .~ 0.0 - - self.vm.inputs.configureWith( - cumulative: cumulative, project: project, - referralAggregates: referralAggregates, referrers: referrers - ) - - self.externalPledgedText.assertValues(["$0"]) - self.externalPercentText.assertValues(["0%"]) - self.customPledgedText.assertValues(["$0"]) - self.customPercentText.assertValues(["0%"]) - self.internalPledgedText.assertValues(["$0"]) - self.internalPercentText.assertValues(["0%"]) - } - - func testCumulativeDataEmits() { - let country = Project.Country.us - let cumulative = .template - |> ProjectStatsEnvelope.CumulativeStats.lens.averagePledge .~ 50 - let project = .template |> Project.lens.country .~ country - let referrers = [ProjectStatsEnvelope.ReferrerStats.template] - let referralAggregates = ProjectStatsEnvelope.ReferralAggregateStats.template - - self.vm.inputs.configureWith( - cumulative: cumulative, project: project, - referralAggregates: referralAggregates, referrers: referrers - ) - self.averagePledgeText.assertValues(["$50"], "Average pledge amount emits.") - } - - func testReferrersRowDataEmits() { - let stats1 = .template |> ProjectStatsEnvelope.ReferrerStats.lens.backersCount .~ 1 - let stats2 = .template |> ProjectStatsEnvelope.ReferrerStats.lens.backersCount .~ 2 - let stats3 = .template |> ProjectStatsEnvelope.ReferrerStats.lens.backersCount .~ 3 - let stats4 = .template |> ProjectStatsEnvelope.ReferrerStats.lens.backersCount .~ 4 - let stats5 = .template |> ProjectStatsEnvelope.ReferrerStats.lens.backersCount .~ 5 - let stats6 = .template |> ProjectStatsEnvelope.ReferrerStats.lens.backersCount .~ 6 - let stats7 = .template |> ProjectStatsEnvelope.ReferrerStats.lens.backersCount .~ 7 - let stats8 = .template |> ProjectStatsEnvelope.ReferrerStats.lens.backersCount .~ 8 - let stats9 = .template |> ProjectStatsEnvelope.ReferrerStats.lens.backersCount .~ 9 - let stats10 = .template |> ProjectStatsEnvelope.ReferrerStats.lens.backersCount .~ 10 - - let country = Project.Country.us - let cumulative = ProjectStatsEnvelope.CumulativeStats.template - let project = .template |> Project.lens.country .~ country - let referrers = [stats1, stats2, stats3, stats4, stats5, stats6, stats7, stats8, stats9, stats10] - let referralAggregates = ProjectStatsEnvelope.ReferralAggregateStats.template - - self.vm.inputs.configureWith( - cumulative: cumulative, project: project, - referralAggregates: referralAggregates, referrers: referrers - ) - self.referrersRowCountry.assertValues([country], "Project country emits.") - self.referrersRowReferrers.assertValues( - [[stats1, stats2, stats3]], - "First four referrer stats emit." - ) - self.showMoreReferrersButtonHidden.assertValues([false], "Button shown when there are more referrers.") - XCTAssertEqual([], self.segmentTrackingClient.events) - - self.vm.inputs.showMoreReferrersTapped() - self.referrersRowReferrers.assertValues( - [ - [stats1, stats2, stats3], - [stats1, stats2, stats3, stats4, stats5, stats6, stats7, stats8, stats9, stats10] - ], - "Remaining referrer stats emit." - ) - self.notifyDelegateAddedReferrerRows.assertValueCount(1, "Notified delegate that rows were added.") - self.showMoreReferrersButtonHidden.assertValues([false, true], "Button hidden when clicked.") - - XCTAssertEqual([], self.segmentTrackingClient.events) - } - - func testSortByColumn() { - let country = Project.Country.us - let cumulative = ProjectStatsEnvelope.CumulativeStats.template - let project = .template |> Project.lens.country .~ country - - let stats1 = .template - |> ProjectStatsEnvelope.ReferrerStats.lens.backersCount .~ 6 - |> ProjectStatsEnvelope.ReferrerStats.lens.percentageOfDollars .~ 0.3 - |> ProjectStatsEnvelope.ReferrerStats.lens.pledged .~ 300.0 - |> ProjectStatsEnvelope.ReferrerStats.lens.referrerName .~ "B" - - let stats2 = .template - |> ProjectStatsEnvelope.ReferrerStats.lens.backersCount .~ 3 - |> ProjectStatsEnvelope.ReferrerStats.lens.percentageOfDollars .~ 0.5 - |> ProjectStatsEnvelope.ReferrerStats.lens.pledged .~ 500.0 - |> ProjectStatsEnvelope.ReferrerStats.lens.referrerName .~ "A" - - let stats3 = .template - |> ProjectStatsEnvelope.ReferrerStats.lens.backersCount .~ 10 - |> ProjectStatsEnvelope.ReferrerStats.lens.percentageOfDollars .~ 0.2 - |> ProjectStatsEnvelope.ReferrerStats.lens.pledged .~ 200.0 - |> ProjectStatsEnvelope.ReferrerStats.lens.referrerName .~ "C" - - let stats4 = .template - |> ProjectStatsEnvelope.ReferrerStats.lens.backersCount .~ 2 - |> ProjectStatsEnvelope.ReferrerStats.lens.percentageOfDollars .~ 0.05 - |> ProjectStatsEnvelope.ReferrerStats.lens.pledged .~ 50.0 - |> ProjectStatsEnvelope.ReferrerStats.lens.referrerName .~ "E" - - let stats5 = .template - |> ProjectStatsEnvelope.ReferrerStats.lens.backersCount .~ 7 - |> ProjectStatsEnvelope.ReferrerStats.lens.percentageOfDollars .~ 0.15 - |> ProjectStatsEnvelope.ReferrerStats.lens.pledged .~ 150.0 - |> ProjectStatsEnvelope.ReferrerStats.lens.referrerName .~ "D" - - let referrers = [stats1, stats2, stats3, stats4, stats5] - let referralAggregates = ProjectStatsEnvelope.ReferralAggregateStats.template - - self.vm.inputs.configureWith( - cumulative: cumulative, project: project, - referralAggregates: referralAggregates, referrers: referrers - ) - self.referrersRowReferrers.assertValues( - [[stats2, stats1, stats3, stats5, stats4]], - "Initial stats emit sorted by descending pledge amount." - ) - - self.vm.inputs.backersButtonTapped() - self.referrersRowReferrers.assertValues( - [[stats3, stats5, stats1, stats2, stats4]], - "Stats emit sorted by descending backers count." - ) - - self.vm.inputs.percentButtonTapped() - self.referrersRowReferrers.assertValues( - [[stats2, stats1, stats3, stats5, stats4]], - "Stats emit sorted by descending percent pledged amount." - ) - - self.vm.inputs.pledgedButtonTapped() - self.referrersRowReferrers.assertValues( - [[stats2, stats1, stats3, stats5, stats4]], - "Stats emit sorted by descending pledge amount." - ) - - self.vm.inputs.sourceButtonTapped() - self.referrersRowReferrers.assertValues( - [[stats2, stats1, stats3, stats5, stats4]], - "Stats emit sorted alphabetically." - ) - self.notifyDelegateAddedReferrerRows.assertDidNotEmitValue("Delegate should not have added any rows.") - } -} diff --git a/Library/ViewModels/DashboardRewardRowStackViewViewModel.swift b/Library/ViewModels/DashboardRewardRowStackViewViewModel.swift deleted file mode 100644 index 4ce1d7ad9b..0000000000 --- a/Library/ViewModels/DashboardRewardRowStackViewViewModel.swift +++ /dev/null @@ -1,76 +0,0 @@ -import KsApi -import Prelude -import ReactiveExtensions -import ReactiveSwift - -public protocol DashboardRewardRowStackViewViewModelInputs { - /// Call to configure view model with Country, RewardsStats, and total pledged. - func configureWith( - country: Project.Country, - reward: ProjectStatsEnvelope.RewardStats, - totalPledged: Int - ) -} - -public protocol DashboardRewardRowStackViewViewModelOutputs { - /// Emits string for backer text label. - var backersText: Signal { get } - - /// Emits string for pledged text label. - var pledgedText: Signal { get } - - /// Emits string for top rewards text label. - var topRewardText: Signal { get } -} - -public protocol DashboardRewardRowStackViewViewModelType { - var inputs: DashboardRewardRowStackViewViewModelInputs { get } - var outputs: DashboardRewardRowStackViewViewModelOutputs { get } -} - -public final class DashboardRewardRowStackViewViewModel: DashboardRewardRowStackViewViewModelType, - DashboardRewardRowStackViewViewModelInputs, DashboardRewardRowStackViewViewModelOutputs { - public init() { - let countryRewardPledged = self.countryRewardPledgedProperty.signal.skipNil() - - self.backersText = countryRewardPledged.map { _, reward, _ in - Format.wholeNumber(reward.backersCount) - } - - self.pledgedText = countryRewardPledged.map(pledgedWithPercentText) - - self.topRewardText = countryRewardPledged - .map { country, reward, _ in - reward.rewardId == Reward.noReward.id - ? Strings.dashboard_graphs_rewards_no_reward() - : Format.currency(reward.minimum ?? 0, country: country) - } - } - - public var inputs: DashboardRewardRowStackViewViewModelInputs { return self } - public var outputs: DashboardRewardRowStackViewViewModelOutputs { return self } - - public let backersText: Signal - public let pledgedText: Signal - public let topRewardText: Signal - - fileprivate let countryRewardPledgedProperty = - MutableProperty<(Project.Country, ProjectStatsEnvelope.RewardStats, Int)?>(nil) - public func configureWith( - country: Project.Country, - reward: ProjectStatsEnvelope.RewardStats, - totalPledged: Int - ) { - self.countryRewardPledgedProperty.value = (country, reward, totalPledged) - } -} - -private func pledgedWithPercentText( - country: Project.Country, - reward: ProjectStatsEnvelope.RewardStats, - totalPledged: Int -) -> String { - let percent = Double(reward.pledged) / Double(totalPledged) - let percentText = (percent > 0.01 || percent == 0) ? Format.percentage(percent) : "<1%" - return Format.currency(reward.pledged, country: country) + " (\(percentText))" -} diff --git a/Library/ViewModels/DashboardRewardRowStackViewViewModelTests.swift b/Library/ViewModels/DashboardRewardRowStackViewViewModelTests.swift deleted file mode 100644 index 16115961e0..0000000000 --- a/Library/ViewModels/DashboardRewardRowStackViewViewModelTests.swift +++ /dev/null @@ -1,99 +0,0 @@ -@testable import KsApi -@testable import Library -import Prelude -import ReactiveExtensions -import ReactiveExtensions_TestHelpers -import ReactiveSwift -import XCTest - -internal final class DashboardRewardRowStackViewViewModelTests: TestCase { - let vm: DashboardRewardRowStackViewViewModelType = DashboardRewardRowStackViewViewModel() - - let backersText = TestObserver() - let pledgedText = TestObserver() - let topRewardText = TestObserver() - - override func setUp() { - super.setUp() - - self.vm.outputs.backersText.observe(self.backersText.observer) - self.vm.outputs.pledgedText.observe(self.pledgedText.observer) - self.vm.outputs.topRewardText.observe(self.topRewardText.observer) - } - - func testRewardBackers() { - let reward = .template - |> ProjectStatsEnvelope.RewardStats.lens.backersCount .~ 50 - |> ProjectStatsEnvelope.RewardStats.lens.id .~ 5 - |> ProjectStatsEnvelope.RewardStats.lens.minimum .~ 5.0 - |> ProjectStatsEnvelope.RewardStats.lens.pledged .~ 250 - - self.vm.inputs.configureWith(country: .us, reward: reward, totalPledged: 1_000) - - self.backersText.assertValues(["50"]) - self.pledgedText.assertValues(["$250 (25%)"]) - self.topRewardText.assertValues(["$5"]) - } - - func testRewardLowBackers() { - let reward = .template - |> ProjectStatsEnvelope.RewardStats.lens.backersCount .~ 2 - |> ProjectStatsEnvelope.RewardStats.lens.id .~ 5 - |> ProjectStatsEnvelope.RewardStats.lens.minimum .~ 5.0 - |> ProjectStatsEnvelope.RewardStats.lens.pledged .~ 10 - - self.vm.inputs.configureWith(country: .us, reward: reward, totalPledged: 10_000) - - self.backersText.assertValues(["2"]) - self.pledgedText.assertValues(["$10 (<1%)"]) - self.topRewardText.assertValues(["$5"]) - } - - func testRewardNoBackers() { - let reward = .template - |> ProjectStatsEnvelope.RewardStats.lens.backersCount .~ 0 - |> ProjectStatsEnvelope.RewardStats.lens.id .~ 5 - |> ProjectStatsEnvelope.RewardStats.lens.minimum .~ 3.0 - |> ProjectStatsEnvelope.RewardStats.lens.pledged .~ 0 - - self.vm.inputs.configureWith(country: .us, reward: reward, totalPledged: 1_000) - - self.backersText.assertValues(["0"]) - self.pledgedText.assertValues(["$0 (0%)"]) - self.topRewardText.assertValues(["$3"]) - } - - func testNoRewardBackers() { - let reward = .unPledged - |> ProjectStatsEnvelope.RewardStats.lens.backersCount .~ 200 - |> ProjectStatsEnvelope.RewardStats.lens.pledged .~ 200 - - self.vm.inputs.configureWith(country: .us, reward: reward, totalPledged: 1_000) - - self.backersText.assertValues(["200"]) - self.pledgedText.assertValues(["$200 (20%)"]) - self.topRewardText.assertValues(["No reward"]) - } - - func testNoRewardLowBackers() { - let reward = .unPledged - |> ProjectStatsEnvelope.RewardStats.lens.backersCount .~ 2 - |> ProjectStatsEnvelope.RewardStats.lens.pledged .~ 2 - - self.vm.inputs.configureWith(country: .us, reward: reward, totalPledged: 10_000) - - self.backersText.assertValues(["2"]) - self.pledgedText.assertValues(["$2 (<1%)"]) - self.topRewardText.assertValues(["No reward"]) - } - - func testNoRewardNoBackers() { - let reward = ProjectStatsEnvelope.RewardStats.unPledged - - self.vm.inputs.configureWith(country: .us, reward: reward, totalPledged: 1_000) - - self.backersText.assertValues(["0"]) - self.pledgedText.assertValues(["$0 (0%)"]) - self.topRewardText.assertValues(["No reward"]) - } -} diff --git a/Library/ViewModels/DashboardRewardsCellViewModel.swift b/Library/ViewModels/DashboardRewardsCellViewModel.swift deleted file mode 100644 index b0995023a1..0000000000 --- a/Library/ViewModels/DashboardRewardsCellViewModel.swift +++ /dev/null @@ -1,162 +0,0 @@ -import KsApi -import Prelude -import ReactiveExtensions -import ReactiveSwift - -public struct RewardsRowData { - public let country: Project.Country - public let rewardsStats: [ProjectStatsEnvelope.RewardStats] - public let totalPledged: Int -} - -extension RewardsRowData: Equatable {} -public func == (lhs: RewardsRowData, rhs: RewardsRowData) -> Bool { - return - lhs.country == rhs.country && - lhs.rewardsStats == rhs.rewardsStats && - lhs.totalPledged == rhs.totalPledged -} - -public protocol DashboardRewardsCellViewModelInputs { - /// Call when Backers button is tapped. - func backersButtonTapped() - - /// Call when load cell with project. - func configureWith(rewardStats: [ProjectStatsEnvelope.RewardStats], project: Project) - - /// Call when Pledged button is tapped. - func pledgedButtonTapped() - - /// Call when See all tiers button is tapped. - func seeAllTiersButtonTapped() - - /// Call when Top Rewards button is tapped. - func topRewardsButtonTapped() -} - -public protocol DashboardRewardsCellViewModelOutputs { - /// Emits when should hide See all tiers button. - var hideSeeAllTiersButton: Signal { get } - - /// Emits when should notify the delegate that reward rows have been added to the stack view. - var notifyDelegateAddedRewardRows: Signal { get } - - /// Emits RewardsRowData. Rewards array is truncated if 'see all' button is present. - var rewardsRowData: Signal { get } -} - -public protocol DashboardRewardsCellViewModelType { - var inputs: DashboardRewardsCellViewModelInputs { get } - var outputs: DashboardRewardsCellViewModelOutputs { get } -} - -public final class DashboardRewardsCellViewModel: DashboardRewardsCellViewModelType, - DashboardRewardsCellViewModelInputs, DashboardRewardsCellViewModelOutputs { - public init() { - let statsProject = self.statsProjectProperty.signal.skipNil() - - let rewards = statsProject - .map { stats, project in (project.rewards, stats) } - .map(allRewardsStats(rewards:stats:)) - - let initialSort = rewards.sort { $0.pledged > $1.pledged } - - let sortedByTop = rewards - .sort { ($0.minimum ?? 0) > ($1.minimum ?? 0) } - .takeWhen(self.topRewardsButtonTappedProperty.signal) - - let sortedByBackers = rewards - .takeWhen(self.backersButtonTappedProperty.signal) - .sort { $0.backersCount > $1.backersCount } - - let sortedByPledged = initialSort - .takeWhen(self.pledgedButtonTappedProperty.signal) - - let allRewards = Signal.merge( - initialSort, - sortedByTop, - sortedByBackers, - sortedByPledged - ) - - let allRewardsRowData = Signal.combineLatest( - statsProject.map { $1 }, - allRewards - ) - .map { project, stats in - RewardsRowData(country: project.country, rewardsStats: stats, totalPledged: project.stats.pledged) - } - - let allTiersButtonIsHidden = Signal.merge( - rewards.map { $0.count < 5 }, - self.seeAllTiersButtonTappedProperty.signal.mapConst(true) - ) - - self.hideSeeAllTiersButton = allTiersButtonIsHidden.skipRepeats() - - // if more than 6 rewards, truncate at 4 - self.rewardsRowData = Signal.combineLatest(allRewardsRowData, allTiersButtonIsHidden) - .map { rowData, isHidden in - let rewardCount = rowData.rewardsStats.count - let maxRewards = isHidden ? rowData.rewardsStats : - Array(rowData.rewardsStats[0.. - public let notifyDelegateAddedRewardRows: Signal - public let rewardsRowData: Signal - - fileprivate let backersButtonTappedProperty = MutableProperty(()) - public func backersButtonTapped() { - self.backersButtonTappedProperty.value = () - } - - fileprivate let statsProjectProperty = - MutableProperty<([ProjectStatsEnvelope.RewardStats], Project)?>(nil) - public func configureWith(rewardStats: [ProjectStatsEnvelope.RewardStats], project: Project) { - self.statsProjectProperty.value = (rewardStats, project) - } - - fileprivate let pledgedButtonTappedProperty = MutableProperty(()) - public func pledgedButtonTapped() { - self.pledgedButtonTappedProperty.value = () - } - - fileprivate let seeAllTiersButtonTappedProperty = MutableProperty(()) - public func seeAllTiersButtonTapped() { - self.seeAllTiersButtonTappedProperty.value = () - } - - fileprivate let topRewardsButtonTappedProperty = MutableProperty(()) - public func topRewardsButtonTapped() { - self.topRewardsButtonTappedProperty.value = () - } -} - -private func allRewardsStats(rewards: [Reward], stats: [ProjectStatsEnvelope.RewardStats]) - -> [ProjectStatsEnvelope.RewardStats] { - let statsIds = stats.map { $0.rewardId } - - let zeroPledgedStats = rewards.filter { !statsIds.contains($0.id) } - .map { - .zero - |> ProjectStatsEnvelope.RewardStats.lens.backersCount .~ ($0.backersCount ?? 0) - |> ProjectStatsEnvelope.RewardStats.lens.id .~ $0.id - |> ProjectStatsEnvelope.RewardStats.lens.minimum .~ $0.minimum - } - - return zeroPledgedStats + stats -} diff --git a/Library/ViewModels/DashboardRewardsCellViewModelTests.swift b/Library/ViewModels/DashboardRewardsCellViewModelTests.swift deleted file mode 100644 index 9c352ffa9c..0000000000 --- a/Library/ViewModels/DashboardRewardsCellViewModelTests.swift +++ /dev/null @@ -1,195 +0,0 @@ -@testable import KsApi -@testable import Library -import Prelude -import ReactiveExtensions_TestHelpers -import XCTest - -internal final class DashboardRewardsCellViewModelTests: TestCase { - let vm: DashboardRewardsCellViewModelType = DashboardRewardsCellViewModel() - - let hideSeeAllTiersButton = TestObserver() - let notifyDelegateAddedRewardRows = TestObserver() - let rewardsRowCountry = TestObserver() - let rewardsRowRewards = TestObserver<[ProjectStatsEnvelope.RewardStats], Never>() - let rewardsRowTotalPledged = TestObserver() - - let reward1 = Reward.template - let reward2 = Reward.noReward - let reward3 = Reward.template - |> Reward.lens.backersCount .~ 120 - |> Reward.lens.id .~ 2 - |> Reward.lens.minimum .~ 20.0 - let reward4 = Reward.template - |> Reward.lens.backersCount .~ 4 - |> Reward.lens.id .~ 3 - |> Reward.lens.minimum .~ 15.0 - let reward5 = Reward.template - |> Reward.lens.backersCount .~ 25 - |> Reward.lens.id .~ 4 - |> Reward.lens.minimum .~ 35.0 - let reward6 = Reward.template - |> Reward.lens.backersCount .~ 16 - |> Reward.lens.id .~ 5 - |> Reward.lens.minimum .~ 30.0 - let reward7 = Reward.template - |> Reward.lens.id .~ 6 - |> Reward.lens.backersCount .~ 0 - |> Reward.lens.minimum .~ 100.0 - - let stat1 = .template - |> ProjectStatsEnvelope.RewardStats.lens.id .~ 1 - - let stat2 = .template - |> ProjectStatsEnvelope.RewardStats.lens.backersCount .~ 120 - |> ProjectStatsEnvelope.RewardStats.lens.id .~ 2 - |> ProjectStatsEnvelope.RewardStats.lens.minimum .~ 20.0 - |> ProjectStatsEnvelope.RewardStats.lens.pledged .~ 1_000 - - let stat3 = .template - |> ProjectStatsEnvelope.RewardStats.lens.backersCount .~ 4 - |> ProjectStatsEnvelope.RewardStats.lens.id .~ 3 - |> ProjectStatsEnvelope.RewardStats.lens.minimum .~ 15.0 - |> ProjectStatsEnvelope.RewardStats.lens.pledged .~ 250 - - let stat4 = .template - |> ProjectStatsEnvelope.RewardStats.lens.backersCount .~ 25 - |> ProjectStatsEnvelope.RewardStats.lens.id .~ 4 - |> ProjectStatsEnvelope.RewardStats.lens.minimum .~ 35.0 - |> ProjectStatsEnvelope.RewardStats.lens.pledged .~ 1_750 - - let stat5 = .template - |> ProjectStatsEnvelope.RewardStats.lens.backersCount .~ 16 - |> ProjectStatsEnvelope.RewardStats.lens.id .~ 5 - |> ProjectStatsEnvelope.RewardStats.lens.minimum .~ 30.0 - |> ProjectStatsEnvelope.RewardStats.lens.pledged .~ 1_500 - - let zeroPledgedStat1 = ProjectStatsEnvelope.RewardStats.unPledged - let zeroPledgedStat2 = ProjectStatsEnvelope.RewardStats.unPledged - |> ProjectStatsEnvelope.RewardStats.lens.id .~ 6 - |> ProjectStatsEnvelope.RewardStats.lens.minimum .~ 100.0 - - override func setUp() { - super.setUp() - - self.vm.outputs.hideSeeAllTiersButton.observe(self.hideSeeAllTiersButton.observer) - self.vm.outputs.notifyDelegateAddedRewardRows.observe(self.notifyDelegateAddedRewardRows.observer) - self.vm.outputs.rewardsRowData.map { $0.country }.observe(self.rewardsRowCountry.observer) - self.vm.outputs.rewardsRowData.map { $0.rewardsStats }.observe(self.rewardsRowRewards.observer) - self.vm.outputs.rewardsRowData.map { $0.totalPledged }.observe(self.rewardsRowTotalPledged.observer) - } - - func testRewards() { - let rewards = [reward1, reward2, reward3] - let project = .template - |> Project.lens.rewardData.rewards .~ rewards - |> Project.lens.stats.pledged .~ 1_500 - let stats = [stat1, stat2] - - self.rewardsRowRewards.assertValueCount(0) - self.rewardsRowCountry.assertValueCount(0) - self.rewardsRowTotalPledged.assertValueCount(0) - self.hideSeeAllTiersButton.assertValueCount(0) - - self.vm.inputs.configureWith(rewardStats: stats, project: project) - - self.rewardsRowRewards.assertValues( - [[self.stat2, self.stat1, self.zeroPledgedStat1]], - "Emits initial reward stats sorted by minimum value" - ) - self.rewardsRowCountry.assertValues([.us]) - self.rewardsRowTotalPledged.assertValues([1_500]) - self.hideSeeAllTiersButton.assertValues([true]) - } - - func testShowAllRewards() { - let rewards = [reward1, reward2, reward3, reward4, reward5, reward6, reward7] - let project = .template - |> Project.lens.rewardData.rewards .~ rewards - |> Project.lens.stats.pledged .~ 5_000 - let stats = [stat1, stat2, stat3, stat4, stat5] - - self.rewardsRowRewards.assertValueCount(0) - self.hideSeeAllTiersButton.assertValueCount(0) - - self.vm.inputs.configureWith(rewardStats: stats, project: project) - - self.rewardsRowRewards.assertValues( - [[self.stat4, self.stat5, self.stat2]], - "Emits 4 initial rewards sorted by minimum value" - ) - - self.rewardsRowCountry.assertValues([.us]) - self.rewardsRowTotalPledged.assertValues([5_000]) - self.hideSeeAllTiersButton.assertValues([false]) - self.notifyDelegateAddedRewardRows.assertDidNotEmitValue("No additional rewards were added.") - - self.vm.inputs.seeAllTiersButtonTapped() - - self.rewardsRowRewards.assertValues([ - [self.stat4, self.stat5, self.stat2], - [ - self.stat4, - self.stat5, - self.stat2, - self.stat1, - self.stat3, - self.zeroPledgedStat1, - self.zeroPledgedStat2 - ] - ], "Emit all rewards sorted by minimum value") - self.rewardsRowCountry.assertValues([.us, .us]) - self.rewardsRowTotalPledged.assertValues([5_000, 5_000]) - self.hideSeeAllTiersButton.assertValues([false, true]) - self.notifyDelegateAddedRewardRows.assertValueCount(1, "Additional rewards were added.") - - XCTAssertEqual([], self.segmentTrackingClient.events) - } - - func testSorting() { - let rewards = [reward1, reward2, reward3, reward4, reward5, reward6, reward7] - let project = Project.template |> Project.lens.rewardData.rewards .~ rewards - let stats = [stat1, stat2, stat3, stat4, stat5] - - self.rewardsRowRewards.assertValueCount(0) - self.hideSeeAllTiersButton.assertValueCount(0) - - self.vm.inputs.configureWith(rewardStats: stats, project: project) - - self.rewardsRowRewards.assertValues( - [[self.stat4, self.stat5, self.stat2]], - "Emits 4 initial rewards sorted by minimum value" - ) - self.vm.inputs.backersButtonTapped() - - self.rewardsRowRewards.assertValues( - [ - [self.stat4, self.stat5, self.stat2], - [self.stat2, self.stat1, self.stat4] - ], - "Emits rewards sorted by backers count" - ) - - self.vm.inputs.topRewardsButtonTapped() - - self.rewardsRowRewards.assertValues( - [ - [self.stat4, self.stat5, self.stat2], - [self.stat2, self.stat1, self.stat4], - [self.zeroPledgedStat2, self.stat4, self.stat5] - ], - "Emits rewards sorted by min value" - ) - - self.vm.inputs.pledgedButtonTapped() - - self.rewardsRowRewards.assertValues( - [ - [self.stat4, self.stat5, self.stat2], - [self.stat2, self.stat1, self.stat4], - [self.zeroPledgedStat2, self.stat4, self.stat5], - [self.stat4, self.stat5, self.stat2] - ], - "Emits rewards sorted by pledged count" - ) - } -} diff --git a/Library/ViewModels/DashboardTitleViewViewModel.swift b/Library/ViewModels/DashboardTitleViewViewModel.swift deleted file mode 100644 index 5b04637f20..0000000000 --- a/Library/ViewModels/DashboardTitleViewViewModel.swift +++ /dev/null @@ -1,97 +0,0 @@ -import KsApi -import Prelude -import ReactiveExtensions -import ReactiveSwift - -public protocol DashboardTitleViewViewModelInputs { - /// Call to update the data for the title. - func updateData(_ data: DashboardTitleViewData) - - /// Call when title button is tapped. - func titleButtonTapped() -} - -public protocol DashboardTitleViewViewModelOutputs { - /// Emits whether should hide arrow icon. - var hideArrow: Signal { get } - - /// Emits when the delegate should show/hide the projects drawer when the title is tapped. - var notifyDelegateShowHideProjectsDrawer: Signal<(), Never> { get } - - /// Emits a11y label for title view. - var titleAccessibilityLabel: Signal { get } - - /// Emits a11y hint for title view. - var titleAccessibilityHint: Signal { get } - - /// Emits whether title should be tappable. - var titleButtonIsEnabled: Signal { get } - - /// Emits the text for the title view. - var titleText: Signal { get } - - /// Emits to update arrow icon to open or closed state. - var updateArrowState: Signal { get } -} - -public protocol DashboardTitleViewViewModelType { - var inputs: DashboardTitleViewViewModelInputs { get } - var outputs: DashboardTitleViewViewModelOutputs { get } -} - -public final class DashboardTitleViewViewModel: DashboardTitleViewViewModelType, - DashboardTitleViewViewModelInputs, DashboardTitleViewViewModelOutputs { - public init() { - self.titleText = self.currentProjectIndexProperty.signal.skipNil() - .map { Strings.dashboard_switcher_project_number(current_project_index: "\($0 + 1)") } - - let isArrowHidden = self.updateDrawerStateHideArrowProperty.signal.skipNil().map(second) - - self.titleButtonIsEnabled = isArrowHidden.map(negate).skipRepeats() - - self.hideArrow = isArrowHidden - - self.updateArrowState = self.updateDrawerStateHideArrowProperty.signal.skipNil() - .filter { _, hideArrow in !hideArrow } - .map(first) - - self.notifyDelegateShowHideProjectsDrawer = self.titleButtonTappedProperty.signal - - self.titleAccessibilityLabel = self.titleText - .takeWhen(isArrowHidden.filter(isFalse)) - .map { Strings.tabbar_dashboard() + ", " + $0 } - - self.titleAccessibilityHint = self.updateArrowState - .map { - switch $0 { - case .open: - return Strings.dashboard_switcher_accessibility_label_closes_list_of_projects() - case .closed: - return Strings.dashboard_switcher_accessibility_label_opens_list_of_projects() - } - } - } - - public var inputs: DashboardTitleViewViewModelInputs { return self } - public var outputs: DashboardTitleViewViewModelOutputs { return self } - - public let updateArrowState: Signal - public let hideArrow: Signal - public let notifyDelegateShowHideProjectsDrawer: Signal<(), Never> - public let titleAccessibilityLabel: Signal - public let titleAccessibilityHint: Signal - public let titleText: Signal - public let titleButtonIsEnabled: Signal - - fileprivate let currentProjectIndexProperty = MutableProperty(nil) - fileprivate let updateDrawerStateHideArrowProperty = MutableProperty<(DrawerState, Bool)?>(nil) - public func updateData(_ data: DashboardTitleViewData) { - self.currentProjectIndexProperty.value = data.currentProjectIndex - self.updateDrawerStateHideArrowProperty.value = (data.drawerState, data.isArrowHidden) - } - - fileprivate let titleButtonTappedProperty = MutableProperty(()) - public func titleButtonTapped() { - self.titleButtonTappedProperty.value = () - } -} diff --git a/Library/ViewModels/DashboardTitleViewViewModelTests.swift b/Library/ViewModels/DashboardTitleViewViewModelTests.swift deleted file mode 100644 index 4253a5ffc5..0000000000 --- a/Library/ViewModels/DashboardTitleViewViewModelTests.swift +++ /dev/null @@ -1,157 +0,0 @@ -@testable import KsApi -@testable import Library -import Prelude -import ReactiveExtensions_TestHelpers -import ReactiveSwift -import XCTest - -internal final class DashboardTitleViewViewModelTests: TestCase { - internal let vm: DashboardTitleViewViewModelType = DashboardTitleViewViewModel() - - let hideArrow = TestObserver() - let notifyDelegateShowHideProjectsDrawer = TestObserver<(), Never>() - let titleText = TestObserver() - let titleButtonIsEnabled = TestObserver() - let updateArrowState = TestObserver() - let titleAccessibilityLabel = TestObserver() - let titleAccessibilityHint = TestObserver() - - internal override func setUp() { - super.setUp() - - self.vm.outputs.hideArrow.observe(self.hideArrow.observer) - self.vm.outputs.notifyDelegateShowHideProjectsDrawer - .observe(self.notifyDelegateShowHideProjectsDrawer.observer) - self.vm.outputs.titleText.observe(self.titleText.observer) - self.vm.outputs.titleButtonIsEnabled.observe(self.titleButtonIsEnabled.observer) - self.vm.outputs.updateArrowState.observe(self.updateArrowState.observer) - self.vm.outputs.titleAccessibilityLabel.observe(self.titleAccessibilityLabel.observer) - self.vm.outputs.titleAccessibilityHint.observe(self.titleAccessibilityHint.observer) - } - - func testTitleText() { - withEnvironment(apiService: MockService(fetchProjectsResponse: [.template])) { - self.titleText.assertValueCount(0) - - self.vm.inputs.updateData(DashboardTitleViewData( - drawerState: .closed, isArrowHidden: true, - currentProjectIndex: 0 - )) - - self.titleText.assertValues(["Project #1"]) - } - } - - func testDrawerState_OneProject() { - self.hideArrow.assertValueCount(0) - self.titleButtonIsEnabled.assertValueCount(0) - - self.vm.inputs.updateData(DashboardTitleViewData( - drawerState: .closed, isArrowHidden: true, - currentProjectIndex: 0 - )) - - self.hideArrow.assertValues([true]) - self.titleButtonIsEnabled.assertValues([false]) - self.updateArrowState.assertValueCount(0) - self.notifyDelegateShowHideProjectsDrawer.assertValueCount(0) - self.titleAccessibilityLabel.assertValueCount(0) - self.titleAccessibilityHint.assertValueCount(0) - } - - func testDrawerState_MultipleProjects() { - self.hideArrow.assertValueCount(0) - self.titleButtonIsEnabled.assertValueCount(0) - self.updateArrowState.assertValueCount(0) - self.titleAccessibilityLabel.assertValueCount(0) - self.titleAccessibilityHint.assertValueCount(0) - - self.vm.inputs.updateData(DashboardTitleViewData( - drawerState: .closed, isArrowHidden: false, - currentProjectIndex: 0 - )) - - self.hideArrow.assertValues([false]) - self.titleButtonIsEnabled.assertValues([true]) - self.updateArrowState.assertValues([DrawerState.closed]) - self.notifyDelegateShowHideProjectsDrawer.assertValueCount(0) - self.titleAccessibilityLabel.assertValues(["Dashboard, Project #1"]) - self.titleAccessibilityHint.assertValues(["Opens list of projects."]) - - self.vm.inputs.titleButtonTapped() - - self.notifyDelegateShowHideProjectsDrawer.assertValueCount(1) - - self.vm.inputs.updateData(DashboardTitleViewData( - drawerState: .open, isArrowHidden: false, - currentProjectIndex: 0 - )) - - self.titleText.assertValues(["Project #1", "Project #1"]) - self.updateArrowState.assertValues([DrawerState.closed, DrawerState.open]) - self.titleAccessibilityLabel.assertValues(["Dashboard, Project #1", "Dashboard, Project #1"]) - self.titleAccessibilityHint.assertValues(["Opens list of projects.", "Closes list of projects."]) - - self.vm.inputs.titleButtonTapped() - - self.notifyDelegateShowHideProjectsDrawer.assertValueCount(2) - - self.vm.inputs.updateData(DashboardTitleViewData( - drawerState: .closed, isArrowHidden: false, - currentProjectIndex: 0 - )) - - self.titleText.assertValues(["Project #1", "Project #1", "Project #1"]) - self.updateArrowState.assertValues([DrawerState.closed, DrawerState.open, DrawerState.closed]) - self.titleAccessibilityLabel.assertValues([ - "Dashboard, Project #1", "Dashboard, Project #1", - "Dashboard, Project #1" - ]) - self.titleAccessibilityHint.assertValues([ - "Opens list of projects.", "Closes list of projects.", - "Opens list of projects." - ]) - - self.vm.inputs.titleButtonTapped() - - self.notifyDelegateShowHideProjectsDrawer.assertValueCount(3) - - self.vm.inputs.updateData(DashboardTitleViewData( - drawerState: .open, isArrowHidden: false, - currentProjectIndex: 0 - )) - - self.titleText.assertValues(["Project #1", "Project #1", "Project #1", "Project #1"]) - self.updateArrowState.assertValues([ - DrawerState.closed, DrawerState.open, DrawerState.closed, - DrawerState.open - ]) - self.titleAccessibilityLabel.assertValues([ - "Dashboard, Project #1", "Dashboard, Project #1", - "Dashboard, Project #1", "Dashboard, Project #1" - ]) - self.titleAccessibilityHint.assertValues([ - "Opens list of projects.", "Closes list of projects.", - "Opens list of projects.", "Closes list of projects." - ]) - - self.vm.inputs.updateData(DashboardTitleViewData( - drawerState: .closed, isArrowHidden: false, - currentProjectIndex: 2 - )) - - self.titleText.assertValues(["Project #1", "Project #1", "Project #1", "Project #1", "Project #3"]) - self.updateArrowState.assertValues([ - DrawerState.closed, DrawerState.open, DrawerState.closed, - DrawerState.open, DrawerState.closed - ]) - self.titleAccessibilityLabel.assertValues([ - "Dashboard, Project #1", "Dashboard, Project #1", - "Dashboard, Project #1", "Dashboard, Project #1", "Dashboard, Project #3" - ]) - self.titleAccessibilityHint.assertValues([ - "Opens list of projects.", "Closes list of projects.", - "Opens list of projects.", "Closes list of projects.", "Opens list of projects." - ]) - } -} diff --git a/Library/ViewModels/DashboardVideoCellViewModel.swift b/Library/ViewModels/DashboardVideoCellViewModel.swift deleted file mode 100644 index 9a3b1d3f12..0000000000 --- a/Library/ViewModels/DashboardVideoCellViewModel.swift +++ /dev/null @@ -1,128 +0,0 @@ -import KsApi -import Prelude -import ReactiveExtensions -import ReactiveSwift -import UIKit - -public protocol DashboardVideoCellViewModelInputs { - /// Call to configure cell with video stats. - func configureWith(videoStats stats: ProjectStatsEnvelope.VideoStats) -} - -public protocol DashboardVideoCellViewModelOutputs { - /// Emits the total completion percentage to be displayed. - var completionPercentage: Signal { get } - - /// Emits the count of external starts to be displayed. - var externalStartCount: Signal { get } - - /// Emits the external start progress to be displayed in a progress bar. - var externalStartProgress: Signal { get } - - /// Emits text for the external label. - var externalText: Signal { get } - - /// Emits the count of internal starts to be displayed. - var internalStartCount: Signal { get } - - /// Emits the internal start progress to be displayed in a progress bar. - var internalStartProgress: Signal { get } - - /// Emits text for the internal label. - var internalText: Signal { get } - - /// Emits the total count of video starts to be displayed. - var totalStartCount: Signal { get } -} - -public protocol DashboardVideoCellViewModelType { - var inputs: DashboardVideoCellViewModelInputs { get } - var outputs: DashboardVideoCellViewModelOutputs { get } -} - -public final class DashboardVideoCellViewModel: DashboardVideoCellViewModelInputs, - DashboardVideoCellViewModelOutputs, DashboardVideoCellViewModelType { - public init() { - let videoStats = self.statsProperty.signal.skipNil() - - self.completionPercentage = videoStats - .map { - Strings.dashboard_graphs_video_stats_percent_plays_completed( - percent_plays_completed: formattedCompletionPercentage(videoStats: $0) - ) - } - - self.externalStartCount = videoStats - .map { Format.wholeNumber($0.externalStarts) } - - self.externalStartProgress = videoStats.map(externalStartPercentage) - - self.internalStartCount = videoStats - .map { Format.wholeNumber($0.internalStarts) } - - self.internalStartProgress = videoStats.map(internalStartPercentage) - - self.totalStartCount = videoStats - .map { // TODO: need new string with count value - let string = Strings.dashboard_graphs_video_stats_total_plays_count( - total_start_count: totalStarts(videoStats: $0) - ) - return string.simpleHtmlAttributedString(font: UIFont.ksr_body(), bold: UIFont.ksr_body().bolded) - ?? NSAttributedString(string: "") - } - - self.internalText = videoStats - .map { Format.percentage(Double(internalStartPercentage(videoStats: $0))) + - " " + Strings.dashboard_graphs_video_stats_on_kickstarter() - } - - self.externalText = videoStats - .map { Format.percentage(Double(externalStartPercentage(videoStats: $0))) + - " " + Strings.dashboard_graphs_video_stats_off_site() - } - } - - fileprivate let statsProperty = MutableProperty(nil) - public func configureWith(videoStats stats: ProjectStatsEnvelope.VideoStats) { - self.statsProperty.value = stats - } - - public let completionPercentage: Signal - public let externalStartCount: Signal - public let externalStartProgress: Signal - public let externalText: Signal - public let internalStartCount: Signal - public let internalStartProgress: Signal - public let internalText: Signal - public let totalStartCount: Signal - - public var inputs: DashboardVideoCellViewModelInputs { return self } - public var outputs: DashboardVideoCellViewModelOutputs { return self } -} - -// Formatted string percent of video completions. -private func formattedCompletionPercentage(videoStats stats: ProjectStatsEnvelope.VideoStats) -> String { - let totalCompletionCount = CGFloat(totalCompletions(videoStats: stats)) - let totalStartCount = CGFloat(totalStarts(videoStats: stats)) - return Format.percentage(Int(floor(100 * totalCompletionCount / totalStartCount))) -} - -// Percent ratio of external starts to total starts measured from `0.0` to `1.0`. -private func externalStartPercentage(videoStats stats: ProjectStatsEnvelope.VideoStats) -> CGFloat { - let internalStarts = internalStartPercentage(videoStats: stats) - return 1.0 - internalStarts -} - -// Percent ratio of internal starts to total starts measured from `0.0` to `1.0`. -private func internalStartPercentage(videoStats stats: ProjectStatsEnvelope.VideoStats) -> CGFloat { - let total = totalStarts(videoStats: stats) - return ceil(100.0 * CGFloat(stats.internalStarts) / CGFloat(total)) / 100.0 -} - -private func totalCompletions(videoStats stats: ProjectStatsEnvelope.VideoStats) -> Int { - return stats.externalCompletions + stats.internalCompletions -} - -private func totalStarts(videoStats stats: ProjectStatsEnvelope.VideoStats) -> Int { - return stats.externalStarts + stats.internalStarts -} diff --git a/Library/ViewModels/DashboardVideoCellViewModelTests.swift b/Library/ViewModels/DashboardVideoCellViewModelTests.swift deleted file mode 100644 index 2159d57198..0000000000 --- a/Library/ViewModels/DashboardVideoCellViewModelTests.swift +++ /dev/null @@ -1,91 +0,0 @@ -@testable import KsApi -@testable import Library -import Prelude -import ReactiveExtensions -import ReactiveExtensions_TestHelpers -import ReactiveSwift -import XCTest - -internal final class DashboardVideoCellViewModelTests: TestCase { - internal let vm = DashboardVideoCellViewModel() - internal let completionPercentage = TestObserver() - internal let externalStartCount = TestObserver() - internal let externalStartProgress = TestObserver() - internal let externalText = TestObserver() - internal let internalStartCount = TestObserver() - internal let internalStartProgress = TestObserver() - internal let internalText = TestObserver() - internal let totalStartCount = TestObserver() - - internal override func setUp() { - super.setUp() - self.vm.outputs.completionPercentage.observe(self.completionPercentage.observer) - self.vm.outputs.externalStartCount.observe(self.externalStartCount.observer) - self.vm.outputs.externalStartProgress.map { round($0 * 100) }.observe(self.externalStartProgress.observer) - self.vm.outputs.externalText.observe(self.externalText.observer) - self.vm.outputs.internalStartCount.observe(self.internalStartCount.observer) - self.vm.outputs.internalStartProgress.map { round($0 * 100) }.observe(self.internalStartProgress.observer) - self.vm.outputs.internalText.observe(self.internalText.observer) - self.vm.outputs.totalStartCount.map { $0.string }.observe(self.totalStartCount.observer) - } - - func testInternalAndExternalPercentRounding() { - let videoStats = .template - |> ProjectStatsEnvelope.VideoStats.lens.externalCompletions .~ 25 - |> ProjectStatsEnvelope.VideoStats.lens.externalStarts .~ 145 - |> ProjectStatsEnvelope.VideoStats.lens.internalCompletions .~ 400 - |> ProjectStatsEnvelope.VideoStats.lens.internalStarts .~ 855 - - self.vm.inputs.configureWith(videoStats: videoStats) - - self.externalStartProgress.assertValues([14], "0.145, external, rounds down.") - self.externalText.assertValues(["14% \(Strings.dashboard_graphs_video_stats_off_site())"]) - - self.internalStartProgress.assertValues([86], "0.855, internal, rounds up.") - self.internalText.assertValues( - ["86% \(Strings.dashboard_graphs_video_stats_on_kickstarter())"], - "Internal rounds up." - ) - - let videoStats2 = .template - |> ProjectStatsEnvelope.VideoStats.lens.externalCompletions .~ 25 - |> ProjectStatsEnvelope.VideoStats.lens.externalStarts .~ 600 - |> ProjectStatsEnvelope.VideoStats.lens.internalCompletions .~ 400 - |> ProjectStatsEnvelope.VideoStats.lens.internalStarts .~ 500 - - self.vm.inputs.configureWith(videoStats: videoStats2) - - self.externalStartProgress.assertValues([14, 54], "0.545, external, rounds down.") - self.externalText.assertValues( - [ - "14% \(Strings.dashboard_graphs_video_stats_off_site())", - "54% \(Strings.dashboard_graphs_video_stats_off_site())" - ], "External rounds down." - ) - - self.internalStartProgress.assertValues([86, 46], "0.454, internal, rounds up.") - self.internalText.assertValues( - [ - "86% \(Strings.dashboard_graphs_video_stats_on_kickstarter())", - "46% \(Strings.dashboard_graphs_video_stats_on_kickstarter())" - ], "Internal rounds up." - ) - } - - func testVideoStatsEmit() { - let videoStats = .template - |> ProjectStatsEnvelope.VideoStats.lens.externalCompletions .~ 1_000 - |> ProjectStatsEnvelope.VideoStats.lens.externalStarts .~ 2_000 - |> ProjectStatsEnvelope.VideoStats.lens.internalCompletions .~ 2_000 - |> ProjectStatsEnvelope.VideoStats.lens.internalStarts .~ 3_000 - - self.vm.inputs.configureWith(videoStats: videoStats) - - self.completionPercentage.assertValues(["60% of plays completed"], "Floored completion percent emits.") - self.externalStartCount.assertValues(["2,000"], "Formatted external start count emits.") - self.externalStartProgress.assertValues([40], "External start percentage float value emits.") - self.internalStartCount.assertValues(["3,000"], "Formatted internal start count emits.") - self.internalStartProgress.assertValues([60], "Internal start percentage float value emits.") - self.totalStartCount.assertValues(["5,000 total plays"], "Formatted total start count emits.") - } -} diff --git a/Library/ViewModels/DashboardViewModel.swift b/Library/ViewModels/DashboardViewModel.swift deleted file mode 100644 index 7ef421c70a..0000000000 --- a/Library/ViewModels/DashboardViewModel.swift +++ /dev/null @@ -1,430 +0,0 @@ -import KsApi -import Prelude -import ReactiveExtensions -import ReactiveSwift - -public enum DrawerState { - case open - case closed - - public var toggled: DrawerState { - return self == .open ? .closed : .open - } -} - -public struct DashboardTitleViewData { - public let drawerState: DrawerState - public let isArrowHidden: Bool - public let currentProjectIndex: Int -} - -public struct ProjectsDrawerData { - public let project: Project - public let indexNum: Int - public let isChecked: Bool -} - -public protocol DashboardViewModelInputs { - /// Call to navigate to activities for project with id - func activitiesNavigated(projectId: Param) - - /// Call to switch display to another project from the drawer. - func `switch`(toProject param: Param) - - /// Call when the projects drawer has animated out. - func dashboardProjectsDrawerDidAnimateOut() - - /// Call to open project messages thread - func messagesCellTapped() - - /// Call to open message thread for specific project - func messageThreadNavigated(projectId: Param, messageThread: MessageThread) - - /// Call when the project context cell is tapped. - func projectContextCellTapped() - - /// Call when to show or hide the projects drawer. - func showHideProjectsDrawer() - - /// Call when Post Update is clicked - func trackPostUpdateClicked() - - /// Call when the view loads. - func viewDidLoad() - - /// Call when the view will appear. - func viewWillAppear(animated: Bool) - - /// Call when the view will disappear - func viewWillDisappear() -} - -public protocol DashboardViewModelOutputs { - /// Emits when should animate out projects drawer. - var animateOutProjectsDrawer: Signal<(), Never> { get } - - /// Emits when should dismiss projects drawer. - var dismissProjectsDrawer: Signal<(), Never> { get } - - /// Emits when to focus the screen reader on the titleView. - var focusScreenReaderOnTitleView: Signal<(), Never> { get } - - /// Emits the funding stats and project to be displayed in the funding cell. - var fundingData: Signal< - ( - funding: [ProjectStatsEnvelope.FundingDateStats], - project: Project - ), Never - > { get } - - /// Emits when navigating to project activities - var goToActivities: Signal { get } - - /// Emits when to go to project messages thread - var goToMessages: Signal { get } - - /// Emits when opening specific project message thread - var goToMessageThread: Signal<(Project, MessageThread), Never> { get } - - /// Emits when to go to the project page. - var goToProject: Signal<(Project, RefTag), Never> { get } - - /// Emits when should present projects drawer with data to populate it. - var presentProjectsDrawer: Signal<[ProjectsDrawerData], Never> { get } - - /// Emits the currently selected project to display in the context and action cells. - var project: Signal { get } - - /// Emits a boolean that determines if projects are currently loading. - var loaderIsAnimating: Signal { get } - - /// Emits the cumulative, project, and referreral distribution data to display in the referrers cell. - var referrerData: Signal< - ( - cumulative: ProjectStatsEnvelope.CumulativeStats, - project: Project, aggregates: ProjectStatsEnvelope.ReferralAggregateStats, - stats: [ProjectStatsEnvelope.ReferrerStats] - ), Never - > { get } - - /// Emits the project, reward stats, and cumulative pledges to display in the rewards cell. - var rewardData: Signal<(stats: [ProjectStatsEnvelope.RewardStats], project: Project), Never> { get } - - /// Emits the video stats to display in the video cell. - var videoStats: Signal { get } - - /// Emits data for the title view. - var updateTitleViewData: Signal { get } -} - -public protocol DashboardViewModelType { - var inputs: DashboardViewModelInputs { get } - var outputs: DashboardViewModelOutputs { get } -} - -public final class DashboardViewModel: DashboardViewModelInputs, DashboardViewModelOutputs, - DashboardViewModelType { - public init() { - let projects = self.viewWillAppearAnimatedProperty.signal.ignoreValues() - .switchMap { - AppEnvironment.current.apiService.fetchProjects(member: true) - .ksr_delay(AppEnvironment.current.apiDelayInterval, on: AppEnvironment.current.scheduler) - .demoteErrors() - .map { $0.projects } - .prefix(value: []) - } - - let selectedProjectProducer = SignalProducer.merge( - self.switchToProjectProperty.producer, - self.activitiesNavigatedProperty.producer, - self.messageThreadNavigatedProperty.producer.skipNil().map(first) - ) - - /* Interim MutableProperty used to default to first project on viewWillAppear - * and to subsequently switch to the selected project. - */ - let selectProjectPropertyOrFirst = MutableProperty(nil) - - selectProjectPropertyOrFirst <~ SignalProducer.combineLatest( - selectedProjectProducer, - self.viewWillAppearAnimatedProperty.producer.ignoreValues() - ) - .map(first) - .skipRepeats { lhs, rhs in lhs == rhs } - - let projectsAndSelected = projects - .switchMap { projects in - selectProjectPropertyOrFirst.producer - .map { param -> Project? in - param.flatMap { find(projectForParam: $0, in: projects) } ?? projects.first - } - .skipNil() - .map { (projects, $0) } - } - - self.project = projectsAndSelected.map(second) - - self.loaderIsAnimating = Signal.merge( - self.viewDidLoadProperty.signal.map(const(true)), - projects.filter { !$0.isEmpty }.map(const(false)) - ).skipRepeats() - - /* Interim MutableProperty used to inject nil on viewWillDisappear - * in order to ensure that same MessageThread is not navigated to again - * on viewWillAppear as projects will refresh each time. - */ - let messageThreadReceived = MutableProperty<(Param, MessageThread)?>(nil) - - messageThreadReceived <~ Signal.merge( - self.viewWillDisappearProperty.signal.mapConst(nil), - self.messageThreadNavigatedProperty.signal - ) - - self.goToMessageThread = self.project - .switchMap { project in - messageThreadReceived.producer - .skipNil() - .filter { $0.0 == .id(project.id) } - .map { (project, $1) } - } - - /* Interim MutableProperty used to inject nil on viewWillDisappear - * in order to ensure that same navigateToActivities is not navigated to again - * on viewWillAppear as projects will refresh each time. - */ - let navigateToActivitiesReceived = MutableProperty(nil) - - navigateToActivitiesReceived <~ Signal.merge( - self.viewWillDisappearProperty.signal.mapConst(nil), - self.activitiesNavigatedProperty.signal - ) - - self.goToActivities = self.project - .switchMap { project in - navigateToActivitiesReceived.producer - .skipNil() - .filter { $0 == .id(project.id) } - .map { _ in project } - } - - let selectedProjectAndStatsEvent = self.project - .switchMap { project in - AppEnvironment.current.apiService.fetchProjectStats(projectId: project.id) - .ksr_delay(AppEnvironment.current.apiDelayInterval, on: AppEnvironment.current.scheduler) - .map { (project, $0) } - .materialize() - } - - let selectedProjectAndStats = selectedProjectAndStatsEvent.values() - - self.fundingData = selectedProjectAndStats - .map { project, stats in - (funding: stats.fundingDistribution, project: project) - } - - self.referrerData = selectedProjectAndStats - .map { project, stats in - ( - cumulative: stats.cumulativeStats, project: project, - aggregates: stats.referralAggregateStats, stats: stats.referralDistribution - ) - } - - self.videoStats = selectedProjectAndStats.map { _, stats in stats.videoStats }.skipNil() - - self.rewardData = selectedProjectAndStats - .map { project, stats in - (stats: stats.rewardDistribution, project: project) - } - - let drawerStateProjectsAndSelectedProject = Signal.merge( - projectsAndSelected.map { ($0, $1, false) }, - projectsAndSelected - .takeWhen(self.showHideProjectsDrawerProperty.signal).map { ($0, $1, true) } - ) - .scan(nil) { (data, projectsProjectToggle) -> (DrawerState, [Project], Project)? in - let (projects, project, toggle) = projectsProjectToggle - - return ( - toggle ? (data?.0.toggled ?? DrawerState.closed) : DrawerState.closed, - projects, - project - ) - } - .skipNil() - - self.updateTitleViewData = drawerStateProjectsAndSelectedProject - .map { drawerState, projects, selectedProject in - DashboardTitleViewData( - drawerState: drawerState, - isArrowHidden: projects.count <= 1, - currentProjectIndex: projects.firstIndex(of: selectedProject) ?? 0 - ) - } - - let updateDrawerStateToOpen = self.updateTitleViewData - .map { $0.drawerState == .open } - .skip(first: 1) - - self.presentProjectsDrawer = drawerStateProjectsAndSelectedProject - .filter { drawerState, _, _ in drawerState == .open } - .map { _, projects, selectedProject in - projects.map { project in - ProjectsDrawerData( - project: project, - indexNum: projects.firstIndex(of: project) ?? 0, - isChecked: project == selectedProject - ) - } - } - - self.animateOutProjectsDrawer = updateDrawerStateToOpen - .filter(isFalse) - .ignoreValues() - - self.dismissProjectsDrawer = self.projectsDrawerDidAnimateOutProperty.signal - - self.goToProject = self.project - .takeWhen(self.projectContextCellTappedProperty.signal) - .map { ($0, RefTag.dashboard) } - - self.goToMessages = self.project - .takeWhen(self.messagesCellTappedProperty.signal) - - self.focusScreenReaderOnTitleView = self.viewWillAppearAnimatedProperty.signal.ignoreValues() - .filter { AppEnvironment.current.isVoiceOverRunning() } - - // MARK: - Tracking - - self.viewWillAppearAnimatedProperty.signal.observeValues { _ in - AppEnvironment.current.ksrAnalytics.trackCreatorDashboardPageViewed() - } - - _ = projects - .takePairWhen(self.switchToProjectProperty.signal) - .map { allProjects, param -> Project? in - param.flatMap { find(projectForParam: $0, in: allProjects) } - } - .skipNil() - .observeValues { switchedToProject in - AppEnvironment.current.ksrAnalytics - .trackCreatorDashboardSwitchProjectClicked(project: switchedToProject, refTag: RefTag.dashboard) - } - - _ = self.project - .takePairWhen(self.trackPostUpdateClickedProperty.signal) - .observeValues { project, _ in - AppEnvironment.current.ksrAnalytics.trackCreatorDashboardPostUpdateClicked( - project: project, - refTag: RefTag.dashboard - ) - } - } - - fileprivate let showHideProjectsDrawerProperty = MutableProperty(()) - public func showHideProjectsDrawer() { - self.showHideProjectsDrawerProperty.value = () - } - - fileprivate let projectContextCellTappedProperty = MutableProperty(()) - public func projectContextCellTapped() { - self.projectContextCellTappedProperty.value = () - } - - fileprivate let switchToProjectProperty = MutableProperty(nil) - public func `switch`(toProject param: Param) { - self.switchToProjectProperty.value = param - } - - fileprivate let activitiesNavigatedProperty = MutableProperty(nil) - public func activitiesNavigated(projectId: Param) { - self.activitiesNavigatedProperty.value = projectId - } - - fileprivate let messageThreadNavigatedProperty = MutableProperty<(Param, MessageThread)?>(nil) - public func messageThreadNavigated(projectId: Param, messageThread: MessageThread) { - self.messageThreadNavigatedProperty.value = (projectId, messageThread) - } - - fileprivate let projectsDrawerDidAnimateOutProperty = MutableProperty(()) - public func dashboardProjectsDrawerDidAnimateOut() { - self.projectsDrawerDidAnimateOutProperty.value = () - } - - fileprivate let trackPostUpdateClickedProperty = MutableProperty(()) - public func trackPostUpdateClicked() { - self.trackPostUpdateClickedProperty.value = () - } - - fileprivate let viewDidLoadProperty = MutableProperty(()) - public func viewDidLoad() { - self.viewDidLoadProperty.value = () - } - - fileprivate let viewWillAppearAnimatedProperty = MutableProperty(false) - public func viewWillAppear(animated: Bool) { - self.viewWillAppearAnimatedProperty.value = animated - } - - fileprivate let viewWillDisappearProperty = MutableProperty(()) - public func viewWillDisappear() { - self.viewWillDisappearProperty.value = () - } - - fileprivate let messagesCellTappedProperty = MutableProperty(()) - public func messagesCellTapped() { - self.messagesCellTappedProperty.value = () - } - - public let animateOutProjectsDrawer: Signal<(), Never> - public let dismissProjectsDrawer: Signal<(), Never> - public let focusScreenReaderOnTitleView: Signal<(), Never> - public let fundingData: Signal< - ( - funding: [ProjectStatsEnvelope.FundingDateStats], - project: Project - ), Never - > - public let goToActivities: Signal - public let goToMessages: Signal - public let goToMessageThread: Signal<(Project, MessageThread), Never> - public let goToProject: Signal<(Project, RefTag), Never> - public let project: Signal - public let loaderIsAnimating: Signal - public let presentProjectsDrawer: Signal<[ProjectsDrawerData], Never> - public let referrerData: Signal< - ( - cumulative: ProjectStatsEnvelope.CumulativeStats, - project: Project, aggregates: ProjectStatsEnvelope.ReferralAggregateStats, - stats: [ProjectStatsEnvelope.ReferrerStats] - ), Never - > - public let rewardData: Signal<(stats: [ProjectStatsEnvelope.RewardStats], project: Project), Never> - public let videoStats: Signal - public let updateTitleViewData: Signal - - public var inputs: DashboardViewModelInputs { return self } - public var outputs: DashboardViewModelOutputs { return self } -} - -extension ProjectsDrawerData: Equatable {} -public func == (lhs: ProjectsDrawerData, rhs: ProjectsDrawerData) -> Bool { - return lhs.project.id == rhs.project.id -} - -extension DashboardTitleViewData: Equatable {} -public func == (lhs: DashboardTitleViewData, rhs: DashboardTitleViewData) -> Bool { - return lhs.drawerState == rhs.drawerState && - lhs.currentProjectIndex == rhs.currentProjectIndex && - lhs.isArrowHidden == rhs.isArrowHidden -} - -private func find(projectForParam param: Param?, in projects: [Project]) -> Project? { - guard let param = param else { return nil } - - return projects.first { project in - if case .id(project.id) = param { return true } - if case .slug(project.slug) = param { return true } - return false - } -} diff --git a/Library/ViewModels/DashboardViewModelTests.swift b/Library/ViewModels/DashboardViewModelTests.swift deleted file mode 100644 index 9e166c4791..0000000000 --- a/Library/ViewModels/DashboardViewModelTests.swift +++ /dev/null @@ -1,402 +0,0 @@ -@testable import KsApi -@testable import Library -import Prelude -import ReactiveExtensions -import ReactiveExtensions_TestHelpers -import ReactiveSwift -import XCTest - -internal final class DashboardViewModelTests: TestCase { - internal let vm: DashboardViewModelType = DashboardViewModel() - - internal let animateOutProjectsDrawer = TestObserver<(), Never>() - internal let dismissProjectsDrawer = TestObserver<(), Never>() - internal let focusScreenReaderOnTitleView = TestObserver<(), Never>() - internal let fundingStats = TestObserver<[ProjectStatsEnvelope.FundingDateStats], Never>() - internal let goToMessageThread = TestObserver() - internal let loaderIsAnimating = TestObserver() - internal let presentProjectsDrawer = TestObserver<[ProjectsDrawerData], Never>() - internal let project = TestObserver() - internal let referrerCumulativeStats = TestObserver() - internal let referrerStats = TestObserver<[ProjectStatsEnvelope.ReferrerStats], Never>() - internal let rewardStats = TestObserver<[ProjectStatsEnvelope.RewardStats], Never>() - internal let updateTitleViewData = TestObserver() - internal let videoStats = TestObserver() - - let project1 = Project.template - let project2 = .template |> Project.lens.id .~ 4 - - internal override func setUp() { - super.setUp() - self.vm.outputs.animateOutProjectsDrawer.observe(self.animateOutProjectsDrawer.observer) - self.vm.outputs.dismissProjectsDrawer.observe(self.dismissProjectsDrawer.observer) - self.vm.outputs.focusScreenReaderOnTitleView.observe(self.focusScreenReaderOnTitleView.observer) - self.vm.outputs.fundingData.map { stats, _ in stats }.observe(self.fundingStats.observer) - self.vm.outputs.goToMessageThread.map { $0.0 }.observe(self.goToMessageThread.observer) - self.vm.outputs.loaderIsAnimating.observe(self.loaderIsAnimating.observer) - self.vm.outputs.presentProjectsDrawer.observe(self.presentProjectsDrawer.observer) - self.vm.outputs.project.observe(self.project.observer) - self.vm.outputs.referrerData - .map { cumulative, _, _, _ in cumulative } - .observe(self.referrerCumulativeStats.observer) - self.vm.outputs.referrerData.map { _, _, _, stats in stats }.observe(self.referrerStats.observer) - self.vm.outputs.rewardData.map { stats, _ in stats }.observe(self.rewardStats.observer) - self.vm.outputs.videoStats.observe(self.videoStats.observer) - self.vm.outputs.updateTitleViewData.observe(self.updateTitleViewData.observer) - } - - func testScreenReaderFocus() { - let projects = [Project.template] - - let mockApiService = MockService(fetchProjectsResponse: projects) - withEnvironment(apiService: mockApiService, isVoiceOverRunning: { true }) { - self.focusScreenReaderOnTitleView.assertValueCount(0) - - self.vm.inputs.viewWillAppear(animated: false) - - self.focusScreenReaderOnTitleView.assertValueCount(1) - - self.vm.inputs.viewWillAppear(animated: false) - - self.focusScreenReaderOnTitleView.assertValueCount(2) - } - } - - func testProject() { - let projects = (0...4).map { .template |> Project.lens.id .~ $0 } - let titleViewData = DashboardTitleViewData( - drawerState: DrawerState.closed, - isArrowHidden: false, - currentProjectIndex: 0 - ) - - withEnvironment(apiService: MockService(fetchProjectsResponse: projects)) { - self.vm.inputs.viewWillAppear(animated: false) - - self.project.assertValueCount(0) - self.updateTitleViewData.assertValueCount(0) - - self.scheduler.advance() - - self.project.assertValues([.template |> Project.lens.id .~ 0]) - self.updateTitleViewData.assertValues([titleViewData], "Update title data") - - self.fundingStats.assertValueCount(1) - - let updatedProjects = (0...4).map { - .template - |> Project.lens.id .~ $0 - |> Project.lens.name %~ { "\($0)" + " (updated)" } - } - - withEnvironment(apiService: MockService(fetchProjectsResponse: updatedProjects)) { - self.vm.inputs.viewWillAppear(animated: false) - self.scheduler.advance() - - self.project.assertValueCount(2) - XCTAssertEqual("\(projects[0].name) (updated)", self.project.values.last!.name) - - self.fundingStats.assertValueCount(2) - } - } - } - - func testTitleData_ForOneProject() { - let projects = [Project.template] - let titleViewData = DashboardTitleViewData( - drawerState: DrawerState.closed, - isArrowHidden: true, - currentProjectIndex: 0 - ) - - withEnvironment(apiService: MockService(fetchProjectsResponse: projects)) { - self.vm.inputs.viewWillAppear(animated: false) - - self.updateTitleViewData.assertValueCount(0) - - self.scheduler.advance() - - self.updateTitleViewData.assertValues([titleViewData], "Update title data") - } - } - - func testLoaderIsAnimating() { - let projects = (0...4).map { .template |> Project.lens.id .~ $0 } - - withEnvironment(apiService: MockService(fetchProjectsResponse: projects)) { - self.vm.inputs.viewDidLoad() - self.vm.inputs.viewWillAppear(animated: false) - self.loaderIsAnimating.assertValues([true]) - - self.scheduler.advance() - self.loaderIsAnimating.assertValues([true, false]) - } - } - - func testProjectStatsEmit() { - let projects = [Project.template] - let projects2 = projects + [.template |> Project.lens.id .~ 5] - - let statsEnvelope = .template - |> ProjectStatsEnvelope.lens.cumulativeStats .~ .template - |> ProjectStatsEnvelope.lens.fundingDistribution .~ [.template] - |> ProjectStatsEnvelope.lens.referralDistribution .~ [.template] - |> ProjectStatsEnvelope.lens.rewardDistribution .~ [.template, .template] - |> ProjectStatsEnvelope.lens.videoStats .~ .template - - let statsEnvelope2 = .template - |> ProjectStatsEnvelope.lens.cumulativeStats .~ .template - |> ProjectStatsEnvelope.lens.fundingDistribution .~ [.template] - |> ProjectStatsEnvelope.lens.referralDistribution .~ [.template, .template, .template] - |> ProjectStatsEnvelope.lens.rewardDistribution .~ [.template] - |> ProjectStatsEnvelope.lens.videoStats .~ nil - - withEnvironment(apiService: MockService( - fetchProjectsResponse: projects, - fetchProjectStatsResponse: statsEnvelope - )) { - self.vm.inputs.viewWillAppear(animated: false) - - self.videoStats.assertValueCount(0) - self.fundingStats.assertValueCount(0) - self.referrerCumulativeStats.assertValueCount(0) - self.referrerStats.assertValueCount(0) - self.rewardStats.assertValueCount(0) - - self.scheduler.advance() - - self.fundingStats.assertValues([[.template]], "Funding stats emitted.") - self.referrerCumulativeStats.assertValues([.template], "Cumulative stats emitted.") - self.referrerStats.assertValues([[.template]], "Referrer stats emitted.") - self.rewardStats.assertValues([[.template, .template]], "Reward stats emitted.") - self.videoStats.assertValues([.template], "Video stats emitted.") - - withEnvironment(apiService: MockService( - fetchProjectsResponse: projects2, - fetchProjectStatsResponse: statsEnvelope2 - )) { - self.vm.inputs.viewWillAppear(animated: false) - self.scheduler.advance() - - self.fundingStats.assertValues([[.template], [.template]], "Funding stats emitted.") - self.referrerCumulativeStats.assertValues([.template, .template], "Cumulative stats emitted.") - self.referrerStats.assertValues( - [[.template], [.template, .template, .template]], - "Referrer stats emitted." - ) - self.rewardStats.assertValues([[.template, .template], [.template]], "Reward stats emitted.") - self.videoStats.assertValues([.template], "Video stats does not emit") - } - } - } - - func testDeepLink() { - let projects = (0...4).map { .template |> Project.lens.id .~ $0 } - - withEnvironment(apiService: MockService(fetchProjectsResponse: projects)) { - self.vm.inputs.switch(toProject: .id(projects.last!.id)) - self.vm.inputs.viewWillAppear(animated: false) - self.scheduler.advance() - - self.project.assertValues([projects.last!]) - } - } - - func testGoToThread() { - let projects = (0...4).map { .template |> Project.lens.id .~ $0 } - let thread = MessageThread.template - - let threadProj = projects[1] - - withEnvironment(apiService: MockService(fetchProjectsResponse: projects)) { - self.project.assertValues([]) - - self.vm.inputs.messageThreadNavigated(projectId: .id(threadProj.id), messageThread: thread) - self.project.assertValues([]) - - self.vm.inputs.viewWillAppear(animated: false) - self.scheduler.advance() - - self.goToMessageThread.assertValues([threadProj], "Go to message thread emitted") - self.project.assertValues([threadProj], "Thread project is selected") - - self.vm.inputs.viewWillDisappear() - self.scheduler.advance() - - self.vm.inputs.viewWillAppear(animated: false) - self.scheduler.advance() - - self.goToMessageThread.assertValues( - [threadProj], - "Go to message thread not emitted again when view appears" - ) - - self.project.assertValues( - [threadProj, threadProj], - "Keep previously selected project when view Appears" - ) - } - } - - func testProjectsDrawer_OpenClose() { - let project1 = Project.template - let project2 = .template |> Project.lens.id .~ 4 - let projects = [project1, project2] - let projectData1 = ProjectsDrawerData(project: project1, indexNum: 0, isChecked: true) - let projectData2 = ProjectsDrawerData(project: project2, indexNum: 1, isChecked: false) - - let titleViewDataClosed1 = DashboardTitleViewData( - drawerState: DrawerState.closed, - isArrowHidden: false, - currentProjectIndex: 0 - ) - - let titleViewDataOpen1 = DashboardTitleViewData( - drawerState: DrawerState.open, - isArrowHidden: false, - currentProjectIndex: 0 - ) - - let titleViewDataClosed2 = DashboardTitleViewData( - drawerState: DrawerState.closed, - isArrowHidden: false, - currentProjectIndex: 1 - ) - - let titleViewDataOpen2 = DashboardTitleViewData( - drawerState: DrawerState.open, - isArrowHidden: false, - currentProjectIndex: 1 - ) - - withEnvironment(apiService: MockService(fetchProjectsResponse: projects)) { - self.vm.inputs.viewWillAppear(animated: false) - self.scheduler.advance() - - self.updateTitleViewData.assertValues([titleViewDataClosed1], "Update title with closed data") - - self.vm.inputs.showHideProjectsDrawer() - - self.updateTitleViewData.assertValues( - [titleViewDataClosed1, titleViewDataOpen1], - "Update title with open data" - ) - self.presentProjectsDrawer.assertValues([[projectData1, projectData2]]) - self.dismissProjectsDrawer.assertValueCount(0) - self.animateOutProjectsDrawer.assertValueCount(0) - - self.vm.inputs.showHideProjectsDrawer() - - self.updateTitleViewData.assertValues( - [titleViewDataClosed1, titleViewDataOpen1, titleViewDataClosed1], - "Update title with closed data" - ) - self.animateOutProjectsDrawer.assertValueCount(1) - self.dismissProjectsDrawer.assertValueCount(0) - - self.vm.inputs.dashboardProjectsDrawerDidAnimateOut() - - self.dismissProjectsDrawer.assertValueCount(1) - - self.vm.inputs.showHideProjectsDrawer() - - self.updateTitleViewData.assertValues([ - titleViewDataClosed1, titleViewDataOpen1, titleViewDataClosed1, - titleViewDataOpen1 - ], "Update title with open data") - self.presentProjectsDrawer.assertValues([[projectData1, projectData2], [projectData1, projectData2]]) - - self.vm.inputs.switch(toProject: .id(project2.id)) - - self.updateTitleViewData.assertValues([ - titleViewDataClosed1, titleViewDataOpen1, titleViewDataClosed1, - titleViewDataOpen1, titleViewDataClosed2 - ], "Update title with closed data") - self.animateOutProjectsDrawer.assertValueCount(2, "Animate out drawer emits") - self.dismissProjectsDrawer.assertValueCount(1, "Dismiss drawer does not emit") - - self.vm.inputs.dashboardProjectsDrawerDidAnimateOut() - - self.dismissProjectsDrawer.assertValueCount(2) - - self.vm.inputs.showHideProjectsDrawer() - - self.updateTitleViewData.assertValues([ - titleViewDataClosed1, titleViewDataOpen1, titleViewDataClosed1, - titleViewDataOpen1, titleViewDataClosed2, titleViewDataOpen2 - ], "Update title with open data") - self.presentProjectsDrawer.assertValues([ - [projectData1, projectData2], [projectData1, projectData2], - [projectData1, projectData2] - ]) - self.animateOutProjectsDrawer.assertValueCount(2, "Animate out drawer emits") - self.dismissProjectsDrawer.assertValueCount(2, "Dismiss drawer does not emit") - - self.vm.inputs.showHideProjectsDrawer() - } - } - - func testTrackingEvents_CreatorDashboardViewed() { - let projects = (0...4).map { .template |> Project.lens.id .~ $0 } - - withEnvironment(apiService: MockService(fetchProjectsResponse: projects)) { - XCTAssertEqual([], self.segmentTrackingClient.events) - - self.vm.inputs.viewWillAppear(animated: false) - - XCTAssertEqual(["Page Viewed"], self.segmentTrackingClient.events) - - self.vm.inputs.viewWillDisappear() - - XCTAssertEqual(["Page Viewed"], self.segmentTrackingClient.events) - - self.vm.inputs.viewWillAppear(animated: false) - - XCTAssertEqual(["Page Viewed", "Page Viewed"], self.segmentTrackingClient.events) - } - } - - func testTrackingEvents_CreatorDashboardSwitchProjectClicked() { - let project1 = Project.template - let project2 = .template |> Project.lens.id .~ 4 - let projects = [project1, project2] - - withEnvironment(apiService: MockService(fetchProjectsResponse: projects)) { - XCTAssertEqual([], self.segmentTrackingClient.events) - - self.vm.inputs.viewWillAppear(animated: false) - - self.scheduler.advance() - - self.project.assertValues([project1]) - XCTAssertEqual(["Page Viewed"], self.segmentTrackingClient.events) - - self.vm.inputs.switch(toProject: .id(project2.id)) - - self.project.assertValues([project1, project2]) - XCTAssertEqual(["Page Viewed", "CTA Clicked"], self.segmentTrackingClient.events) - - self.vm.inputs.switch(toProject: .id(project1.id)) - - self.project - .assertValues([project1, project2, project1]) - XCTAssertEqual(["Page Viewed", "CTA Clicked", "CTA Clicked"], self.segmentTrackingClient.events) - } - } - - func testTrackingEvents_CreatorDashboardPostUpdateClicked() { - withEnvironment(apiService: MockService(fetchProjectsResponse: [Project.template])) { - XCTAssertEqual([], self.segmentTrackingClient.events) - - self.vm.inputs.viewWillAppear(animated: false) - - self.scheduler.advance() - - XCTAssertEqual(["Page Viewed"], self.segmentTrackingClient.events) - - self.vm.inputs.trackPostUpdateClicked() - - XCTAssertEqual(["Page Viewed", "CTA Clicked"], self.segmentTrackingClient.events) - } - } -} diff --git a/Library/ViewModels/ProjectActivitiesViewModel.swift b/Library/ViewModels/ProjectActivitiesViewModel.swift deleted file mode 100644 index d28e6bdb8e..0000000000 --- a/Library/ViewModels/ProjectActivitiesViewModel.swift +++ /dev/null @@ -1,231 +0,0 @@ -import KsApi -import Prelude -import ReactiveExtensions -import ReactiveSwift - -public enum ProjectActivitiesGoTo { - case backing(ManagePledgeViewParamConfigData) - case comments(Project?, Update?) - case project(Project) - case sendMessage(Backing, KSRAnalytics.MessageDialogContext) - case sendReply(Project, Update?, ActivityComment) - case update(Project, Update) -} - -public protocol ProjectActivitiesViewModelInputs { - /// Call when a cell containing an activity and project is tapped. - func activityAndProjectCellTapped(activity: Activity, project: Project) - - /// Call to set project. - func configureWith(_ project: Project) - - /// Call when the backing cell's backing button is pressed. - func projectActivityBackingCellGoToBacking(project: Project, backing: Backing) - - /// Call when the backing cell's send message button is pressed. - func projectActivityBackingCellGoToSendMessage(project: Project, backing: Backing) - - /// Call when the comment cell's backing button is pressed. - func projectActivityCommentCellGoToBacking(project: Project, user: User) - - /// Call when the comment cell's reply button is pressed. - func projectActivityCommentCellGoToSendReply(project: Project, update: Update?, comment: ActivityComment) - - /// Call when pull-to-refresh is invoked. - func refresh() - - /// Call when the view loads. - func viewDidLoad() - - /** - Call from the controller's `tableView:willDisplayCell:forRowAtIndexPath` method. - - - parameter row: The 0-based index of the row displaying. - - parameter totalRows: The total number of rows in the table view. - */ - func willDisplayRow(_ row: Int, outOf totalRows: Int) -} - -public protocol ProjectActivitiesViewModelOutputs { - /// Emits when another screen should be loaded. - var goTo: Signal { get } - - /// Emits a boolean that indicates whether the view is refreshing. - var isRefreshing: Signal { get } - - /// Emits project activity data. - var projectActivityData: Signal { get } - - /// Emits `true` when the empty state should be shown, and `false` when it should be hidden. - var showEmptyState: Signal { get } -} - -public protocol ProjectActivitiesViewModelType { - var inputs: ProjectActivitiesViewModelInputs { get } - var outputs: ProjectActivitiesViewModelOutputs { get } -} - -public final class ProjectActivitiesViewModel: ProjectActivitiesViewModelType, - ProjectActivitiesViewModelInputs, ProjectActivitiesViewModelOutputs { - public init() { - let project = self.projectProperty.signal.skipNil() - - let isCloseToBottom = self.willDisplayRowProperty.signal.skipNil() - .map { row, total in row >= total - 3 } - .skipRepeats() - .filter(isTrue) - .ignoreValues() - - let requestFirstPage = project - .takeWhen( - .merge( - self.viewDidLoadProperty.signal, - self.refreshProperty.signal - ) - ) - - let activities: Signal<[Activity], Never> - (activities, self.isRefreshing, _, _) = paginate( - requestFirstPageWith: requestFirstPage, - requestNextPageWhen: isCloseToBottom, - clearOnNewRequest: false, - valuesFromEnvelope: { $0.activities }, - cursorFromEnvelope: { $0.urls.api.moreActivities }, - requestFromParams: { AppEnvironment.current.apiService.fetchProjectActivities(forProject: $0) }, - requestFromCursor: { AppEnvironment.current.apiService.fetchProjectActivities(paginationUrl: $0) } - ) - - self.projectActivityData = Signal.combineLatest(activities, project) - .map { activities, project in - ProjectActivityData( - activities: activities, - project: project, - groupedDates: !AppEnvironment.current.isVoiceOverRunning() - ) - } - - self.showEmptyState = activities - .map { $0.isEmpty } - .skipRepeats() - - let cellTappedGoTo = self.activityAndProjectCellTappedProperty.signal.skipNil() - .flatMap { activity, project -> SignalProducer in - switch activity.category { - case .backing, .backingAmount, .backingCanceled, .backingReward: - guard let params = backingParams(project: project, activity: activity) else { return .empty } - return .init(value: params) - case .commentProject: - return .init(value: .comments(project, nil)) - case .commentPost: - return .init(value: .comments(nil, activity.update)) - case .launch, .success, .cancellation, .failure, .suspension: - return .init(value: .project(project)) - case .update: - guard let update = activity.update else { return .empty } - return .init(value: .update(project, update)) - case .backingDropped, .follow, .funding, .watch, .unknown: - assertionFailure("Unsupported activity: \(activity)") - return .empty - } - } - - let projectActivityBackingCellGoToBacking = self.projectActivityBackingCellGoToBackingProperty.signal - .skipNil() - .compactMap(backingParams(project:backing:)) - - let projectActivityBackingCellGoToSendMessage = - self.projectActivityBackingCellGoToSendMessageProperty.signal.skipNil() - .map { _, backing in - ProjectActivitiesGoTo.sendMessage(backing, KSRAnalytics.MessageDialogContext.creatorActivity) - } - - let projectActivityCommentCellGoToBacking = - self.projectActivityCommentCellGoToBackingProperty.signal.skipNil() - .switchMap { project, user in - AppEnvironment.current.apiService.fetchBacking(forProject: project, forUser: user) - .ksr_delay(AppEnvironment.current.apiDelayInterval, on: AppEnvironment.current.scheduler) - .map { backing in (project, backing) } - .materialize() - } - .values() - .compactMap(backingParams(project:backing:)) - - let projectActivityCommentCellGoToSendReply = - self.projectActivityCommentCellGoToSendReplyProperty.signal.skipNil() - .map(ProjectActivitiesGoTo.sendReply) - - self.goTo = Signal.merge( - cellTappedGoTo, - projectActivityBackingCellGoToBacking, - projectActivityBackingCellGoToSendMessage, - projectActivityCommentCellGoToBacking, - projectActivityCommentCellGoToSendReply - ) - } - - private let activityAndProjectCellTappedProperty = MutableProperty<(Activity, Project)?>(nil) - public func activityAndProjectCellTapped(activity: Activity, project: Project) { - self.activityAndProjectCellTappedProperty.value = (activity, project) - } - - private let projectProperty = MutableProperty(nil) - public func configureWith(_ project: Project) { self.projectProperty.value = project } - - private let projectActivityBackingCellGoToBackingProperty = MutableProperty<(Project, Backing)?>(nil) - public func projectActivityBackingCellGoToBacking(project: Project, backing: Backing) { - self.projectActivityBackingCellGoToBackingProperty.value = (project, backing) - } - - private let projectActivityBackingCellGoToSendMessageProperty = MutableProperty<(Project, Backing)?>(nil) - public func projectActivityBackingCellGoToSendMessage(project: Project, backing: Backing) { - self.projectActivityBackingCellGoToSendMessageProperty.value = (project, backing) - } - - private let projectActivityCommentCellGoToBackingProperty = MutableProperty<(Project, User)?>(nil) - public func projectActivityCommentCellGoToBacking(project: Project, user: User) { - self.projectActivityCommentCellGoToBackingProperty.value = (project, user) - } - - private let projectActivityCommentCellGoToSendReplyProperty - = MutableProperty<(Project, Update?, ActivityComment)?>(nil) - public func projectActivityCommentCellGoToSendReply( - project: Project, - update: Update?, - comment: ActivityComment - ) { - self.projectActivityCommentCellGoToSendReplyProperty.value = (project, update, comment) - } - - private let refreshProperty = MutableProperty(()) - public func refresh() { self.refreshProperty.value = () } - - private let viewDidLoadProperty = MutableProperty(()) - public func viewDidLoad() { self.viewDidLoadProperty.value = () } - - private let willDisplayRowProperty = MutableProperty<(row: Int, totalRows: Int)?>(nil) - public func willDisplayRow(_ row: Int, outOf totalRows: Int) { - self.willDisplayRowProperty.value = (row, totalRows) - } - - public let goTo: Signal - public let isRefreshing: Signal - public let projectActivityData: Signal - public let showEmptyState: Signal - - public var inputs: ProjectActivitiesViewModelInputs { return self } - public var outputs: ProjectActivitiesViewModelOutputs { return self } -} - -private func backingParams(project: Project, activity: Activity) -> ProjectActivitiesGoTo? { - let backingId = activity.memberData.backing?.id - - return ProjectActivitiesGoTo.backing( - (projectParam: Param.slug(project.slug), backingParam: backingId.flatMap(Param.id)) - ) -} - -private func backingParams(project: Project, backing: Backing) -> ProjectActivitiesGoTo? { - return ProjectActivitiesGoTo.backing( - (projectParam: Param.slug(project.slug), backingParam: Param.id(backing.id)) - ) -} diff --git a/Library/ViewModels/ProjectActivitiesViewModelTests.swift b/Library/ViewModels/ProjectActivitiesViewModelTests.swift deleted file mode 100644 index 1079d6f050..0000000000 --- a/Library/ViewModels/ProjectActivitiesViewModelTests.swift +++ /dev/null @@ -1,235 +0,0 @@ -@testable import KsApi -@testable import Library -import Prelude -import ReactiveExtensions_TestHelpers -import XCTest - -final class ProjectActivitiesViewModelTests: TestCase { - fileprivate let vm: ProjectActivitiesViewModelType = ProjectActivitiesViewModel() - - fileprivate let activitiesPresent = TestObserver() - fileprivate let goTo = TestObserver() - fileprivate let groupedDates = TestObserver() - fileprivate let isRefreshing = TestObserver() - fileprivate let project = TestObserver() - fileprivate let showEmptyState = TestObserver() - - private var sampleAuthor: ActivityCommentAuthor { - ActivityCommentAuthor( - avatar: .template, - id: 1, - name: "test", - urls: .template - ) - } - - private var sampleComment: ActivityComment { - ActivityComment( - author: sampleAuthor, - body: "Love this project!", - createdAt: .leastNonzeroMagnitude, - deletedAt: nil, - id: 1 - ) - } - - override func setUp() { - super.setUp() - - self.vm.outputs.projectActivityData - .map { !$0.activities.isEmpty } - .observe(self.activitiesPresent.observer) - self.vm.outputs.goTo.observe(self.goTo.observer) - self.vm.outputs.isRefreshing.observe(self.isRefreshing.observer) - self.vm.outputs.projectActivityData - .map { $0.project } - .observe(self.project.observer) - self.vm.outputs.projectActivityData - .map { $0.groupedDates } - .observe(self.groupedDates.observer) - self.vm.outputs.showEmptyState.observe(self.showEmptyState.observer) - - AppEnvironment.login(AccessTokenEnvelope(accessToken: "deadbeef", user: .template)) - } - - func testFlow() { - let project = Project.template - - withEnvironment(apiService: MockService( - fetchProjectActivitiesResponse: - [.template |> Activity.lens.id .~ 1] - )) { - self.vm.inputs.configureWith(project) - self.vm.inputs.viewDidLoad() - self.activitiesPresent.assertDidNotEmitValue("No activities") - self.scheduler.advance() - - self.activitiesPresent.assertValues([true], "Show activities after scheduler advances") - self.groupedDates.assertValues([true], "Group dates by default") - self.project.assertValues([project], "Emits project") - } - - withEnvironment(apiService: MockService( - fetchProjectActivitiesResponse: - [.template |> Activity.lens.id .~ 2] - )) { - self.vm.inputs.refresh() - self.scheduler.advance() - - self.activitiesPresent.assertValues([true, true], "Activities refreshed") - self.groupedDates.assertValues([true, true], "Group dates by default") - self.project.assertValues([project, project], "Emits project") - } - - withEnvironment(apiService: MockService( - fetchProjectActivitiesResponse: - [.template |> Activity.lens.id .~ 3] - )) { - self.vm.inputs.willDisplayRow(9, outOf: 10) - self.scheduler.advance() - - self.activitiesPresent.assertValues([true, true, true], "Activities paginate") - self.groupedDates.assertValues([true, true, true], "Group dates by default") - self.project.assertValues([project, project, project], "Emits project") - } - - self.showEmptyState.assertValues( - [false], - "Don't show, because each activity emission was a non-empty array" - ) - } - - func testEmptyState() { - let project = Project.template - - withEnvironment(apiService: MockService(fetchProjectActivitiesResponse: [])) { - self.vm.inputs.configureWith(project) - self.vm.inputs.viewDidLoad() - self.scheduler.advance() - - self.activitiesPresent.assertValues([false], "No activities") - self.showEmptyState.assertValues([true], "Activities not present, show empty state") - self.project.assertValues([project], "Emits project") - } - } - - func testGoTo() { - let backing = Backing.template |> Backing.lens.projectId .~ Project.template.id - let project = Project.template |> Project.lens.personalization.backing .~ backing - let comment = self.sampleComment - let update = Update.template - let user = User.template - - let backingActivity = .template - |> Activity.lens.category .~ .backing - |> Activity.lens.memberData.backing .~ backing - |> Activity.lens.project .~ project - |> Activity.lens.user .~ user - - let commentPostActivity = .template - |> Activity.lens.category .~ .commentPost - |> Activity.lens.comment .~ comment - |> Activity.lens.project .~ project - |> Activity.lens.update .~ update - |> Activity.lens.user .~ user - - let commentProjectActivity = .template - |> Activity.lens.category .~ .commentProject - |> Activity.lens.comment .~ comment - |> Activity.lens.project .~ project - |> Activity.lens.user .~ user - - let successActivity = .template - |> Activity.lens.category .~ .failure - |> Activity.lens.project .~ (project |> Project.lens.state .~ .successful) - |> Activity.lens.user .~ user - - let updateActivity = .template - |> Activity.lens.category .~ .update - |> Activity.lens.project .~ project - |> Activity.lens.update .~ update - |> Activity.lens.user .~ user - - withEnvironment(apiService: MockService( - fetchProjectActivitiesResponse: - [backingActivity, commentPostActivity, commentProjectActivity, successActivity, updateActivity] - )) { - self.vm.inputs.configureWith(project) - self.vm.inputs.viewDidLoad() - self.scheduler.advance() - - // Testing when cells are tapped for different categories of activity - - self.vm.inputs.activityAndProjectCellTapped(activity: backingActivity, project: project) - self.goTo.assertValueCount(1, "Should go to backing") - - self.vm.inputs.activityAndProjectCellTapped(activity: commentPostActivity, project: project) - self.goTo.assertValueCount(2, "Should go to comments for update") - - self.vm.inputs.activityAndProjectCellTapped(activity: commentProjectActivity, project: project) - self.goTo.assertValueCount(3, "Should go to comments for project") - - self.vm.inputs.activityAndProjectCellTapped(activity: successActivity, project: project) - self.goTo.assertValueCount(4, "Should go to project") - - self.vm.inputs.activityAndProjectCellTapped(activity: updateActivity, project: project) - self.goTo.assertValueCount(5, "Should go to update") - - // Testing delegate methods - - self.vm.inputs.projectActivityBackingCellGoToBacking(project: project, backing: backing) - self.goTo.assertValueCount(6, "Should go to backing") - - self.vm.inputs.projectActivityBackingCellGoToSendMessage(project: project, backing: backing) - self.goTo.assertValueCount(7, "Should go to send message") - - self.vm.inputs.projectActivityCommentCellGoToSendReply(project: project, update: nil, comment: comment) - self.goTo.assertValueCount(8, "Should go to comments for project") - - self.vm.inputs.projectActivityCommentCellGoToSendReply( - project: project, - update: update, - comment: comment - ) - self.goTo.assertValueCount(9, "Should go to comments for update") - - withEnvironment(apiService: MockService(fetchBackingResponse: .template)) { - self.vm.inputs.projectActivityCommentCellGoToBacking(project: project, user: user) - - self.scheduler.advance() - - self.goTo.assertValueCount(10, "Should go to backing after fetching backing") - } - } - } - - func testGroupedDatesWhenVoiceOverIsNotRunning() { - let project = Project.template - let activities = [.template |> Activity.lens.project .~ project] - - withEnvironment( - apiService: MockService(fetchProjectActivitiesResponse: activities), - isVoiceOverRunning: { false } - ) { - self.vm.inputs.configureWith(project) - self.vm.inputs.viewDidLoad() - self.scheduler.advance() - self.groupedDates.assertValues([true], "Group dates when VoiceOver is not running") - } - } - - func testGroupedDatesWhenVoiceOverIsRunning() { - let project = Project.template - let activities = [.template |> Activity.lens.project .~ project] - - withEnvironment( - apiService: MockService(fetchProjectActivitiesResponse: activities), - isVoiceOverRunning: { true } - ) { - self.vm.inputs.configureWith(project) - self.vm.inputs.viewDidLoad() - self.scheduler.advance() - self.groupedDates.assertValues([false], "Don't group dates when VoiceOver is running") - } - } -} diff --git a/Library/ViewModels/ProjectActivityBackingCellViewModel.swift b/Library/ViewModels/ProjectActivityBackingCellViewModel.swift deleted file mode 100644 index e0dffc98dc..0000000000 --- a/Library/ViewModels/ProjectActivityBackingCellViewModel.swift +++ /dev/null @@ -1,253 +0,0 @@ -import Foundation -import KsApi -import Prelude -import ReactiveExtensions -import ReactiveSwift - -public protocol ProjectActivityBackingCellViewModelInputs { - /// Call when the backing button is pressed. - func backingButtonPressed() - - /// Call to set the activity and project. - func configureWith(activity: Activity, project: Project) - - /// Call when the send message button is pressed. - func sendMessageButtonPressed() -} - -public protocol ProjectActivityBackingCellViewModelOutputs { - /// Emits a URL for the backer's avatar. - var backerImageURL: Signal { get } - - /// Emits the cell's accessibility label. - var cellAccessibilityLabel: Signal { get } - - /// Emits the cell's accessibility value. - var cellAccessibilityValue: Signal { get } - - /// Emits when the delegate should go to the backing screen. - var notifyDelegateGoToBacking: Signal<(Project, Backing), Never> { get } - - /// Emits when the delegate should go to the send message screen. - var notifyDelegateGoToSendMessage: Signal<(Project, Backing), Never> { get } - - /// Emits the new pledge amount. - var pledgeAmount: Signal { get } - - /// Emits whether the pledge amount label should be hidden. - var pledgeAmountLabelIsHidden: Signal { get } - - /// Emits whether the pledge amounts stack view should be hidden. - var pledgeAmountsStackViewIsHidden: Signal { get } - - /// Emits where the pledge details separator stack view should be hidden. - var pledgeDetailsSeparatorStackViewIsHidden: Signal { get } - - /// Emits the old pledge amount. - var previousPledgeAmount: Signal { get } - - /// Emits whether the previous pledge amount label should be hidden. - var previousPledgeAmountLabelIsHidden: Signal { get } - - /// Emits a description of the reward. - var reward: Signal { get } - - /// Emits whether the reward label should be hidden. - var rewardLabelIsHidden: Signal { get } - - /// Emits whether the send message button should be hidden. - var sendMessageButtonAndBulletSeparatorHidden: Signal { get } - - /// Emits the activity's title. - var title: Signal { get } -} - -public protocol ProjectActivityBackingCellViewModelType { - var inputs: ProjectActivityBackingCellViewModelInputs { get } - var outputs: ProjectActivityBackingCellViewModelOutputs { get } -} - -public final class ProjectActivityBackingCellViewModel: ProjectActivityBackingCellViewModelType, - ProjectActivityBackingCellViewModelInputs, ProjectActivityBackingCellViewModelOutputs { - public init() { - let activityAndProject = self.activityAndProjectProperty.signal.skipNil() - let activity = activityAndProject.map(first) - let title = activity.map(title(activity:)) - - self.backerImageURL = activityAndProject - .map { activity, _ in (activity.user?.avatar.medium).flatMap(URL.init) } - - self.cellAccessibilityLabel = title.map { title in title.htmlStripped() ?? "" } - - self.cellAccessibilityValue = activityAndProject - .flatMap { activity, project -> SignalProducer in - .init(value: accessibilityValue(activity: activity, project: project)) - } - - self.notifyDelegateGoToBacking = activityAndProject - .takeWhen(self.backingButtonPressedProperty.signal) - .flatMap { activity, project -> SignalProducer<(Project, Backing), Never> in - guard let backing = activity.memberData.backing else { return .empty } - return .init(value: (project, backing)) - } - - self.notifyDelegateGoToSendMessage = activityAndProject - .takeWhen(self.sendMessageButtonPressedProperty.signal) - .flatMap { activity, project -> SignalProducer<(Project, Backing), Never> in - guard let backing = activity.memberData.backing else { return .empty } - return .init(value: (project, backing)) - } - - self.pledgeAmount = activityAndProject - .map(amount(activity:project:)) - - let pledgeAmountLabelIsHidden = self.pledgeAmount - .map { $0.isEmpty } - - self.pledgeAmountLabelIsHidden = pledgeAmountLabelIsHidden.skipRepeats() - - self.previousPledgeAmount = activityAndProject - .map(oldAmount(activity:project:)) - - let previousPledgeAmountLabelIsHidden = self.previousPledgeAmount - .map { $0.isEmpty } - - self.previousPledgeAmountLabelIsHidden = previousPledgeAmountLabelIsHidden.skipRepeats() - - let pledgeAmountsStackViewIsHidden = Signal.zip( - pledgeAmountLabelIsHidden, - previousPledgeAmountLabelIsHidden - ) - .map { $0 && $1 } - .skipRepeats() - - self.pledgeAmountsStackViewIsHidden = pledgeAmountsStackViewIsHidden - - self.reward = activityAndProject.map(rewardSummary(activity:project:)) - - let rewardLabelIsHidden = self.reward - .map { $0.isEmpty } - - self.rewardLabelIsHidden = rewardLabelIsHidden - - self.sendMessageButtonAndBulletSeparatorHidden = activityAndProject - .map { .some($1.creator) != AppEnvironment.current.currentUser } - - self.title = title - - self.pledgeDetailsSeparatorStackViewIsHidden = Signal.zip( - pledgeAmountsStackViewIsHidden, - rewardLabelIsHidden - ) - .map { $0 || $1 } - .skipRepeats() - } - - fileprivate let backingButtonPressedProperty = MutableProperty(()) - public func backingButtonPressed() { - self.backingButtonPressedProperty.value = () - } - - fileprivate let activityAndProjectProperty = MutableProperty<(Activity, Project)?>(nil) - public func configureWith(activity: Activity, project: Project) { - self.activityAndProjectProperty.value = (activity, project) - } - - fileprivate let sendMessageButtonPressedProperty = MutableProperty(()) - public func sendMessageButtonPressed() { - self.sendMessageButtonPressedProperty.value = () - } - - public let backerImageURL: Signal - public let cellAccessibilityLabel: Signal - public let cellAccessibilityValue: Signal - public let notifyDelegateGoToBacking: Signal<(Project, Backing), Never> - public let notifyDelegateGoToSendMessage: Signal<(Project, Backing), Never> - public let pledgeAmount: Signal - public let pledgeAmountLabelIsHidden: Signal - public let pledgeAmountsStackViewIsHidden: Signal - public let pledgeDetailsSeparatorStackViewIsHidden: Signal - public let previousPledgeAmount: Signal - public let previousPledgeAmountLabelIsHidden: Signal - public let reward: Signal - public let rewardLabelIsHidden: Signal - public var sendMessageButtonAndBulletSeparatorHidden: Signal - public let title: Signal - - public var inputs: ProjectActivityBackingCellViewModelInputs { return self } - public var outputs: ProjectActivityBackingCellViewModelOutputs { return self } -} - -private func accessibilityValue(activity: Activity, project: Project) -> String { - switch activity.category { - case .backing, .backingCanceled: - return Strings.Amount_reward( - amount: amount(activity: activity, project: project), - reward: rewardSummary(activity: activity, project: project).htmlStripped() ?? "" - ) - case .backingAmount: - return Strings.Amount_previous_amount( - amount: amount(activity: activity, project: project), - previous_amount: oldAmount(activity: activity, project: project) - ) - case .backingReward: - return rewardSummary(activity: activity, project: project).htmlStripped() ?? "" - case .backingDropped, .cancellation, .commentPost, .commentProject, .failure, .follow, .funding, - .launch, .success, .suspension, .update, .watch, .unknown: - assertionFailure("Unrecognized activity: \(activity).") - return "" - } -} - -private func currentUserIsBacker(activity: Activity) -> Bool { - guard let backing = activity.memberData.backing else { return false } - return AppEnvironment.current.currentUser?.id == backing.backerId -} - -private func rewardSummary(activity: Activity, project: Project) -> String { - guard let reward = reward(activity: activity, project: project) else { return "" } - return reward.isNoReward ? - Strings.dashboard_activity_no_reward_selected() : - Strings.dashboard_activity_reward_name(reward_name: reward.title ?? reward.description) -} - -private func reward(activity: Activity, project: Project) -> Reward? { - guard let rewardId = activity.memberData.rewardId ?? activity.memberData.newRewardId else { return nil } - return project.rewards.first { $0.id == rewardId } -} - -private func title(activity: Activity) -> String { - guard let userName = activity.user?.name else { return "" } - - switch activity.category { - case .backing: - return currentUserIsBacker(activity: activity) ? - Strings.dashboard_activity_you_pledged() : - Strings.dashboard_activity_user_name_pledged(user_name: userName) - case .backingAmount: - return currentUserIsBacker(activity: activity) ? - Strings.dashboard_activity_you_adjusted_your_pledge() : - Strings.dashboard_activity_user_name_adjusted_their_pledge(user_name: userName) - case .backingCanceled: - return currentUserIsBacker(activity: activity) ? - Strings.dashboard_activity_you_canceled_your_pledge() : - Strings.dashboard_activity_user_name_canceled_their_pledge(user_name: userName) - case .backingReward: - return currentUserIsBacker(activity: activity) ? - Strings.dashboard_activity_you_changed_your_reward() : - Strings.dashboard_activity_user_name_changed_their_reward(user_name: userName) - default: - assertionFailure("Unrecognized activity: \(activity).") - return "" - } -} - -private func amount(activity: Activity, project: Project) -> String { - guard let amount = activity.memberData.amount ?? activity.memberData.newAmount else { return "" } - return Format.currency(amount, country: project.country) -} - -private func oldAmount(activity: Activity, project: Project) -> String { - guard let amount = activity.memberData.oldAmount else { return "" } - return Format.currency(amount, country: project.country) -} diff --git a/Library/ViewModels/ProjectActivityBackingCellViewModelTests.swift b/Library/ViewModels/ProjectActivityBackingCellViewModelTests.swift deleted file mode 100644 index 604ea3d623..0000000000 --- a/Library/ViewModels/ProjectActivityBackingCellViewModelTests.swift +++ /dev/null @@ -1,494 +0,0 @@ -@testable import KsApi -@testable import Library -import Prelude -import ReactiveExtensions_TestHelpers -import XCTest - -internal final class ProjectActivityBackingCellViewModelTests: TestCase { - fileprivate let vm: ProjectActivityBackingCellViewModelType = ProjectActivityBackingCellViewModel() - - fileprivate let backerImage = TestObserver() - fileprivate let cellAccessibilityLabel = TestObserver() - fileprivate let cellAccessibilityValue = TestObserver() - fileprivate let defaultUser = User.template |> \.id .~ 90 - fileprivate let notifyDelegateGoToBacking = TestObserver<(Project, Backing), Never>() - fileprivate let notifyDelegateGoToSendMessage = TestObserver<(Project, Backing), Never>() - fileprivate let pledgeAmount = TestObserver() - fileprivate let pledgeAmountLabelIsHidden = TestObserver() - fileprivate let pledgeAmountsStackViewIsHidden = TestObserver() - fileprivate let pledgeDetailsSeparatorStackViewIsHidden = TestObserver() - fileprivate let previousPledgeAmount = TestObserver() - fileprivate let previousPledgeAmountLabelIsHidden = TestObserver() - fileprivate let reward = TestObserver() - fileprivate let rewardLabelIsHidden = TestObserver() - fileprivate let title = TestObserver() - - internal override func setUp() { - super.setUp() - - self.vm.outputs.backerImageURL.map { $0?.absoluteString }.observe(self.backerImage.observer) - self.vm.outputs.cellAccessibilityLabel.observe(self.cellAccessibilityLabel.observer) - self.vm.outputs.cellAccessibilityValue.observe(self.cellAccessibilityValue.observer) - self.vm.outputs.notifyDelegateGoToBacking.observe(self.notifyDelegateGoToBacking.observer) - self.vm.outputs.notifyDelegateGoToSendMessage.observe(self.notifyDelegateGoToSendMessage.observer) - self.vm.outputs.pledgeAmount.observe(self.pledgeAmount.observer) - self.vm.outputs.pledgeAmountLabelIsHidden.observe(self.pledgeAmountLabelIsHidden.observer) - self.vm.outputs.pledgeAmountsStackViewIsHidden.observe(self.pledgeAmountsStackViewIsHidden.observer) - self.vm.outputs.pledgeDetailsSeparatorStackViewIsHidden - .observe(self.pledgeDetailsSeparatorStackViewIsHidden.observer) - self.vm.outputs.previousPledgeAmount.observe(self.previousPledgeAmount.observer) - self.vm.outputs.previousPledgeAmountLabelIsHidden.observe(self.previousPledgeAmountLabelIsHidden.observer) - self.vm.outputs.reward.observe(self.reward.observer) - self.vm.outputs.rewardLabelIsHidden.observe(self.rewardLabelIsHidden.observer) - self.vm.outputs.title.observe(self.title.observer) - - AppEnvironment.login(AccessTokenEnvelope(accessToken: "deadbeef", user: self.defaultUser)) - } - - func testBackerImage() { - let project = Project.template - let user = User.template - |> \.avatar.medium .~ "http://coolpic.com/cool.jpg" - let activity = .template - |> Activity.lens.category .~ .backing - |> Activity.lens.project .~ project - |> Activity.lens.user .~ user - - self.vm.inputs.configureWith(activity: activity, project: project) - self.backerImage.assertValues(["http://coolpic.com/cool.jpg"], "Emits backer's image URL") - } - - func testCellAccessibilityLabel() { - let project = Project.template - let user = User.template - let activity = .template - |> Activity.lens.category .~ .backing - |> Activity.lens.project .~ project - |> Activity.lens.user .~ user - - self.vm.inputs.configureWith(activity: activity, project: project) - self.cellAccessibilityLabel.assertValues( - [Strings.dashboard_activity_user_name_pledged(user_name: user.name).htmlStripped() ?? ""], - "Emits accessibility label" - ) - } - - func testCellAccessibilityValueForBacking() { - let amount = 25 - let title = "Sick Skull Graphic Mousepad" - let reward = .template - |> Reward.lens.id .~ 10 - |> Reward.lens.title .~ title - let project = .template - |> Project.lens.rewardData.rewards .~ [reward] - let user = User.template - let activity = .template - |> Activity.lens.category .~ .backing - |> Activity.lens.memberData.amount .~ amount - |> Activity.lens.memberData.rewardId .~ reward.id - |> Activity.lens.project .~ project - |> Activity.lens.user .~ user - - self.vm.inputs.configureWith(activity: activity, project: project) - let expected = Strings.Amount_reward( - amount: Format.currency(amount, country: project.country), - reward: Strings.dashboard_activity_reward_name(reward_name: title).htmlStripped() ?? "" - ) - - self.cellAccessibilityValue.assertValues([expected], "Emits accessibility value") - } - - func testCellAccessibilityValueForBackingAmountAndReward() { - let title = "Sick Skull Graphic Calculator" - let oldAmount = 15 - let newAmount = 25 - let oldReward = .template - |> Reward.lens.id .~ 10 - let newReward = .template - |> Reward.lens.id .~ 11 - |> Reward.lens.title .~ title - let project = .template - |> Project.lens.rewardData.rewards .~ [oldReward, newReward] - let user = User.template - let activity = .template - |> Activity.lens.category .~ .backingAmount - |> Activity.lens.memberData.oldAmount .~ oldAmount - |> Activity.lens.memberData.oldRewardId .~ oldReward.id - |> Activity.lens.memberData.newAmount .~ newAmount - |> Activity.lens.memberData.newRewardId .~ newReward.id - |> Activity.lens.project .~ project - |> Activity.lens.user .~ user - - self.vm.inputs.configureWith(activity: activity, project: project) - let expected = Strings.Amount_previous_amount( - amount: Format.currency(newAmount, country: project.country), - previous_amount: Format.currency( - oldAmount, - country: project.country - ) - ) - self.cellAccessibilityValue.assertValues([expected], "Emits accessibility value") - } - - func testCellAccessibilityValueForBackingReward() { - let title = "Sick Skull Graphic Pen" - let oldReward = .template - |> Reward.lens.id .~ 10 - let newReward = .template - |> Reward.lens.id .~ 11 - |> Reward.lens.title .~ title - let project = .template - |> Project.lens.rewardData.rewards .~ [oldReward, newReward] - let user = User.template - let activity = .template - |> Activity.lens.category .~ .backingReward - |> Activity.lens.memberData.oldRewardId .~ oldReward.id - |> Activity.lens.memberData.newRewardId .~ newReward.id - |> Activity.lens.project .~ project - |> Activity.lens.user .~ user - - self.vm.inputs.configureWith(activity: activity, project: project) - self.cellAccessibilityValue.assertValues( - [Strings.dashboard_activity_reward_name(reward_name: title).htmlStripped() ?? ""], - "Emits accessibility value" - ) - } - - func testNotifyDelegateGoToBacking() { - let project = Project.template - let activity = .template - |> Activity.lens.category .~ .backing - |> Activity.lens.memberData.backing .~ .template - |> Activity.lens.project .~ project - - self.vm.inputs.configureWith(activity: activity, project: project) - self.notifyDelegateGoToBacking.assertValueCount(0) - - self.vm.inputs.backingButtonPressed() - self.notifyDelegateGoToBacking.assertValueCount(1, "Should go to backing") - } - - func testNotifyDelegateGoToSendMessage() { - let project = Project.template - let activity = .template - |> Activity.lens.category .~ .backing - |> Activity.lens.memberData.backing .~ .template - |> Activity.lens.project .~ project - - self.vm.inputs.configureWith(activity: activity, project: project) - self.notifyDelegateGoToSendMessage.assertValueCount(0) - - self.vm.inputs.sendMessageButtonPressed() - self.notifyDelegateGoToSendMessage.assertValueCount( - 1, "Go to send message after pressing send message button" - ) - } - - func testPledgeAmount() { - let project = Project.template - let backingActivity = .template - |> Activity.lens.category .~ .backing - |> Activity.lens.memberData.amount .~ 25 - |> Activity.lens.project .~ project - self.vm.inputs.configureWith(activity: backingActivity, project: project) - self.pledgeAmount.assertValues(["$25"], "Emits pledge amount") - self.pledgeAmountLabelIsHidden.assertValues([false], "Not hidden when there's an amount") - - let backingAmountActivity = .template - |> Activity.lens.category .~ .backingAmount - |> Activity.lens.memberData.newAmount .~ 15 - |> Activity.lens.memberData.oldAmount .~ 25 - |> Activity.lens.project .~ project - self.vm.inputs.configureWith(activity: backingAmountActivity, project: project) - self.pledgeAmount.assertValues(["$25", "$15"], "Emits new pledge amount") - self.pledgeAmountLabelIsHidden.assertValues([false], "Not hidden when there's an amount") - - let backingCanceledActivity = .template - |> Activity.lens.category .~ .backingCanceled - |> Activity.lens.project .~ project - self.vm.inputs.configureWith(activity: backingCanceledActivity, project: project) - self.pledgeAmount.assertValues(["$25", "$15", ""], "Emits empty string when no pledge amount") - self.pledgeAmountLabelIsHidden.assertValues([false, true], "Hidden when there's no amount") - } - - func testPledgeDetailsSeparatorStackViewIsHidden() { - let reward1 = .template - |> Reward.lens.description .~ "Super sick" - |> Reward.lens.id .~ 19 - |> Reward.lens.title .~ "Sick Skull Graphic Skateboard" - - let project = .template - |> Project.lens.rewardData.rewards .~ [.noReward, reward1] - - let activity1 = .template - |> Activity.lens.category .~ .backing - |> Activity.lens.memberData.rewardId .~ 19 - |> Activity.lens.memberData.amount .~ 5 - |> Activity.lens.project .~ project - - self.vm.inputs.configureWith(activity: activity1, project: project) - self.pledgeDetailsSeparatorStackViewIsHidden.assertValues( - [false], - "Not hidden when activity has reward and amount" - ) - - let activity2 = .template - |> Activity.lens.category .~ .backing - |> Activity.lens.memberData.rewardId .~ 19 - |> Activity.lens.project .~ project - - self.vm.inputs.configureWith(activity: activity2, project: project) - self.pledgeDetailsSeparatorStackViewIsHidden.assertValues( - [false, true], - "Hidden when activity has reward but no amount" - ) - - let activity3 = .template - |> Activity.lens.category .~ .backing - |> Activity.lens.memberData.amount .~ 5 - |> Activity.lens.project .~ project - - self.vm.inputs.configureWith(activity: activity3, project: project) - self.pledgeDetailsSeparatorStackViewIsHidden.assertValues( - [false, true], - "Hidden when activity has reward but no amount" - ) - } - - func testPreviousPledgeAmount() { - let project = Project.template - let activity = .template - |> Activity.lens.category .~ .backingAmount - |> Activity.lens.memberData.newAmount .~ 25 - |> Activity.lens.memberData.oldAmount .~ 15 - |> Activity.lens.project .~ project - - self.vm.inputs.configureWith(activity: activity, project: project) - self.pledgeAmount.assertValues(["$25"], "Emits new plege amount") - self.pledgeAmountLabelIsHidden.assertValues([false], "Not hidden when there's an amount") - self.previousPledgeAmount.assertValues(["$15"], "Emits previous pledge amount") - self.previousPledgeAmountLabelIsHidden.assertValues([false], "Not hidden when there's an amount") - } - - func testPledgeAmountsStackViewIsHidden() { - let project = Project.template - let activityWithoutAmount = .template - |> Activity.lens.category .~ .backing - |> Activity.lens.project .~ project - - self.vm.inputs.configureWith(activity: activityWithoutAmount, project: project) - self.pledgeAmountsStackViewIsHidden.assertValues([true], "Hidden when there are no amounts.") - - let activityWithAmount = activityWithoutAmount |> Activity.lens.memberData.amount .~ 5 - self.vm.inputs.configureWith(activity: activityWithAmount, project: project) - self.pledgeAmountsStackViewIsHidden.assertValues([true, false], "Not hidden if there is an amount.") - - let activityWithNewAmount = activityWithoutAmount |> Activity.lens.memberData.newAmount .~ 5 - self.vm.inputs.configureWith(activity: activityWithNewAmount, project: project) - self.pledgeAmountsStackViewIsHidden.assertValues([true, false], "Not hidden if there is a new amount.") - - let activityWithOldAmount = activityWithoutAmount |> Activity.lens.memberData.oldAmount .~ 5 - self.vm.inputs.configureWith(activity: activityWithOldAmount, project: project) - self.pledgeAmountsStackViewIsHidden.assertValues([true, false], "Not hidden if there is an old amount.") - } - - func testReward() { - let reward1 = .template - |> Reward.lens.description .~ "Super sick" - |> Reward.lens.id .~ 32 - |> Reward.lens.title .~ "Sick Skull Graphic Notepad" - let reward2 = .template - |> Reward.lens.title .~ nil - |> Reward.lens.description .~ "Sick Skull Graphic Binder" - |> Reward.lens.id .~ 33 - let project = .template - |> Project.lens.rewardData.rewards .~ [.noReward, reward1, reward2] - - let activity1 = .template - |> Activity.lens.category .~ .backing - |> Activity.lens.memberData.rewardId .~ 32 - |> Activity.lens.project .~ project - - self.vm.inputs.configureWith(activity: activity1, project: project) - let expected1 = Strings.dashboard_activity_reward_name(reward_name: reward1.title!) - self.reward.assertValues([expected1], "Should emit reward title if present") - self.rewardLabelIsHidden.assertValues([false]) - - let activity2 = .template - |> Activity.lens.category .~ .backing - |> Activity.lens.memberData.rewardId .~ 33 - |> Activity.lens.project .~ project - - self.vm.inputs.configureWith(activity: activity2, project: project) - let expected2 = Strings.dashboard_activity_reward_name(reward_name: reward2.description) - self.reward.assertValues( - [expected1, expected2], - "Should emit reward description if title not present" - ) - self.rewardLabelIsHidden.assertValues([false, false]) - - let activity3 = .template - |> Activity.lens.category .~ .backing - |> Activity.lens.memberData.rewardId .~ Reward.noReward.id - |> Activity.lens.project .~ project - - self.vm.inputs.configureWith(activity: activity3, project: project) - let expected3 = Strings.dashboard_activity_no_reward_selected() - self.reward.assertValues([expected1, expected2, expected3], "Should emit no reward selected") - self.rewardLabelIsHidden.assertValues([false, false, false]) - - let activity4 = .template - |> Activity.lens.category .~ .backingAmount - |> Activity.lens.memberData.oldAmount .~ 5 - |> Activity.lens.memberData.newAmount .~ 10 - |> Activity.lens.project .~ project - - self.vm.inputs.configureWith(activity: activity4, project: project) - self.reward.assertValues([expected1, expected2, expected3, ""]) - self.rewardLabelIsHidden.assertValues([false, false, false, true]) - } - - func testTitleBacking() { - let project = Project.template - let activity = .template - |> Activity.lens.category .~ .backing - |> Activity.lens.memberData.backing .~ (.template |> Backing.lens.backerId .~ 1_001) - |> Activity.lens.project .~ project - |> Activity.lens.user .~ (.template - |> \.id .~ 1_001 - |> \.name .~ "Christopher" - ) - self.vm.inputs.configureWith(activity: activity, project: project) - - self.title.assertValues( - [Strings.dashboard_activity_user_name_pledged(user_name: "Christopher")], - "Should emit that the user pledged" - ) - } - - func testTitleBackingByCurrentUser() { - let project = Project.template - let activity = .template - |> Activity.lens.category .~ .backing - |> Activity.lens.memberData.backing .~ (.template |> Backing.lens.backerId .~ self.defaultUser.id) - |> Activity.lens.project .~ project - |> Activity.lens.user .~ self.defaultUser - self.vm.inputs.configureWith(activity: activity, project: project) - - self.title.assertValues( - [Strings.dashboard_activity_you_pledged()], - "Should emit that 'you' pledged" - ) - } - - func testTitleBackingAmount() { - let project = Project.template - let activity = .template - |> Activity.lens.category .~ .backingAmount - |> Activity.lens.memberData.oldAmount .~ 15 - |> Activity.lens.memberData.newAmount .~ 25 - |> Activity.lens.memberData.backing .~ (.template |> Backing.lens.backerId .~ 1_001) - |> Activity.lens.project .~ project - |> Activity.lens.user .~ (.template - |> \.id .~ 1_001 - |> \.name .~ "Christopher" - ) - self.vm.inputs.configureWith(activity: activity, project: project) - - self.title.assertValues( - [Strings.dashboard_activity_user_name_adjusted_their_pledge(user_name: "Christopher")], - "Should emit that the user adjusted their pledge" - ) - } - - func testTitleBackingAmountByCurrentUser() { - let project = Project.template - let activity = .template - |> Activity.lens.category .~ .backingAmount - |> Activity.lens.memberData.oldAmount .~ 15 - |> Activity.lens.memberData.newAmount .~ 25 - |> Activity.lens.memberData.backing .~ (.template |> Backing.lens.backerId .~ self.defaultUser.id) - |> Activity.lens.project .~ project - |> Activity.lens.user .~ self.defaultUser - self.vm.inputs.configureWith(activity: activity, project: project) - - self.title.assertValues( - [Strings.dashboard_activity_you_adjusted_your_pledge()], - "Should emit that 'you' adjusted your pledge" - ) - } - - func testTitleBackingCanceled() { - let project = Project.template - let activity = .template - |> Activity.lens.category .~ .backingCanceled - |> Activity.lens.memberData.backing .~ (.template |> Backing.lens.backerId .~ 1_001) - |> Activity.lens.project .~ project - |> Activity.lens.user .~ (.template - |> \.id .~ 1_001 - |> \.name .~ "Christopher" - ) - self.vm.inputs.configureWith(activity: activity, project: project) - - self.title.assertValues( - [Strings.dashboard_activity_user_name_canceled_their_pledge(user_name: "Christopher")], - "Should emit that the user canceled their pledge" - ) - } - - func testTitleBackingCanceledByCurrentUser() { - let project = Project.template - let activity = .template - |> Activity.lens.category .~ .backingCanceled - |> Activity.lens.memberData.backing .~ (.template |> Backing.lens.backerId .~ self.defaultUser.id) - |> Activity.lens.project .~ project - |> Activity.lens.user .~ self.defaultUser - self.vm.inputs.configureWith(activity: activity, project: project) - - self.title.assertValues( - [Strings.dashboard_activity_you_canceled_your_pledge()], - "Should emit that 'you' canceled your pledge" - ) - } - - func testTitleBackingReward() { - let project = Project.template - let activity = .template - |> Activity.lens.category .~ .backingReward - |> Activity.lens.memberData.oldAmount .~ 15 - |> Activity.lens.memberData.oldRewardId .~ 1 - |> Activity.lens.memberData.newAmount .~ 25 - |> Activity.lens.memberData.newRewardId .~ 2 - |> Activity.lens.memberData.backing .~ (.template |> Backing.lens.backerId .~ 1_001) - |> Activity.lens.project .~ project - |> Activity.lens.user .~ (.template - |> \.id .~ 1_001 - |> \.name .~ "Christopher" - ) - self.vm.inputs.configureWith(activity: activity, project: project) - - self.title.assertValues( - [Strings.dashboard_activity_user_name_changed_their_reward(user_name: "Christopher")], - "Should emit that the user changed their reward" - ) - } - - func testTitleBackingRewardByCurrentUser() { - let project = Project.template - let activity = .template - |> Activity.lens.category .~ .backingReward - |> Activity.lens.memberData.oldAmount .~ 15 - |> Activity.lens.memberData.oldRewardId .~ 1 - |> Activity.lens.memberData.newAmount .~ 25 - |> Activity.lens.memberData.newRewardId .~ 2 - |> Activity.lens.memberData.backing .~ (.template |> Backing.lens.backerId .~ self.defaultUser.id) - |> Activity.lens.project .~ project - |> Activity.lens.user .~ self.defaultUser - self.vm.inputs.configureWith(activity: activity, project: project) - - self.title.assertValues( - [Strings.dashboard_activity_you_changed_your_reward()], - "Should emit that 'you' changed your reward" - ) - } -} diff --git a/Library/ViewModels/ProjectActivityCommentCellViewModel.swift b/Library/ViewModels/ProjectActivityCommentCellViewModel.swift deleted file mode 100644 index 3813a5f4bb..0000000000 --- a/Library/ViewModels/ProjectActivityCommentCellViewModel.swift +++ /dev/null @@ -1,156 +0,0 @@ -import Foundation -import KsApi -import Prelude -import ReactiveExtensions -import ReactiveSwift - -public protocol ProjectActivityCommentCellViewModelInputs { - /// Call when the backing button is pressed. - func backingButtonPressed() - - /// Call to set the activity and project. - func configureWith(activity: Activity, project: Project) - - /// Call when the comment button is pressed. - func replyButtonPressed() -} - -public protocol ProjectActivityCommentCellViewModelOutputs { - /// Emits the author's image URL. - var authorImageURL: Signal { get } - - /// Emits the body of the comment. - var body: Signal { get } - - /// Emits the cell's accessibility label. - var cellAccessibilityLabel: Signal { get } - - /// Emits the cell's accessibility value. - var cellAccessibilityValue: Signal { get } - - /// Go to the backing info screen. - var notifyDelegateGoToBacking: Signal<(Project, User), Never> { get } - - /// Go to the comment reply dialog for the project/update comment. - var notifyDelegateGoToSendReply: Signal<(Project, Update?, ActivityComment), Never> { get } - - /// Emits a Bool whether the footer pledge and reply stack view is hidden or not. - var pledgeFooterIsHidden: Signal { get } - - /// Emits the activity's title. - var title: Signal { get } -} - -public protocol ProjectActivityCommentCellViewModelType { - var inputs: ProjectActivityCommentCellViewModelInputs { get } - var outputs: ProjectActivityCommentCellViewModelOutputs { get } -} - -public final class ProjectActivityCommentCellViewModel: ProjectActivityCommentCellViewModelType, - ProjectActivityCommentCellViewModelInputs, ProjectActivityCommentCellViewModelOutputs { - public init() { - let activityAndProject = self.activityAndProjectProperty.signal.skipNil() - let activity = activityAndProject.map(first) - - self.authorImageURL = activity.map { ($0.user?.avatar.medium).flatMap(URL.init) } - - self.body = activity.map { $0.comment?.body ?? "" } - - self.notifyDelegateGoToBacking = activityAndProject - .takeWhen(self.backingButtonPressedProperty.signal) - .map { activity, project -> (Project, User)? in - guard let user = activity.user else { return nil } - return (project, user) - } - .skipNil() - - let projectComment = activityAndProject - .filter { activity, _ in activity.category == .commentProject } - .flatMap { activity, project -> SignalProducer<(Project, Update?, ActivityComment), Never> in - guard let comment = activity.comment else { return .empty } - return .init(value: (project, nil, comment)) - } - - let updateComment = activityAndProject - .filter { activity, _ in activity.category == .commentPost } - .flatMap { activity, project -> SignalProducer<(Project, Update?, ActivityComment), Never> in - guard let update = activity.update, let comment = activity.comment else { return .empty } - return .init(value: (project, update, comment)) - } - - self.notifyDelegateGoToSendReply = Signal.merge(projectComment, updateComment) - .takeWhen(self.replyButtonPressedProperty.signal) - - let projectTitle = activity - .filter { $0.category == .commentProject } - .map(commentOnProjectTitle(activity:)) - - let updateTitle = activity - .filter { $0.category == .commentPost } - .map(commentOnUpdateTitle(activity:)) - - self.title = Signal.merge(projectTitle, updateTitle) - - self.cellAccessibilityLabel = self.title.map { title in title.htmlStripped() ?? "" } - - self.cellAccessibilityValue = self.body - - self.pledgeFooterIsHidden = activityAndProject - .map { activity, project in - activity.user == AppEnvironment.current.currentUser - && project.creator == AppEnvironment.current.currentUser - } - } - - fileprivate let backingButtonPressedProperty = MutableProperty(()) - public func backingButtonPressed() { - self.backingButtonPressedProperty.value = () - } - - fileprivate let replyButtonPressedProperty = MutableProperty(()) - public func replyButtonPressed() { - self.replyButtonPressedProperty.value = () - } - - fileprivate let activityAndProjectProperty = MutableProperty<(Activity, Project)?>(nil) - public func configureWith(activity: Activity, project: Project) { - self.activityAndProjectProperty.value = (activity, project) - } - - public let authorImageURL: Signal - public let body: Signal - public let cellAccessibilityLabel: Signal - public let cellAccessibilityValue: Signal - public let notifyDelegateGoToBacking: Signal<(Project, User), Never> - public let notifyDelegateGoToSendReply: Signal<(Project, Update?, ActivityComment), Never> - public let pledgeFooterIsHidden: Signal - public let title: Signal - - public var inputs: ProjectActivityCommentCellViewModelInputs { return self } - public var outputs: ProjectActivityCommentCellViewModelOutputs { return self } -} - -private func commentOnProjectTitle(activity: Activity) -> String { - guard let user = activity.user else { return "" } - - return AppEnvironment.current.currentUser == user ? - Strings.dashboard_activity_you_commented_on_your_project() : - Strings.dashboard_activity_user_name_commented_on_your_project(user_name: user.name) -} - -private func commentOnUpdateTitle(activity: Activity) -> String { - guard let update = activity.update, let user = activity.user else { return "" } - - if AppEnvironment.current.currentUser == user { - return Strings.dashboard_activity_you_commented_on_update_number( - space: "\u{00a0}", - update_number: String(update.sequence) - ) - } else { - return Strings.dashboard_activity_user_name_commented_on_update_number( - user_name: user.name, - space: "\u{00a0}", - update_number: String(update.sequence) - ) - } -} diff --git a/Library/ViewModels/ProjectActivityCommentCellViewModelTests.swift b/Library/ViewModels/ProjectActivityCommentCellViewModelTests.swift deleted file mode 100644 index f6d745d8bc..0000000000 --- a/Library/ViewModels/ProjectActivityCommentCellViewModelTests.swift +++ /dev/null @@ -1,262 +0,0 @@ -@testable import KsApi -@testable import Library -import Prelude -import ReactiveExtensions_TestHelpers -import XCTest - -internal final class ProjectActivityCommentCellViewModelTests: TestCase { - fileprivate let vm: ProjectActivityCommentCellViewModelType = ProjectActivityCommentCellViewModel() - - fileprivate let authorImage = TestObserver() - fileprivate let body = TestObserver() - fileprivate let cellAccessibilityLabel = TestObserver() - fileprivate let cellAccessibilityValue = TestObserver() - fileprivate let defaultUser = User.template |> \.id .~ 9 - fileprivate let notifyDelegateGoToBacking = TestObserver<(Project, User), Never>() - fileprivate let notifyDelegateGoToSendReply = TestObserver<(Project, Update?, ActivityComment), Never>() - fileprivate let pledgeFooterIsHidden = TestObserver() - fileprivate let title = TestObserver() - - private var sampleAuthor: ActivityCommentAuthor { - ActivityCommentAuthor( - avatar: .template, - id: 1, - name: "test", - urls: .template - ) - } - - private var sampleComment: ActivityComment { - ActivityComment( - author: self.sampleAuthor, - body: "Love this project!", - createdAt: .leastNonzeroMagnitude, - deletedAt: nil, - id: 1 - ) - } - - internal override func setUp() { - super.setUp() - - self.vm.outputs.authorImageURL.map { $0?.absoluteString }.observe(self.authorImage.observer) - self.vm.outputs.body.observe(self.body.observer) - self.vm.outputs.cellAccessibilityLabel.observe(self.cellAccessibilityLabel.observer) - self.vm.outputs.cellAccessibilityValue.observe(self.cellAccessibilityValue.observer) - self.vm.outputs.notifyDelegateGoToBacking.observe(self.notifyDelegateGoToBacking.observer) - self.vm.outputs.notifyDelegateGoToSendReply - .observe(self.notifyDelegateGoToSendReply.observer) - self.vm.outputs.pledgeFooterIsHidden.observe(self.pledgeFooterIsHidden.observer) - self.vm.outputs.title.observe(self.title.observer) - - AppEnvironment.login(AccessTokenEnvelope(accessToken: "deadbeef", user: self.defaultUser)) - } - - func testAuthorImage() { - let project = Project.template - let user = User.template - |> \.avatar.medium .~ "http://coolpic.com/cool.jpg" - let activity = .template - |> Activity.lens.project .~ project - |> Activity.lens.user .~ user - - self.vm.inputs.configureWith(activity: activity, project: project) - self.authorImage.assertValues(["http://coolpic.com/cool.jpg"], "Emits author's image URL") - } - - func testBody() { - let project = Project.template - let body1 = "Thanks for the update!" - - let comment = ActivityComment( - author: sampleAuthor, - body: body1, - createdAt: .leastNonzeroMagnitude, - deletedAt: nil, - id: 1 - ) - let commentPostActivity = .template - |> Activity.lens.category .~ .commentPost - |> Activity.lens.project .~ project - |> Activity.lens.comment .~ comment - - self.vm.inputs.configureWith(activity: commentPostActivity, project: project) - self.body.assertValues([body1], "Emits post comment's body") - - let body2 = "Aw, the limited bundle is all gone!" - let comment2 = ActivityComment( - author: sampleAuthor, - body: body2, - createdAt: .leastNonzeroMagnitude, - deletedAt: nil, - id: 1 - ) - - let commentProjectActivity = .template - |> Activity.lens.category .~ .commentProject - |> Activity.lens.comment .~ comment2 - |> Activity.lens.project .~ project - - self.vm.inputs.configureWith(activity: commentProjectActivity, project: project) - self.body.assertValues([body1, body2], "Emits project comment's body") - } - - func testCellAccessibilityLabel() { - let project = Project.template - let activity = .template - |> Activity.lens.category .~ .commentProject - |> Activity.lens.comment .~ self.sampleComment - |> Activity.lens.project .~ project - |> Activity.lens.user .~ (.template |> \.name .~ "Christopher") - - self.vm.inputs.configureWith(activity: activity, project: project) - let expected = Strings.dashboard_activity_user_name_commented_on_your_project(user_name: "Christopher") - .htmlStripped() ?? "" - self.cellAccessibilityLabel.assertValues([expected], "Should emit accessibility label") - } - - func testCellAccessibilityValue() { - let project = Project.template - let body = "Thanks for the update!" - let comment = ActivityComment( - author: sampleAuthor, - body: body, - createdAt: .leastNonzeroMagnitude, - deletedAt: nil, - id: 1 - ) - let activity = .template - |> Activity.lens.category .~ .commentPost - |> Activity.lens.comment .~ comment - |> Activity.lens.project .~ project - - self.vm.inputs.configureWith(activity: activity, project: project) - self.cellAccessibilityValue.assertValues([body], "Emits accessibility value") - } - - func testNotifyDelegateGoToBacking() { - let project = Project.template - let activity = .template - |> Activity.lens.category .~ .commentProject - |> Activity.lens.project .~ project - - self.pledgeFooterIsHidden.assertValueCount(0) - - self.vm.inputs.configureWith(activity: activity, project: project) - - self.pledgeFooterIsHidden.assertValues([false], "Show the footer to go to pledge info.") - - self.vm.inputs.backingButtonPressed() - self.notifyDelegateGoToBacking.assertValueCount(1, "Should go to backing") - } - - func testNotifyDelegateGoToSendReply_Project() { - let project = Project.template - let user = User.template |> \.name .~ "Christopher" - let activity = .template - |> Activity.lens.category .~ .commentProject - |> Activity.lens.comment .~ self.sampleComment - |> Activity.lens.project .~ project - |> Activity.lens.user .~ user - - self.vm.inputs.configureWith(activity: activity, project: project) - self.vm.inputs.replyButtonPressed() - self.notifyDelegateGoToSendReply.assertValueCount(1, "Should go to send reply on project") - } - - func testNotifyDelegateGoToSendReply_Update() { - let project = Project.template - let update = Update.template - let user = User.template |> \.name .~ "Christopher" - let activity = .template - |> Activity.lens.category .~ .commentPost - |> Activity.lens.comment .~ self.sampleComment - |> Activity.lens.project .~ project - |> Activity.lens.update .~ update - |> Activity.lens.user .~ user - - self.vm.inputs.configureWith(activity: activity, project: project) - self.vm.inputs.replyButtonPressed() - self.notifyDelegateGoToSendReply.assertValueCount(1, "Should go to send reply on update") - } - - func testTitleProject() { - let project = Project.template - - let activity = .template - |> Activity.lens.category .~ .commentProject - |> Activity.lens.comment .~ self.sampleComment - |> Activity.lens.project .~ project - |> Activity.lens.user .~ (.template |> \.name .~ "Christopher") - - self.vm.inputs.configureWith(activity: activity, project: project) - let expected = Strings.dashboard_activity_user_name_commented_on_your_project(user_name: "Christopher") - self.title.assertValues([expected], "Should emit that author commented on project") - } - - func testTitleProjectAsCurrentUser() { - let project = Project.template - let activity = .template - |> Activity.lens.category .~ .commentProject - |> Activity.lens.comment .~ self.sampleComment - |> Activity.lens.project .~ project - |> Activity.lens.user .~ self.defaultUser - - self.vm.inputs.configureWith(activity: activity, project: project) - let expected = Strings.dashboard_activity_you_commented_on_your_project() - self.title.assertValues([expected], "Should emit 'you' commented on project") - } - - func testTitleUpdate() { - let project = Project.template - let activity = .template - |> Activity.lens.category .~ .commentPost - |> Activity.lens.comment .~ self.sampleComment - |> Activity.lens.project .~ project - |> Activity.lens.update .~ (.template |> Update.lens.sequence .~ 5) - |> Activity.lens.user .~ (.template |> \.name .~ "Christopher") - - self.vm.inputs.configureWith(activity: activity, project: project) - let expected = Strings.dashboard_activity_user_name_commented_on_update_number( - user_name: "Christopher", - space: "\u{00a0}", - update_number: "5" - ) - self.title.assertValues([expected], "Should emit that author commented on update") - } - - func testTitleUpdateAsCurrentUser() { - let project = Project.template - let activity = .template - |> Activity.lens.category .~ .commentPost - |> Activity.lens.comment .~ self.sampleComment - |> Activity.lens.project .~ project - |> Activity.lens.update .~ (.template |> Update.lens.sequence .~ 5) - |> Activity.lens.user .~ self.defaultUser - - self.vm.inputs.configureWith(activity: activity, project: project) - let expected = Strings.dashboard_activity_you_commented_on_update_number( - space: "\u{00a0}", - update_number: "5" - ) - self.title.assertValues([expected], "Should emit 'you' commented on update") - } - - func testHideReplyAndPledgeInfoButtons_IfUserIsCreator() { - let creator = User.template |> \.name .~ "Benny" - let project = .template |> Project.lens.creator .~ creator - let activity = .template - |> Activity.lens.category .~ .commentPost - |> Activity.lens.comment .~ self.sampleComment - |> Activity.lens.project .~ project - |> Activity.lens.user .~ creator - - withEnvironment(currentUser: creator) { - self.pledgeFooterIsHidden.assertValueCount(0) - - self.vm.inputs.configureWith(activity: activity, project: project) - - self.pledgeFooterIsHidden.assertValues([true], "Hide the footer for the creator.") - } - } -} diff --git a/Library/ViewModels/ProjectActivityLaunchCellViewModel.swift b/Library/ViewModels/ProjectActivityLaunchCellViewModel.swift deleted file mode 100644 index 17f8d547c2..0000000000 --- a/Library/ViewModels/ProjectActivityLaunchCellViewModel.swift +++ /dev/null @@ -1,61 +0,0 @@ -import Foundation -import KsApi -import Prelude -import ReactiveExtensions -import ReactiveSwift - -public protocol ProjectActivityLaunchCellViewModelInputs { - /// Call to set the activity and project. - func configureWith(activity: Activity, project: Project) -} - -public protocol ProjectActivityLaunchCellViewModelOutputs { - /// Emits the background image URL. - var backgroundImageURL: Signal { get } - - /// Emits the title of the activity. - var title: Signal { get } -} - -public protocol ProjectActivityLaunchCellViewModelType { - var inputs: ProjectActivityLaunchCellViewModelInputs { get } - var outputs: ProjectActivityLaunchCellViewModelOutputs { get } -} - -public final class ProjectActivityLaunchCellViewModel: ProjectActivityLaunchCellViewModelType, - ProjectActivityLaunchCellViewModelInputs, ProjectActivityLaunchCellViewModelOutputs { - public init() { - let activityAndProject = self.activityAndProjectProperty.signal.skipNil() - let project = activityAndProject.map(second) - - self.backgroundImageURL = project.map { $0.photo.med }.map(URL.init(string:)) - - self.title = project.map { project in - var formatted = "" - - if let launchedAtDate = project.dates.launchedAt { - formatted = Format.date( - secondsInUTC: launchedAtDate, - dateStyle: .long, timeStyle: .none - ).nonBreakingSpaced() - } - - return Strings.dashboard_activity_project_name_launched( - project_name: project.name, - launch_date: formatted, - goal: Format.currency(project.stats.goal, country: project.country).nonBreakingSpaced() - ) - } - } - - fileprivate let activityAndProjectProperty = MutableProperty<(Activity, Project)?>(nil) - public func configureWith(activity: Activity, project: Project) { - self.activityAndProjectProperty.value = (activity, project) - } - - public let backgroundImageURL: Signal - public let title: Signal - - public var inputs: ProjectActivityLaunchCellViewModelInputs { return self } - public var outputs: ProjectActivityLaunchCellViewModelOutputs { return self } -} diff --git a/Library/ViewModels/ProjectActivityLaunchCellViewModelTests.swift b/Library/ViewModels/ProjectActivityLaunchCellViewModelTests.swift deleted file mode 100644 index c77d7c006b..0000000000 --- a/Library/ViewModels/ProjectActivityLaunchCellViewModelTests.swift +++ /dev/null @@ -1,45 +0,0 @@ -@testable import KsApi -@testable import Library -import Prelude -import ReactiveExtensions_TestHelpers -import XCTest - -internal final class ProjectActivityLaunchCellViewModelTests: TestCase { - fileprivate let vm: ProjectActivityLaunchCellViewModelType = ProjectActivityLaunchCellViewModel() - - fileprivate let title = TestObserver() - - override func setUp() { - super.setUp() - - self.vm.outputs.title.observe(self.title.observer) - - AppEnvironment.login(AccessTokenEnvelope(accessToken: "deadbeef", user: .template)) - } - - func testTitle() { - let country = Project.Country.us - let goal = 5_000 - let projectName = "Sick Skull Graphic Watch" - let launchedAt = Date().timeIntervalSince1970 - - let project = .template - |> Project.lens.country .~ country - |> Project.lens.dates.launchedAt .~ launchedAt - |> Project.lens.name .~ projectName - |> Project.lens.stats.goal .~ goal - let activity = .template - |> Activity.lens.project .~ project - - self.vm.inputs.configureWith(activity: activity, project: project) - - let expected = Strings.dashboard_activity_project_name_launched( - project_name: projectName, - launch_date: Format.date(secondsInUTC: launchedAt, dateStyle: .long, timeStyle: .none) - .nonBreakingSpaced(), - goal: Format.currency(goal, country: country).nonBreakingSpaced() - ) - - self.title.assertValues([expected], "Emits project's name, launch date, and goal") - } -} diff --git a/Library/ViewModels/ProjectActivityNegativeStateChangeCellViewModel.swift b/Library/ViewModels/ProjectActivityNegativeStateChangeCellViewModel.swift deleted file mode 100644 index c59869dbe7..0000000000 --- a/Library/ViewModels/ProjectActivityNegativeStateChangeCellViewModel.swift +++ /dev/null @@ -1,69 +0,0 @@ -import KsApi -import Prelude -import ReactiveExtensions -import ReactiveSwift - -public protocol ProjectActivityNegativeStateChangeCellViewModelInputs { - /// Call to set the activity and project. - func configureWith(activity: Activity, project: Project) -} - -public protocol ProjectActivityNegativeStateChangeCellViewModelOutputs { - /// Emits the title of the activity. - var title: Signal { get } -} - -public protocol ProjectActivityNegativeStateChangeCellViewModelType { - var inputs: ProjectActivityNegativeStateChangeCellViewModelInputs { get } - var outputs: ProjectActivityNegativeStateChangeCellViewModelOutputs { get } -} - -public final class ProjectActivityNegativeStateChangeCellViewModel: - ProjectActivityNegativeStateChangeCellViewModelType, ProjectActivityNegativeStateChangeCellViewModelInputs, - ProjectActivityNegativeStateChangeCellViewModelOutputs { - public init() { - let activityAndProject = self.activityAndProjectProperty.signal.skipNil() - - self.title = activityAndProject.map { activity, project in - switch activity.category { - case .cancellation: - return Strings.dashboard_activity_project_name_was_canceled( - project_name: project.name, - cancellation_date: Format.date( - secondsInUTC: activity.createdAt, dateStyle: .long, - timeStyle: .none - ).nonBreakingSpaced() - ) - case .failure: - return Strings.dashboard_activity_project_name_was_unsuccessful( - project_name: project.name, - unsuccessful_date: Format.date( - secondsInUTC: activity.createdAt, dateStyle: .long, - timeStyle: .none - ).nonBreakingSpaced() - ) - case .suspension: - return Strings.dashboard_activity_project_name_was_suspended( - project_name: project.name, - suspension_date: Format.date( - secondsInUTC: activity.createdAt, dateStyle: .long, - timeStyle: .none - ).nonBreakingSpaced() - ) - default: - assertionFailure("Unrecognized activity: \(activity).") - return "" - } - } - } - - fileprivate let activityAndProjectProperty = MutableProperty<(Activity, Project)?>(nil) - public func configureWith(activity: Activity, project: Project) { - self.activityAndProjectProperty.value = (activity, project) - } - - public let title: Signal - - public var inputs: ProjectActivityNegativeStateChangeCellViewModelInputs { return self } - public var outputs: ProjectActivityNegativeStateChangeCellViewModelOutputs { return self } -} diff --git a/Library/ViewModels/ProjectActivityNegativeStateChangeCellViewModelTests.swift b/Library/ViewModels/ProjectActivityNegativeStateChangeCellViewModelTests.swift deleted file mode 100644 index c0f69d54b4..0000000000 --- a/Library/ViewModels/ProjectActivityNegativeStateChangeCellViewModelTests.swift +++ /dev/null @@ -1,81 +0,0 @@ -@testable import KsApi -@testable import Library -import Prelude -import ReactiveExtensions_TestHelpers -import XCTest - -internal final class ProjectActivityNegativeStateChangeCellViewModelTests: TestCase { - fileprivate let vm: ProjectActivityNegativeStateChangeCellViewModel = - ProjectActivityNegativeStateChangeCellViewModel() - - fileprivate let title = TestObserver() - - override func setUp() { - super.setUp() - - self.vm.outputs.title.observe(self.title.observer) - } - - func testTitleForCancelledProject() { - let canceledAt = Date().timeIntervalSince1970 - let projectName = "Sick Skull Graphic Lunchbox" - - let project = .template - |> Project.lens.name .~ projectName - |> Project.lens.state .~ .canceled - let activity = .template - |> Activity.lens.category .~ .cancellation - |> Activity.lens.createdAt .~ canceledAt - |> Activity.lens.project .~ project - - self.vm.inputs.configureWith(activity: activity, project: project) - let expected = Strings.dashboard_activity_project_name_was_canceled( - project_name: projectName, - cancellation_date: Format.date(secondsInUTC: canceledAt, dateStyle: .long, timeStyle: .none) - .nonBreakingSpaced() - ) - self.title.assertValues([expected], "Emits title indicating the project was cancelled") - } - - func testTitleForFailedProject() { - let failedAt = Date().timeIntervalSince1970 - let projectName = "Sick Skull Graphic Lunchbox" - - let project = .template - |> Project.lens.name .~ projectName - |> Project.lens.state .~ .failed - let activity = .template - |> Activity.lens.category .~ .failure - |> Activity.lens.createdAt .~ failedAt - |> Activity.lens.project .~ project - - self.vm.inputs.configureWith(activity: activity, project: project) - let expected = Strings.dashboard_activity_project_name_was_unsuccessful( - project_name: projectName, - unsuccessful_date: Format.date(secondsInUTC: failedAt, dateStyle: .long, timeStyle: .none) - .nonBreakingSpaced() - ) - self.title.assertValues([expected], "Emits title indicating the project was unsuccessful") - } - - func testTitleForSuspendedProject() { - let suspendedAt = Date().timeIntervalSince1970 - let projectName = "Sick Skull Graphic Lunchbox" - - let project = .template - |> Project.lens.name .~ projectName - |> Project.lens.state .~ .suspended - let activity = .template - |> Activity.lens.category .~ .suspension - |> Activity.lens.createdAt .~ suspendedAt - |> Activity.lens.project .~ project - - self.vm.inputs.configureWith(activity: activity, project: project) - let expected = Strings.dashboard_activity_project_name_was_suspended( - project_name: projectName, - suspension_date: Format.date(secondsInUTC: suspendedAt, dateStyle: .long, timeStyle: .none) - .nonBreakingSpaced() - ) - self.title.assertValues([expected], "Emits title indicating the project was suspended") - } -} diff --git a/Library/ViewModels/ProjectActivitySuccessCellViewModel.swift b/Library/ViewModels/ProjectActivitySuccessCellViewModel.swift deleted file mode 100644 index 1c7342f36d..0000000000 --- a/Library/ViewModels/ProjectActivitySuccessCellViewModel.swift +++ /dev/null @@ -1,62 +0,0 @@ -import Foundation -import KsApi -import Prelude -import ReactiveExtensions -import ReactiveSwift - -public protocol ProjectActivitySuccessCellViewModelInputs { - /// Call to set the activity and project. - func configureWith(activity: Activity, project: Project) -} - -public protocol ProjectActivitySuccessCellViewModelOutputs { - /// Emits the background image URL. - var backgroundImageURL: Signal { get } - - /// Emits the title of the activity. - var title: Signal { get } -} - -public protocol ProjectActivitySuccessCellViewModelType { - var inputs: ProjectActivitySuccessCellViewModelInputs { get } - var outputs: ProjectActivitySuccessCellViewModelOutputs { get } -} - -public final class ProjectActivitySuccessCellViewModel: ProjectActivitySuccessCellViewModelType, - ProjectActivitySuccessCellViewModelInputs, ProjectActivitySuccessCellViewModelOutputs { - public init() { - let activityAndProject = self.activityAndProjectProperty.signal.skipNil() - let project = activityAndProject.map(second) - - self.backgroundImageURL = project.map { $0.photo.med }.map(URL.init(string:)) - - self.title = project.map { project in - var projectDeadline = "" - - if let projectDeadlineValue = project.dates.deadline { - projectDeadline = Format.date( - secondsInUTC: projectDeadlineValue, dateStyle: .long, - timeStyle: .none - ).nonBreakingSpaced() - } - - return Strings.dashboard_activity_successfully_raised_pledged( - pledged: Format.currency(project.stats.pledged, country: project.country).nonBreakingSpaced(), - backers: Strings.general_backer_count_backers(backer_count: project.stats.backersCount) - .nonBreakingSpaced(), - deadline: projectDeadline - ) - } - } - - fileprivate let activityAndProjectProperty = MutableProperty<(Activity, Project)?>(nil) - public func configureWith(activity: Activity, project: Project) { - self.activityAndProjectProperty.value = (activity, project) - } - - public let backgroundImageURL: Signal - public let title: Signal - - public var inputs: ProjectActivitySuccessCellViewModelInputs { return self } - public var outputs: ProjectActivitySuccessCellViewModelOutputs { return self } -} diff --git a/Library/ViewModels/ProjectActivitySuccessCellViewModelTests.swift b/Library/ViewModels/ProjectActivitySuccessCellViewModelTests.swift deleted file mode 100644 index f84244cb1a..0000000000 --- a/Library/ViewModels/ProjectActivitySuccessCellViewModelTests.swift +++ /dev/null @@ -1,62 +0,0 @@ -@testable import KsApi -@testable import Library -import Prelude -import ReactiveExtensions_TestHelpers -import XCTest - -internal final class ProjectActivitySuccessViewModelTests: TestCase { - fileprivate let vm: ProjectActivitySuccessCellViewModelType = ProjectActivitySuccessCellViewModel() - - fileprivate let backgroundImage = TestObserver() - fileprivate let title = TestObserver() - - override func setUp() { - super.setUp() - - self.vm.outputs.backgroundImageURL.map { $0?.absoluteString }.observe(self.backgroundImage.observer) - self.vm.outputs.title.observe(self.title.observer) - - AppEnvironment.login(AccessTokenEnvelope(accessToken: "deadbeef", user: .template)) - } - - func testBackgroundImage() { - let project = .template - |> Project.lens.photo.med .~ "http://coolpic.com/cool.jpg" - |> Project.lens.state .~ .successful - let activity = .template - |> Activity.lens.category .~ .success - |> Activity.lens.project .~ project - - self.vm.inputs.configureWith(activity: activity, project: project) - self.backgroundImage.assertValues(["http://coolpic.com/cool.jpg"], "Emits project's image URL") - } - - func testTitle() { - let country = Project.Country.us - let backersCount = 12_345 - let deadline = Date().timeIntervalSince1970 - let pledged = 5_000 - - let project = .template - |> Project.lens.country .~ country - |> Project.lens.dates.deadline .~ deadline - |> Project.lens.state .~ .successful - |> Project.lens.stats.backersCount .~ backersCount - |> Project.lens.stats.pledged .~ pledged - let activity = .template - |> Activity.lens.category .~ .success - |> Activity.lens.project .~ project - - self.vm.inputs.configureWith(activity: activity, project: project) - - let expected = Strings.dashboard_activity_successfully_raised_pledged( - pledged: Format.currency(pledged, country: country).nonBreakingSpaced(), - backers: Strings.general_backer_count_backers(backer_count: project.stats.backersCount) - .nonBreakingSpaced(), - deadline: Format.date(secondsInUTC: deadline, dateStyle: .long, timeStyle: .none) - .nonBreakingSpaced() - ) - - self.title.assertValues([expected], "Emits title with the pledged amount, backers, date of success") - } -} diff --git a/Library/ViewModels/ProjectActivityUpdateCellViewModel.swift b/Library/ViewModels/ProjectActivityUpdateCellViewModel.swift deleted file mode 100644 index 4b47050148..0000000000 --- a/Library/ViewModels/ProjectActivityUpdateCellViewModel.swift +++ /dev/null @@ -1,102 +0,0 @@ -import KsApi -import Prelude -import ReactiveExtensions -import ReactiveSwift - -public protocol ProjectActivityUpdateCellViewModelInputs { - /// Call to set the activity and project. - func configureWith(activity: Activity, project: Project) -} - -public protocol ProjectActivityUpdateCellViewModelOutputs { - /// Emits the update's author and sequence. - var activityTitle: Signal { get } - - /// Emits the update's body. - var body: Signal { get } - - /// Emits the cell's accessibility label. - var cellAccessibilityLabel: Signal { get } - - /// Emits the cell's accessibility value. - var cellAccessibilityValue: Signal { get } - - /// Emits the number of comments. - var commentsCount: Signal { get } - - /// Emits the number of likes. - var likesCount: Signal { get } - - /// Emits the title of the update. - var updateTitle: Signal { get } -} - -public protocol ProjectActivityUpdateCellViewModelType { - var inputs: ProjectActivityUpdateCellViewModelInputs { get } - var outputs: ProjectActivityUpdateCellViewModelOutputs { get } -} - -public final class ProjectActivityUpdateCellViewModel: ProjectActivityUpdateCellViewModelType, - ProjectActivityUpdateCellViewModelInputs, ProjectActivityUpdateCellViewModelOutputs { - public init() { - let activityAndProject = self.activityAndProjectProperty.signal.skipNil() - let activity = activityAndProject.map(first) - - self.activityTitle = activity.map(updateNumber(activity:)) - - self.body = activity.map { activity in - guard let update = activity.update else { return "" } - return update.body?.htmlStripped()?.truncated(maxLength: 300) ?? "" - } - - self.cellAccessibilityLabel = activity.map { activity in - updateNumber(activity: activity).htmlStripped() ?? "" - } - - self.cellAccessibilityValue = activity.map(title(activity:)) - - self.commentsCount = activity.map { activity in - guard let update = activity.update else { return "" } - guard let commentsCount = update.commentsCount else { return "" } - return Format.wholeNumber(commentsCount) - } - - self.likesCount = activity.map { activity in - guard let update = activity.update else { return "" } - guard let likesCount = update.likesCount else { return "" } - return Format.wholeNumber(likesCount) - } - - self.updateTitle = activity.map(title(activity:)) - } - - fileprivate let activityAndProjectProperty = MutableProperty<(Activity, Project)?>(nil) - public func configureWith(activity: Activity, project: Project) { - self.activityAndProjectProperty.value = (activity, project) - } - - public let activityTitle: Signal - public let body: Signal - public let cellAccessibilityLabel: Signal - public let cellAccessibilityValue: Signal - public let commentsCount: Signal - public let likesCount: Signal - public let updateTitle: Signal - - public var inputs: ProjectActivityUpdateCellViewModelInputs { return self } - public var outputs: ProjectActivityUpdateCellViewModelOutputs { return self } -} - -private func updateNumber(activity: Activity) -> String { - guard let update = activity.update else { return "" } - return Strings.dashboard_activity_update_number_posted_time_count_days_ago( - space: "\u{00a0}", - update_number: Format.wholeNumber(update.sequence), - time_count_days_ago: update.publishedAt.map { Format.relative(secondsInUTC: $0) } ?? "" - ) -} - -private func title(activity: Activity) -> String { - guard let update = activity.update else { return "" } - return update.title -} diff --git a/Library/ViewModels/ProjectActivityUpdateCellViewModelTests.swift b/Library/ViewModels/ProjectActivityUpdateCellViewModelTests.swift deleted file mode 100644 index 254ec82e7f..0000000000 --- a/Library/ViewModels/ProjectActivityUpdateCellViewModelTests.swift +++ /dev/null @@ -1,144 +0,0 @@ -@testable import KsApi -@testable import Library -import Prelude -import ReactiveExtensions_TestHelpers -import XCTest - -internal final class ProjectActivityUpdateCellViewModelTests: TestCase { - fileprivate let vm: ProjectActivityUpdateCellViewModelType = ProjectActivityUpdateCellViewModel() - - fileprivate let activityTitle = TestObserver() - fileprivate let body = TestObserver() - fileprivate let cellAccessibilityLabel = TestObserver() - fileprivate let cellAccessibilityValue = TestObserver() - fileprivate let commentsCount = TestObserver() - fileprivate let defaultUser = User.template |> \.name .~ "Christopher" - fileprivate let likesCount = TestObserver() - fileprivate let updateTitle = TestObserver() - - override func setUp() { - super.setUp() - - self.vm.outputs.activityTitle.observe(self.activityTitle.observer) - self.vm.outputs.body.observe(self.body.observer) - self.vm.outputs.cellAccessibilityLabel.observe(self.cellAccessibilityLabel.observer) - self.vm.outputs.cellAccessibilityValue.observe(self.cellAccessibilityValue.observer) - self.vm.outputs.commentsCount.observe(self.commentsCount.observer) - self.vm.outputs.likesCount.observe(self.likesCount.observer) - self.vm.outputs.updateTitle.observe(self.updateTitle.observer) - - AppEnvironment.login(AccessTokenEnvelope(accessToken: "deadbeef", user: self.defaultUser)) - } - - func testActivityTitle() { - let project = Project.template - let publishedAt = Date().timeIntervalSince1970 - let update = .template - |> Update.lens.publishedAt .~ publishedAt - |> Update.lens.sequence .~ 9 - |> Update.lens.user .~ self.defaultUser - let activity = .template - |> Activity.lens.project .~ project - |> Activity.lens.update .~ update - - self.vm.inputs.configureWith(activity: activity, project: project) - let expected = Strings.dashboard_activity_update_number_posted_time_count_days_ago( - space: "\u{00a0}", - update_number: "9", - time_count_days_ago: Format.relative(secondsInUTC: publishedAt) - ) - self.activityTitle.assertValues([expected], "Emits activity's title") - } - - func testBody() { - let body = "We've reached our funding goal, thanks y'all!" - let project = Project.template - let update = .template - |> Update.lens.body .~ body - let activity = .template - |> Activity.lens.update .~ update - - self.vm.inputs.configureWith(activity: activity, project: project) - self.body.assertValues([body], "Emits update's body") - } - - func testBodyIsStrippedOfHtml() { - let project = Project.template - let update = .template - |> Update.lens.body .~ "Oh yeah!" - let activity = .template - |> Activity.lens.project .~ project - |> Activity.lens.update .~ update - - self.vm.inputs.configureWith(activity: activity, project: project) - self.body.assertValues(["Oh yeah!"], "Emits update's body, with HTML stripped") - } - - func testCellAccessibilityLabel() { - let project = Project.template - let publishedAt = Date().timeIntervalSince1970 - let update = .template - |> Update.lens.publishedAt .~ publishedAt - |> Update.lens.sequence .~ 9 - |> Update.lens.user .~ self.defaultUser - let activity = .template - |> Activity.lens.project .~ project - |> Activity.lens.update .~ update - - self.vm.inputs.configureWith(activity: activity, project: project) - let expected = (Strings.dashboard_activity_update_number_posted_time_count_days_ago( - space: "\u{00a0}", - update_number: "9", - time_count_days_ago: Format.relative(secondsInUTC: publishedAt) - ) - .htmlStripped() ?? "") - self.cellAccessibilityLabel.assertValues([expected], "Emits accessibility label") - } - - func testCellAccessibilityValue() { - let title = "Spirit animals!" - let project = Project.template - let update = .template - |> Update.lens.title .~ title - let activity = .template - |> Activity.lens.update .~ update - - self.vm.inputs.configureWith(activity: activity, project: project) - self.cellAccessibilityValue.assertValues([title], "Emits accessibility value") - } - - func testCommentsCount() { - let project = Project.template - let update = .template - |> Update.lens.commentsCount .~ 50 - let activity = .template - |> Activity.lens.project .~ project - |> Activity.lens.update .~ update - - self.vm.inputs.configureWith(activity: activity, project: project) - self.commentsCount.assertValues([Format.wholeNumber(50)], "Emits number of comments") - } - - func testLikesCount() { - let project = Project.template - let update = .template - |> Update.lens.likesCount .~ 25 - let activity = .template - |> Activity.lens.project .~ project - |> Activity.lens.update .~ update - - self.vm.inputs.configureWith(activity: activity, project: project) - self.likesCount.assertValues([Format.wholeNumber(25)], "Emits number of likes") - } - - func testUpdateTitle() { - let project = Project.template - let update = .template - |> Update.lens.title .~ "Great news!" - let activity = .template - |> Activity.lens.update .~ update - - self.vm.inputs.configureWith(activity: activity, project: project) - self.updateTitle.assertValues(["Great news!"], "Emits update's title") - } -} diff --git a/Library/ViewModels/ProjectPageViewModel.swift b/Library/ViewModels/ProjectPageViewModel.swift index 513aad4102..585a4e335e 100644 --- a/Library/ViewModels/ProjectPageViewModel.swift +++ b/Library/ViewModels/ProjectPageViewModel.swift @@ -93,9 +93,6 @@ public protocol ProjectPageViewModelOutputs { /// Emits a `Project` when the comments are to be rendered. var goToComments: Signal { get } - /// Emits a `Param` when the creator header cell progress view is tapped. - var goToDashboard: Signal { get } - /// Emits `ManagePledgeViewParamConfigData` to take the user to the `ManagePledgeViewController` var goToManagePledge: Signal { get } @@ -349,10 +346,6 @@ public final class ProjectPageViewModel: ProjectPageViewModelType, ProjectPageVi self.goToComments = project .takeWhen(self.tappedCommentsProperty.signal) - self.goToDashboard = self.tappedViewProgressProperty.signal - .skipNil() - .map { .id($0.id) } - self.goToUpdates = project .takeWhen(self.tappedUpdatesProperty.signal) @@ -628,7 +621,6 @@ public final class ProjectPageViewModel: ProjectPageViewModelType, ProjectPageVi public let configureProjectNavigationSelectorView: Signal<(Project, RefTag?), Never> public let dismissManagePledgeAndShowMessageBannerWithMessage: Signal public let goToComments: Signal - public let goToDashboard: Signal public let goToManagePledge: Signal public let goToRewards: Signal<(Project, RefTag?), Never> public let goToUpdates: Signal diff --git a/Library/ViewModels/ProjectPageViewModelTests.swift b/Library/ViewModels/ProjectPageViewModelTests.swift index 3aa8b1cdbb..ec2d787a55 100644 --- a/Library/ViewModels/ProjectPageViewModelTests.swift +++ b/Library/ViewModels/ProjectPageViewModelTests.swift @@ -35,7 +35,6 @@ final class ProjectPageViewModelTests: TestCase { private let configureProjectNavigationSelectorView = TestObserver<(Project, RefTag?), Never>() private let dismissManagePledgeAndShowMessageBannerWithMessage = TestObserver() private let goToComments = TestObserver() - private let goToDashboard = TestObserver() private let goToManagePledgeProjectParam = TestObserver() private let goToManagePledgeBackingParam = TestObserver() private let goToReportProject = TestObserver<(Bool, String, String), Never>() @@ -103,7 +102,6 @@ final class ProjectPageViewModelTests: TestCase { self.vm.outputs.dismissManagePledgeAndShowMessageBannerWithMessage .observe(self.dismissManagePledgeAndShowMessageBannerWithMessage.observer) self.vm.outputs.goToComments.observe(self.goToComments.observer) - self.vm.outputs.goToDashboard.observe(self.goToDashboard.observer) self.vm.outputs.goToManagePledge.map(first).observe(self.goToManagePledgeProjectParam.observer) self.vm.outputs.goToManagePledge.map(second).observe(self.goToManagePledgeBackingParam.observer) self.vm.outputs.goToReportProject.observe(self.goToReportProject.observer) @@ -770,18 +768,6 @@ final class ProjectPageViewModelTests: TestCase { XCTAssertEqual(self.goToReportProject.lastValue?.2, project.urls.web.project) } - func testGoToDashboard() { - self.vm.inputs.configureWith(projectOrParam: .left(.template), refTag: .discovery) - - self.vm.inputs.viewDidLoad() - - self.goToDashboard.assertDidNotEmitValue() - - self.vm.inputs.tappedViewProgress(of: .template) - - self.goToDashboard.assertValues([.id(Project.template.id)]) - } - func testGoToRewards() { withEnvironment(config: .template, mainBundle: self.releaseBundle) { let project = Project.template diff --git a/Library/ViewModels/ProjectPamphletContentViewModel.swift b/Library/ViewModels/ProjectPamphletContentViewModel.swift index c06293c2c1..5b2f57acad 100644 --- a/Library/ViewModels/ProjectPamphletContentViewModel.swift +++ b/Library/ViewModels/ProjectPamphletContentViewModel.swift @@ -19,7 +19,6 @@ public protocol ProjectPamphletContentViewModelInputs { public protocol ProjectPamphletContentViewModelOutputs { var goToBacking: Signal { get } var goToComments: Signal { get } - var goToDashboard: Signal { get } var goToRewardPledge: Signal<(Project, Reward), Never> { get } var goToUpdates: Signal { get } var loadMinimalProjectIntoDataSource: Signal { get } @@ -92,10 +91,6 @@ public final class ProjectPamphletContentViewModel: ProjectPamphletContentViewMo self.goToUpdates = project .takeWhen(self.tappedUpdatesProperty.signal) - - self.goToDashboard = self.tappedViewProgressProperty.signal - .skipNil() - .map { .id($0.id) } } fileprivate let configDataProperty = MutableProperty<(Project, RefTag?)?>(nil) @@ -145,7 +140,6 @@ public final class ProjectPamphletContentViewModel: ProjectPamphletContentViewMo public let goToBacking: Signal public let goToComments: Signal - public let goToDashboard: Signal public let goToRewardPledge: Signal<(Project, Reward), Never> public let goToUpdates: Signal public let loadMinimalProjectIntoDataSource: Signal diff --git a/Library/ViewModels/ProjectPamphletContentViewModelTests.swift b/Library/ViewModels/ProjectPamphletContentViewModelTests.swift index f97457d042..9fbd5fe6a3 100644 --- a/Library/ViewModels/ProjectPamphletContentViewModelTests.swift +++ b/Library/ViewModels/ProjectPamphletContentViewModelTests.swift @@ -11,7 +11,6 @@ final class ProjectPamphletContentViewModelTests: TestCase { fileprivate let goToBackingProjectParam = TestObserver() fileprivate let goToBackingBackingParam = TestObserver() fileprivate let goToComments = TestObserver() - fileprivate let goToDashboard = TestObserver() fileprivate let goToRewardPledgeProject = TestObserver() fileprivate let goToRewardPledgeReward = TestObserver() fileprivate let goToUpdates = TestObserver() @@ -25,7 +24,6 @@ final class ProjectPamphletContentViewModelTests: TestCase { self.vm.outputs.goToBacking.map(first).observe(self.goToBackingProjectParam.observer) self.vm.outputs.goToBacking.map(second).observe(self.goToBackingBackingParam.observer) self.vm.outputs.goToComments.observe(self.goToComments.observer) - self.vm.outputs.goToDashboard.observe(self.goToDashboard.observer) self.vm.outputs.goToRewardPledge.map(first).observe(self.goToRewardPledgeProject.observer) self.vm.outputs.goToRewardPledge.map(second).observe(self.goToRewardPledgeReward.observer) self.vm.outputs.goToUpdates.observe(self.goToUpdates.observer) @@ -70,15 +68,6 @@ final class ProjectPamphletContentViewModelTests: TestCase { self.goToComments.assertValues([project]) } - func testGoToDashboard() { - let project = Project.template - let param: Param = .id(project.id) - - self.vm.inputs.tappedViewProgress(of: project) - - self.goToDashboard.assertValue(param) - } - func testGoToRewardPledge_LiveProject_NoReward() { let project = Project.template let reward = Reward.noReward diff --git a/Library/ViewModels/RemoteConfigFeatureFlagToolsViewModel.swift b/Library/ViewModels/RemoteConfigFeatureFlagToolsViewModel.swift index 1413d96a93..a94c3f40ba 100644 --- a/Library/ViewModels/RemoteConfigFeatureFlagToolsViewModel.swift +++ b/Library/ViewModels/RemoteConfigFeatureFlagToolsViewModel.swift @@ -89,8 +89,6 @@ private func isFeatureEnabled(_ feature: RemoteConfigFeature) -> Bool { return featureConsentManagementDialogEnabled() case .facebookLoginInterstitialEnabled: return featureFacebookLoginInterstitialEnabled() - case .creatorDashboardEnabled: - return featureCreatorDashboardEnabled() case .reportThisProjectEnabled: return featureReportThisProjectEnabled() case .useOfAIProjectTab: @@ -108,9 +106,6 @@ private func getValueFromUserDefaults(for feature: RemoteConfigFeature) -> Bool? case .facebookLoginInterstitialEnabled: return AppEnvironment.current.userDefaults .remoteConfigFeatureFlags[RemoteConfigFeature.facebookLoginInterstitialEnabled.rawValue] - case .creatorDashboardEnabled: - return AppEnvironment.current.userDefaults - .remoteConfigFeatureFlags[RemoteConfigFeature.creatorDashboardEnabled.rawValue] case .reportThisProjectEnabled: return AppEnvironment.current.userDefaults .remoteConfigFeatureFlags[RemoteConfigFeature.reportThisProjectEnabled.rawValue] @@ -130,9 +125,6 @@ private func setValueInUserDefaults(for feature: RemoteConfigFeature, and value: case .facebookLoginInterstitialEnabled: return AppEnvironment.current.userDefaults .remoteConfigFeatureFlags[RemoteConfigFeature.facebookLoginInterstitialEnabled.rawValue] = value - case .creatorDashboardEnabled: - return AppEnvironment.current.userDefaults - .remoteConfigFeatureFlags[RemoteConfigFeature.creatorDashboardEnabled.rawValue] = value case .reportThisProjectEnabled: return AppEnvironment.current.userDefaults .remoteConfigFeatureFlags[RemoteConfigFeature.reportThisProjectEnabled.rawValue] = value diff --git a/Library/ViewModels/RemoteConfigFeatureFlagToolsViewModelTests.swift b/Library/ViewModels/RemoteConfigFeatureFlagToolsViewModelTests.swift index 3bedaade69..ed5eaea83f 100644 --- a/Library/ViewModels/RemoteConfigFeatureFlagToolsViewModelTests.swift +++ b/Library/ViewModels/RemoteConfigFeatureFlagToolsViewModelTests.swift @@ -24,7 +24,6 @@ final class RemoteConfigFlagToolsViewModelTests: TestCase { |> \.features .~ [ RemoteConfigFeature.consentManagementDialogEnabled.rawValue: true, RemoteConfigFeature.facebookLoginInterstitialEnabled.rawValue: true, - RemoteConfigFeature.creatorDashboardEnabled.rawValue: true, RemoteConfigFeature.reportThisProjectEnabled.rawValue: true, RemoteConfigFeature.useOfAIProjectTab.rawValue: true ] @@ -47,7 +46,6 @@ final class RemoteConfigFlagToolsViewModelTests: TestCase { |> \.features .~ [ RemoteConfigFeature.consentManagementDialogEnabled.rawValue: true, RemoteConfigFeature.facebookLoginInterstitialEnabled.rawValue: true, - RemoteConfigFeature.creatorDashboardEnabled.rawValue: false, RemoteConfigFeature.reportThisProjectEnabled.rawValue: false, RemoteConfigFeature.useOfAIProjectTab.rawValue: false ] diff --git a/Library/ViewModels/RootViewModel.swift b/Library/ViewModels/RootViewModel.swift index ee9a13bc06..7e8d8d74a2 100644 --- a/Library/ViewModels/RootViewModel.swift +++ b/Library/ViewModels/RootViewModel.swift @@ -10,7 +10,6 @@ public enum RootViewControllerData: Equatable { case discovery case activities case search - case dashboard(isMember: Bool) case profile(isLoggedIn: Bool) public static func == (lhs: RootViewControllerData, rhs: RootViewControllerData) -> Bool { @@ -18,8 +17,6 @@ public enum RootViewControllerData: Equatable { case (.discovery, .discovery): return true case (.activities, .activities): return true case (.search, .search): return true - case let (.dashboard(lhsIsMember), .dashboard(rhsIsMember)): - return lhsIsMember == rhsIsMember case let (.profile(lhsIsLoggedIn), .profile(rhsIsLoggedIn)): return lhsIsLoggedIn == rhsIsLoggedIn default: @@ -27,24 +24,6 @@ public enum RootViewControllerData: Equatable { } } - var isNil: Bool { - switch self { - case let .dashboard(isMember): - return !isMember - default: - return false - } - } - - var isDashboard: Bool { - switch self { - case .dashboard: - return true - default: - return false - } - } - var isProfile: Bool { switch self { case .profile: @@ -58,12 +37,10 @@ public enum RootViewControllerData: Equatable { public struct TabBarItemsData { public let items: [TabBarItem] public let isLoggedIn: Bool - public let isMember: Bool } public enum TabBarItem { case activity(index: RootViewControllerIndex) - case dashboard(index: RootViewControllerIndex) case home(index: RootViewControllerIndex) case profile(avatarUrl: URL?, index: RootViewControllerIndex) case search(index: RootViewControllerIndex) @@ -88,9 +65,6 @@ public protocol RootViewModelInputs { /// Call when we should switch to the activities tab. func switchToActivities() - /// Call when we should switch to the creator dashboard tab. - func switchToDashboard(project param: Param?) - /// Call when we should switch to the discovery tab. func switchToDiscovery(params: DiscoveryParams?) @@ -136,9 +110,6 @@ public protocol RootViewModelOutputs { /// Emits the array of view controllers that should be set on the tab bar. var setViewControllers: Signal<[RootViewControllerData], Never> { get } - /// Emits when the dashboard should switch projects. - var switchDashboardProject: Signal<(RootViewControllerIndex, Param), Never> { get } - /// Emits data for setting tab bar item styles. var tabBarItemsData: Signal { get } @@ -161,29 +132,26 @@ public final class RootViewModel: RootViewModelType, RootViewModelInputs, RootVi ) .map { _ in AppEnvironment.current.currentUser } - let userState: Signal<(isLoggedIn: Bool, isMember: Bool), Never> = currentUser + let loginState: Signal = currentUser .map { - ( - $0 != nil, - ($0?.stats.memberProjectsCount ?? 0) > 0 - ) + $0 != nil } .skipRepeats(==) let standardViewControllers = self.viewDidLoadProperty.signal.map { _ -> [RootViewControllerData] in generateStandardViewControllers() } - let personalizedViewControllers = userState.map { userState -> [RootViewControllerData] in - generatePersonalizedViewControllers(userState: (userState.isMember, userState.isLoggedIn)) + let personalizedViewControllers = loginState.map { loginState -> [RootViewControllerData] in + generatePersonalizedViewControllers(isLoggedIn: loginState) } let viewControllers = Signal.combineLatest(standardViewControllers, personalizedViewControllers).map(+) - let refreshedViewControllers = userState.takeWhen(self.userLocalePreferencesChangedProperty.signal) - .map { userState -> [RootViewControllerData] in + let refreshedViewControllers = loginState.takeWhen(self.userLocalePreferencesChangedProperty.signal) + .map { loginState -> [RootViewControllerData] in let standard = generateStandardViewControllers() let personalized = generatePersonalizedViewControllers( - userState: (userState.isMember, userState.isLoggedIn) + isLoggedIn: loginState ) return standard + personalized @@ -193,9 +161,7 @@ public final class RootViewModel: RootViewModelType, RootViewModelInputs, RootVi viewControllers, refreshedViewControllers ) - .map { $0.filter { !$0.isNil } } - let loginState = userState.map { $0.isLoggedIn } let vcCount = self.setViewControllers.map { $0.count } let switchToLogin = Signal.combineLatest(vcCount, loginState) @@ -215,19 +181,6 @@ public final class RootViewModel: RootViewModelType, RootViewModelInputs, RootVi self.filterDiscovery = discoveryControllerIndex .takePairWhen(self.switchToDiscoveryProperty.signal.skipNil()) - let dashboardControllerIndex = self.setViewControllers - .map { $0.firstIndex(where: { $0.isDashboard }) } - .skipNil() - - self.switchDashboardProject = Signal - .combineLatest(dashboardControllerIndex, self.switchToDashboardProperty.signal.skipNil(), loginState) - .filter { _, _, loginState in - isTrue(loginState) - } - .map { dashboard, param, _ in - (dashboard, param) - } - self.selectedIndex = Signal.combineLatest( .merge( self.viewDidLoadProperty.signal.mapConst(0), @@ -236,8 +189,7 @@ public final class RootViewModel: RootViewModelType, RootViewModelInputs, RootVi self.switchToDiscoveryProperty.signal.mapConst(0), self.switchToSearchProperty.signal.mapConst(2), switchToLogin, - switchToProfile, - self.switchToDashboardProperty.signal.mapConst(3) + switchToProfile ), self.setViewControllers, self.viewDidLoadProperty.signal @@ -414,11 +366,6 @@ public final class RootViewModel: RootViewModelType, RootViewModelInputs, RootVi self.switchToActivitiesProperty.value = () } - fileprivate let switchToDashboardProperty = MutableProperty(nil) - public func switchToDashboard(project param: Param?) { - self.switchToDashboardProperty.value = param - } - fileprivate let switchToDiscoveryProperty = MutableProperty(nil) public func switchToDiscovery(params: DiscoveryParams?) { self.switchToDiscoveryProperty.value = params @@ -469,7 +416,6 @@ public final class RootViewModel: RootViewModelType, RootViewModelInputs, RootVi public let selectedIndex: Signal public let setBadgeValueAtIndex: Signal public let setViewControllers: Signal<[RootViewControllerData], Never> - public let switchDashboardProject: Signal<(Int, Param), Never> public let tabBarItemsData: Signal public let updateUserInEnvironment: Signal @@ -486,37 +432,20 @@ private func generateStandardViewControllers() -> [RootViewControllerData] { return [.discovery, .activities, .search] } -private func generatePersonalizedViewControllers(userState: (isMember: Bool, isLoggedIn: Bool)) +private func generatePersonalizedViewControllers(isLoggedIn: Bool) -> [RootViewControllerData] { - if featureCreatorDashboardEnabled() { - return [.dashboard(isMember: userState.isMember), .profile(isLoggedIn: userState.isLoggedIn)] - } - - return [.profile(isLoggedIn: userState.isLoggedIn)] + return [.profile(isLoggedIn: isLoggedIn)] } private func tabData(forUser user: User?) -> TabBarItemsData { - let isMember = - (user?.stats.memberProjectsCount ?? 0) > 0 - let items: [TabBarItem] - - switch isMember { - case false: - items = [ - .home(index: 0), .activity(index: 1), .search(index: 2), - .profile(avatarUrl: (user?.avatar.small).flatMap(URL.init(string:)), index: 3) - ] - case true: - items = [ - .home(index: 0), .activity(index: 1), .search(index: 2), .dashboard(index: 3), - .profile(avatarUrl: (user?.avatar.small).flatMap(URL.init(string:)), index: 4) - ] - } + let items: [TabBarItem] = [ + .home(index: 0), .activity(index: 1), .search(index: 2), + .profile(avatarUrl: (user?.avatar.small).flatMap(URL.init(string:)), index: 3) + ] return TabBarItemsData( items: items, - isLoggedIn: user != nil, - isMember: isMember + isLoggedIn: user != nil ) } @@ -540,8 +469,6 @@ private func tabBarItemLabel(for tabBarItem: TabBarItem) -> KSRAnalytics.TabBarI switch tabBarItem { case .activity: return .activity - case .dashboard: - return .dashboard case .home: return .discovery case .profile: diff --git a/Library/ViewModels/RootViewModelTests.swift b/Library/ViewModels/RootViewModelTests.swift index 9c6724dac4..becfa404c5 100644 --- a/Library/ViewModels/RootViewModelTests.swift +++ b/Library/ViewModels/RootViewModelTests.swift @@ -15,7 +15,6 @@ final class RootViewModelTests: TestCase { let setBadgeValueAtIndexValue = TestObserver() let setBadgeValueAtIndexIndex = TestObserver() let scrollToTopControllerName = TestObserver() - let switchDashboardProject = TestObserver() let tabBarItemsData = TestObserver() let updateUserInEnvironment = TestObserver() @@ -30,7 +29,6 @@ final class RootViewModelTests: TestCase { self.vm.outputs.selectedIndex.observe(self.selectedIndex.observer) self.vm.outputs.setBadgeValueAtIndex.map { $0.0 }.observe(self.setBadgeValueAtIndexValue.observer) self.vm.outputs.setBadgeValueAtIndex.map { $0.1 }.observe(self.setBadgeValueAtIndexIndex.observer) - self.vm.outputs.switchDashboardProject.map(second).observe(self.switchDashboardProject.observer) self.vm.outputs.updateUserInEnvironment.observe(self.updateUserInEnvironment.observer) let viewControllers = self.vm.outputs.setViewControllers @@ -360,186 +358,54 @@ final class RootViewModelTests: TestCase { } } - func testSetViewControllers_WhenCreatorDashboardEnabled() { - let mockRemoteConfigClient = MockRemoteConfigClient() - |> \.features .~ [ - RemoteConfigFeature.creatorDashboardEnabled.rawValue: true - ] + func testSetViewControllers() { let viewControllerNames = TestObserver<[String], Never>() - withEnvironment(remoteConfigClient: mockRemoteConfigClient) { - vm.outputs.setViewControllers.map(extractRootNames) - .observe(viewControllerNames.observer) - - self.vm.inputs.viewDidLoad() - - viewControllerNames.assertValues( - [ - ["Discovery", "Activities", "Search", "LoginTout"] - ], - "Show the logged out tabs." - ) - - AppEnvironment.login(AccessTokenEnvelope(accessToken: "deadbeef", user: .template)) - self.vm.inputs.userSessionStarted() - - viewControllerNames.assertValues( - [ - ["Discovery", "Activities", "Search", "LoginTout"], - ["Discovery", "Activities", "Search", "BackerDashboard"] - ], - "Show the logged in tabs." - ) - - AppEnvironment.updateCurrentUser(.template |> \.stats.memberProjectsCount .~ 1) - self.vm.inputs.currentUserUpdated() - - viewControllerNames.assertValues( - [ - ["Discovery", "Activities", "Search", "LoginTout"], - ["Discovery", "Activities", "Search", "BackerDashboard"], - ["Discovery", "Activities", "Search", "Dashboard", "BackerDashboard"] - ], - "Show the creator dashboard tab." - ) - - AppEnvironment.logout() - self.vm.inputs.userSessionEnded() - - viewControllerNames.assertValues( - [ - ["Discovery", "Activities", "Search", "LoginTout"], - ["Discovery", "Activities", "Search", "BackerDashboard"], - ["Discovery", "Activities", "Search", "Dashboard", "BackerDashboard"], - ["Discovery", "Activities", "Search", "LoginTout"] - ], - "Show the logged out tabs." - ) - } - } - - func testSetViewControllers_WhenCreatorDashboardDisabled_NoCreatorDashboard() { - let mockRemoteConfigClient = MockRemoteConfigClient() - |> \.features .~ [ - RemoteConfigFeature.creatorDashboardEnabled.rawValue: false - ] - let viewControllerNames = TestObserver<[String], Never>() + vm.outputs.setViewControllers.map(extractRootNames) + .observe(viewControllerNames.observer) - withEnvironment(remoteConfigClient: mockRemoteConfigClient) { - vm.outputs.setViewControllers.map(extractRootNames) - .observe(viewControllerNames.observer) + self.vm.inputs.viewDidLoad() - self.vm.inputs.viewDidLoad() + viewControllerNames.assertValues( + [ + ["Discovery", "Activities", "Search", "LoginTout"] + ], + "Show the logged out tabs." + ) - viewControllerNames.assertValues( - [ - ["Discovery", "Activities", "Search", "LoginTout"] - ], - "Show the logged out tabs." - ) - - AppEnvironment.login(AccessTokenEnvelope(accessToken: "deadbeef", user: .template)) - self.vm.inputs.userSessionStarted() - - viewControllerNames.assertValues( - [ - ["Discovery", "Activities", "Search", "LoginTout"], - ["Discovery", "Activities", "Search", "BackerDashboard"] - ], - "Show the logged in tabs." - ) - - AppEnvironment.updateCurrentUser(.template |> \.stats.memberProjectsCount .~ 1) - self.vm.inputs.currentUserUpdated() + AppEnvironment.login(AccessTokenEnvelope(accessToken: "deadbeef", user: .template)) + self.vm.inputs.userSessionStarted() - viewControllerNames.assertValues( - [ - ["Discovery", "Activities", "Search", "LoginTout"], - ["Discovery", "Activities", "Search", "BackerDashboard"], - ["Discovery", "Activities", "Search", "BackerDashboard"] - ], - "Don't show the creator dashboard tab." - ) + viewControllerNames.assertValues( + [ + ["Discovery", "Activities", "Search", "LoginTout"], + ["Discovery", "Activities", "Search", "BackerDashboard"] + ], + "Show the logged in tabs." + ) - AppEnvironment.logout() - self.vm.inputs.userSessionEnded() + AppEnvironment.updateCurrentUser(.template |> \.stats.memberProjectsCount .~ 1) + self.vm.inputs.currentUserUpdated() - viewControllerNames.assertValues( - [ - ["Discovery", "Activities", "Search", "LoginTout"], - ["Discovery", "Activities", "Search", "BackerDashboard"], - ["Discovery", "Activities", "Search", "BackerDashboard"], - ["Discovery", "Activities", "Search", "LoginTout"] - ], - "Show the logged out tabs." - ) - } - } + viewControllerNames.assertValues( + [ + ["Discovery", "Activities", "Search", "LoginTout"], + ["Discovery", "Activities", "Search", "BackerDashboard"] + ], + "Updating the member projects does not trigger any view controller changes" + ) - func testBackerDashboardShownWithCreatorDashboard_WhenCreatorDashboardFlagEnabled() { - let mockRemoteConfigClient = MockRemoteConfigClient() - |> \.features .~ [ - RemoteConfigFeature.creatorDashboardEnabled.rawValue: true - ] - - withEnvironment(remoteConfigClient: mockRemoteConfigClient) { - vm.inputs.viewDidLoad() - AppEnvironment.login(AccessTokenEnvelope(accessToken: "deadbeef", user: .template)) - vm.inputs.userSessionStarted() - - viewControllerNames.assertValues( - [ - ["Discovery", "Activities", "Search", "LoginTout"], - ["Discovery", "Activities", "Search", "BackerDashboard"] - ], - "Show the BackerDashboard instead of Profile." - ) - - AppEnvironment.updateCurrentUser(.template |> \.stats.memberProjectsCount .~ 1) - vm.inputs.currentUserUpdated() - - viewControllerNames.assertValues( - [ - ["Discovery", "Activities", "Search", "LoginTout"], - ["Discovery", "Activities", "Search", "BackerDashboard"], - ["Discovery", "Activities", "Search", "Dashboard", "BackerDashboard"] - ], - "Show the creator dashboard tab." - ) - } - } + AppEnvironment.logout() + self.vm.inputs.userSessionEnded() - func testBackerDashboardShownWithCreatorDashboard_WhenCreatorDashboardFlagDisabled_NoCreatorDashboard() { - let mockRemoteConfigClient = MockRemoteConfigClient() - |> \.features .~ [ - RemoteConfigFeature.creatorDashboardEnabled.rawValue: false - ] - - withEnvironment(remoteConfigClient: mockRemoteConfigClient) { - vm.inputs.viewDidLoad() - AppEnvironment.login(AccessTokenEnvelope(accessToken: "deadbeef", user: .template)) - vm.inputs.userSessionStarted() - - viewControllerNames.assertValues( - [ - ["Discovery", "Activities", "Search", "LoginTout"], - ["Discovery", "Activities", "Search", "BackerDashboard"] - ], - "Show the BackerDashboard instead of Profile." - ) - - AppEnvironment.updateCurrentUser(.template |> \.stats.memberProjectsCount .~ 1) - vm.inputs.currentUserUpdated() - - viewControllerNames.assertValues( - [ - ["Discovery", "Activities", "Search", "LoginTout"], - ["Discovery", "Activities", "Search", "BackerDashboard"], - ["Discovery", "Activities", "Search", "BackerDashboard"] - ], - "Show the creator dashboard tab." - ) - } + viewControllerNames.assertValues( + [ + ["Discovery", "Activities", "Search", "LoginTout"], + ["Discovery", "Activities", "Search", "BackerDashboard"], + ["Discovery", "Activities", "Search", "LoginTout"] + ], + "Show the logged out tabs." + ) } func testViewControllersDontOverEmit() { @@ -667,54 +533,7 @@ final class RootViewModelTests: TestCase { self.filterDiscovery.assertValues([params]) } - func testSwitchToDashboardParam_WhenCreatorDashboardEnabled() { - let mockRemoteConfigClient = MockRemoteConfigClient() - |> \.features .~ [ - RemoteConfigFeature.creatorDashboardEnabled.rawValue: true - ] - - withEnvironment(remoteConfigClient: mockRemoteConfigClient) { - self.vm.inputs.viewDidLoad() - - let param = Param.id(1) - - AppEnvironment.login(AccessTokenEnvelope( - accessToken: "deadbeef", user: .template - |> \.stats.memberProjectsCount .~ 1 - )) - self.vm.inputs.userSessionStarted() - - self.switchDashboardProject.assertValues([]) - self.vm.inputs.switchToDashboard(project: param) - - self.switchDashboardProject.assertValues([param]) - } - } - - func testSwitchToDashboardParam_WhenCreatorDashboardDisabled_NoCreatorDashboard() { - let mockRemoteConfigClient = MockRemoteConfigClient() - |> \.features .~ [ - RemoteConfigFeature.creatorDashboardEnabled.rawValue: false - ] - - withEnvironment(remoteConfigClient: mockRemoteConfigClient) { - self.vm.inputs.viewDidLoad() - - let param = Param.id(1) - - AppEnvironment.login(AccessTokenEnvelope( - accessToken: "deadbeef", user: .template - |> \.stats.memberProjectsCount .~ 1 - )) - self.vm.inputs.userSessionStarted() - - self.switchDashboardProject.assertValues([]) - self.vm.inputs.switchToDashboard(project: param) - self.switchDashboardProject.assertValues([]) - } - } - - func testTabBarItemStyles_WhenCreatorDashboardEnabled() { + func testTabBarItemStyles() { let user = User.template |> \.avatar.small .~ "http://image.com/image" let creator = User.template |> \.stats.memberProjectsCount .~ 1 @@ -733,17 +552,16 @@ final class RootViewModelTests: TestCase { .search(index: 2), .profile(avatarUrl: URL(string: user.avatar.small), index: 3) ] - let itemsMember: [TabBarItem] = [ + let itemsCreator: [TabBarItem] = [ .home(index: 0), .activity(index: 1), .search(index: 2), - .dashboard(index: 3), - .profile(avatarUrl: URL(string: creator.avatar.small), index: 4) + .profile(avatarUrl: URL(string: creator.avatar.small), index: 3) ] - let tabData = TabBarItemsData(items: items, isLoggedIn: false, isMember: false) - let tabDataLoggedIn = TabBarItemsData(items: itemsLoggedIn, isLoggedIn: true, isMember: false) - let tabDataMember = TabBarItemsData(items: itemsMember, isLoggedIn: true, isMember: true) + let tabData = TabBarItemsData(items: items, isLoggedIn: false) + let tabDataLoggedIn = TabBarItemsData(items: itemsLoggedIn, isLoggedIn: true) + let tabDataCreator = TabBarItemsData(items: itemsCreator, isLoggedIn: true) self.tabBarItemsData.assertValueCount(0) @@ -768,7 +586,7 @@ final class RootViewModelTests: TestCase { AppEnvironment.login(AccessTokenEnvelope(accessToken: "deadbeef", user: creator)) self.vm.inputs.userSessionStarted() - self.tabBarItemsData.assertValues([tabData, tabDataLoggedIn, tabDataLoggedIn, tabData, tabDataMember]) + self.tabBarItemsData.assertValues([tabData, tabDataLoggedIn, tabDataLoggedIn, tabData, tabDataCreator]) } func testSetViewControllers_DoesNotFilterDiscovery() { @@ -787,13 +605,10 @@ final class RootViewModelTests: TestCase { AppEnvironment.login(AccessTokenEnvelope(accessToken: "deadbeef", user: .template)) self.vm.inputs.userSessionStarted() - AppEnvironment.updateCurrentUser(.template |> \.stats.memberProjectsCount .~ 1) - self.vm.inputs.currentUserUpdated() - AppEnvironment.logout() self.vm.inputs.userSessionEnded() - self.viewControllerNames.assertValueCount(4) + self.viewControllerNames.assertValueCount(3) self.filterDiscovery.assertValues([params]) } } diff --git a/Library/ViewModels/ShareViewModel.swift b/Library/ViewModels/ShareViewModel.swift index 68cac73b3c..7d48556228 100644 --- a/Library/ViewModels/ShareViewModel.swift +++ b/Library/ViewModels/ShareViewModel.swift @@ -71,8 +71,6 @@ public final class ShareViewModel: ShareViewModelType, ShareViewModelInputs, Sha private func activityItemProvider(forShareContext shareContext: ShareContext) -> UIActivityItemProvider { switch shareContext { - case let .creatorDashboard(project): - return ProjectActivityItemProvider(project: project) case let .discovery(project): return ProjectActivityItemProvider(project: project) case let .project(project): @@ -86,8 +84,6 @@ private func activityItemProvider(forShareContext shareContext: ShareContext) -> private func shareUrl(forShareContext shareContext: ShareContext) -> URL? { switch shareContext { - case let .creatorDashboard(project): - return URL(string: project.urls.web.project) case let .discovery(project): return URL(string: project.urls.web.project) case let .project(project): diff --git a/Library/ViewModels/ShareViewModelTests.swift b/Library/ViewModels/ShareViewModelTests.swift index 7b889c59b8..ada1ee9ee2 100644 --- a/Library/ViewModels/ShareViewModelTests.swift +++ b/Library/ViewModels/ShareViewModelTests.swift @@ -43,13 +43,6 @@ internal final class ShareViewModelTests: TestCase { self.showShareSheet.assertValueCount(1) } - func testShowShareSheet_CreatorDashboard() { - self.vm.inputs.configureWith(shareContext: .creatorDashboard(.template), shareContextView: nil) - self.vm.inputs.shareButtonTapped() - - self.showShareSheet.assertValueCount(1) - } - func testShowShareSheet_Update() { self.vm.inputs.configureWith(shareContext: .update(.template, .template), shareContextView: nil) self.vm.inputs.shareButtonTapped()