From 58c72b13d77460713df846d2cb11e590e7b91fa8 Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Thu, 23 Apr 2020 16:01:47 +0300 Subject: [PATCH 01/11] Update dependencies 1.122 (#701) * Bump SDWebImage from 5.7.1 to 5.7.3 Bumps [SDWebImage](https://github.com/SDWebImage/SDWebImage) from 5.7.1 to 5.7.3 - [Release notes](https://github.com/SDWebImage/SDWebImage/releases/tag/5.7.3) - [Commits](https://github.com/SDWebImage/SDWebImage/compare/5.7.1...5.7.3) * Bump Agrume from 5.6.7 to 5.6.8 Bumps [Agrume](https://github.com/JanGorman/Agrume) from 5.6.7 to 5.6.8 - [Release notes](https://github.com/JanGorman/Agrume/releases/tag/5.6.8) - [Commits](https://github.com/JanGorman/Agrume/compare/5.6.7...5.6.8) * Bump Tabman from 2.8.0 to 2.8.1 Bumps [Tabman](https://github.com/uias/Tabman) from 2.8.0 to 2.8.1 - [Release notes](https://github.com/uias/Tabman/releases/tag/2.8.1) - [Commits](https://github.com/uias/Tabman/compare/2.8.0...2.8.1) * Bump Firebase from 6.22.0 to 6.23.0 Bumps [Firebase](https://github.com/firebase/firebase-ios-sdk) from 6.22.0 to 6.23.0 - [Release notes](https://github.com/firebase/firebase-ios-sdk/releases/tag/6.23.0) - [Commits](https://github.com/firebase/firebase-ios-sdk/compare/6.22.0...6.23.0) * Bump fastlane from 2.145.0 to 2.146.1 Bumps [fastlane](https://github.com/fastlane/fastlane) from 2.145.0 to 2.146.1 - [Release notes](https://github.com/fastlane/fastlane/releases/tag/2.146.1) - [Commits](https://github.com/fastlane/fastlane/compare/2.145.0...2.146.1) --- Gemfile | 2 +- Gemfile.lock | 18 +++---- Podfile | 14 +++--- Podfile.lock | 118 +++++++++++++++++++++++----------------------- fastlane/Fastfile | 2 +- 5 files changed, 78 insertions(+), 76 deletions(-) diff --git a/Gemfile b/Gemfile index 386652a2bd..835e0fe8ad 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source "https://rubygems.org" ruby "2.6.5" -gem "fastlane", "2.145.0" +gem "fastlane", "2.146.1" gem "cocoapods", "1.9.1" plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') diff --git a/Gemfile.lock b/Gemfile.lock index ec4277db6e..52e1d368eb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -14,7 +14,7 @@ GEM json (>= 1.5.1) atomos (0.1.3) aws-eventstream (1.1.0) - aws-partitions (1.296.0) + aws-partitions (1.301.0) aws-sdk-core (3.94.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.239.0) @@ -23,11 +23,11 @@ GEM aws-sdk-kms (1.30.0) aws-sdk-core (~> 3, >= 3.71.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.61.2) + aws-sdk-s3 (1.63.0) aws-sdk-core (~> 3, >= 3.83.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.1) - aws-sigv4 (1.1.1) + aws-sigv4 (1.1.2) aws-eventstream (~> 1.0, >= 1.0.2) babosa (1.0.3) claide (1.0.3) @@ -67,7 +67,7 @@ GEM cocoapods-trunk (1.4.1) nap (>= 0.8, < 2.0) netrc (~> 0.11) - cocoapods-try (1.1.0) + cocoapods-try (1.2.0) colored (1.2) colored2 (3.1.2) commander-fastlane (4.4.6) @@ -92,7 +92,7 @@ GEM faraday_middleware (0.13.1) faraday (>= 0.7.4, < 1.0) fastimage (2.1.7) - fastlane (2.145.0) + fastlane (2.146.1) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.3, < 3.0.0) aws-sdk-s3 (~> 1.0) @@ -156,13 +156,13 @@ GEM google-cloud-core (~> 1.2) googleauth (~> 0.9) mini_mime (~> 1.0) - googleauth (0.11.0) + googleauth (0.12.0) faraday (>= 0.17.3, < 2.0) jwt (>= 1.4, < 3.0) memoist (~> 0.16) multi_json (~> 1.11) os (>= 0.9, < 2.0) - signet (~> 0.12) + signet (~> 0.14) highline (1.7.10) http-cookie (1.0.3) domain_name (~> 0.5) @@ -223,7 +223,7 @@ GEM unf_ext (0.0.7.7) unicode-display_width (1.7.0) word_wrap (1.0.0) - xcodeproj (1.15.0) + xcodeproj (1.16.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) @@ -239,7 +239,7 @@ PLATFORMS DEPENDENCIES cocoapods (= 1.9.1) - fastlane (= 2.145.0) + fastlane (= 2.146.1) fastlane-plugin-firebase_app_distribution RUBY VERSION diff --git a/Podfile b/Podfile index e1ecb8ff34..d3ea6e574d 100644 --- a/Podfile +++ b/Podfile @@ -8,7 +8,7 @@ def shared_pods pod 'Alamofire', '5.1.0' pod 'Atributika', '4.9.5' pod 'SwiftyJSON', '5.0.0' - pod 'SDWebImage', '5.7.1' + pod 'SDWebImage', '5.7.3' pod 'SVGKit', :git => 'https://github.com/SVGKit/SVGKit.git', :branch => '2.x' pod 'Fabric', '1.10.2' pod 'Crashlytics', '3.14.0' @@ -29,10 +29,10 @@ def all_pods pod 'SnapKit', '5.0.1' # Firebase - pod 'Firebase/Core', '6.22.0' - pod 'Firebase/Messaging' , '6.22.0' - pod 'Firebase/Analytics' , '6.22.0' - pod 'Firebase/RemoteConfig', '6.22.0' + pod 'Firebase/Core', '6.23.0' + pod 'Firebase/Messaging' , '6.23.0' + pod 'Firebase/Analytics' , '6.23.0' + pod 'Firebase/RemoteConfig', '6.23.0' pod 'YandexMobileMetrica/Dynamic', '3.9.4' pod 'Amplitude-iOS', '4.9.3' @@ -53,7 +53,7 @@ def all_pods pod 'Presentr', '1.9' - pod 'Agrume', '5.6.7' + pod 'Agrume', '5.6.8' pod 'Highlightr', '2.1.0' pod 'TTTAttributedLabel', '2.0.0' pod 'lottie-ios', '2.5.3' @@ -63,7 +63,7 @@ def all_pods pod 'ActionSheetPicker-3.0', '2.4.0' pod 'Nuke', '8.4.1' pod 'STRegex', '2.1.1' - pod 'Tabman', '2.8.0' + pod 'Tabman', '2.8.1' pod 'SwiftDate', '6.1.0' end diff --git a/Podfile.lock b/Podfile.lock index 8777f00f56..418dc13f7d 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,6 +1,6 @@ PODS: - ActionSheetPicker-3.0 (2.4.0) - - Agrume (5.6.7): + - Agrume (5.6.8): - SwiftyGif - Alamofire (5.1.0) - Amplitude-iOS (4.9.3) @@ -32,55 +32,56 @@ PODS: - FBSDKLoginKit/Login (= 5.13.0) - FBSDKLoginKit/Login (5.13.0): - FBSDKCoreKit (~> 5.0) - - Firebase/Analytics (6.22.0): + - Firebase/Analytics (6.23.0): - Firebase/Core - - Firebase/Core (6.22.0): + - Firebase/Core (6.23.0): - Firebase/CoreOnly - - FirebaseAnalytics (= 6.4.1) - - Firebase/CoreOnly (6.22.0): - - FirebaseCore (= 6.6.6) - - Firebase/Messaging (6.22.0): + - FirebaseAnalytics (= 6.4.2) + - Firebase/CoreOnly (6.23.0): + - FirebaseCore (= 6.6.7) + - Firebase/Messaging (6.23.0): - Firebase/CoreOnly - - FirebaseMessaging (~> 4.3.0) - - Firebase/RemoteConfig (6.22.0): + - FirebaseMessaging (~> 4.3.1) + - Firebase/RemoteConfig (6.23.0): - Firebase/CoreOnly - FirebaseRemoteConfig (~> 4.4.9) - FirebaseABTesting (3.2.0): - FirebaseAnalyticsInterop (~> 1.3) - FirebaseCore (~> 6.1) - Protobuf (>= 3.9.2, ~> 3.9) - - FirebaseAnalytics (6.4.1): + - FirebaseAnalytics (6.4.2): - FirebaseCore (~> 6.6) - - FirebaseInstallations (~> 1.1) - - GoogleAppMeasurement (= 6.4.1) + - FirebaseInstallations (~> 1.2) + - GoogleAppMeasurement (= 6.4.2) - GoogleUtilities/AppDelegateSwizzler (~> 6.0) - GoogleUtilities/MethodSwizzler (~> 6.0) - GoogleUtilities/Network (~> 6.0) - "GoogleUtilities/NSData+zlib (~> 6.0)" - nanopb (= 0.3.9011) - FirebaseAnalyticsInterop (1.5.0) - - FirebaseCore (6.6.6): + - FirebaseCore (6.6.7): - FirebaseCoreDiagnostics (~> 1.2) - FirebaseCoreDiagnosticsInterop (~> 1.2) - GoogleUtilities/Environment (~> 6.5) - GoogleUtilities/Logger (~> 6.5) - - FirebaseCoreDiagnostics (1.2.3): + - FirebaseCoreDiagnostics (1.2.4): - FirebaseCoreDiagnosticsInterop (~> 1.2) - - GoogleDataTransportCCTSupport (~> 2.0) + - GoogleDataTransportCCTSupport (~> 3.0) - GoogleUtilities/Environment (~> 6.5) - GoogleUtilities/Logger (~> 6.5) - nanopb (~> 0.3.901) - FirebaseCoreDiagnosticsInterop (1.2.0) - - FirebaseInstallations (1.1.1): + - FirebaseInstallations (1.2.0): - FirebaseCore (~> 6.6) - - GoogleUtilities/UserDefaults (~> 6.5) + - GoogleUtilities/Environment (~> 6.6) + - GoogleUtilities/UserDefaults (~> 6.6) - PromisesObjC (~> 1.2) - - FirebaseInstanceID (4.3.3): + - FirebaseInstanceID (4.3.4): - FirebaseCore (~> 6.6) - FirebaseInstallations (~> 1.0) - GoogleUtilities/Environment (~> 6.5) - GoogleUtilities/UserDefaults (~> 6.5) - - FirebaseMessaging (4.3.0): + - FirebaseMessaging (4.3.1): - FirebaseAnalyticsInterop (~> 1.5) - FirebaseCore (~> 6.6) - FirebaseInstanceID (~> 4.3) @@ -97,33 +98,34 @@ PODS: - GoogleUtilities/Environment (~> 6.2) - "GoogleUtilities/NSData+zlib (~> 6.2)" - Protobuf (>= 3.9.2, ~> 3.9) - - GoogleAppMeasurement (6.4.1): + - GoogleAppMeasurement (6.4.2): - GoogleUtilities/AppDelegateSwizzler (~> 6.0) - GoogleUtilities/MethodSwizzler (~> 6.0) - GoogleUtilities/Network (~> 6.0) - "GoogleUtilities/NSData+zlib (~> 6.0)" - nanopb (= 0.3.9011) - - GoogleDataTransport (5.1.1) - - GoogleDataTransportCCTSupport (2.0.2): - - GoogleDataTransport (~> 5.1) + - GoogleDataTransport (6.0.0) + - GoogleDataTransportCCTSupport (3.0.0): + - GoogleDataTransport (~> 6.0) - nanopb (~> 0.3.901) - - GoogleUtilities/AppDelegateSwizzler (6.5.2): + - GoogleUtilities/AppDelegateSwizzler (6.6.0): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - - GoogleUtilities/Environment (6.5.2) - - GoogleUtilities/Logger (6.5.2): + - GoogleUtilities/Environment (6.6.0): + - PromisesObjC (~> 1.2) + - GoogleUtilities/Logger (6.6.0): - GoogleUtilities/Environment - - GoogleUtilities/MethodSwizzler (6.5.2): + - GoogleUtilities/MethodSwizzler (6.6.0): - GoogleUtilities/Logger - - GoogleUtilities/Network (6.5.2): + - GoogleUtilities/Network (6.6.0): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (6.5.2)" - - GoogleUtilities/Reachability (6.5.2): + - "GoogleUtilities/NSData+zlib (6.6.0)" + - GoogleUtilities/Reachability (6.6.0): - GoogleUtilities/Logger - - GoogleUtilities/UserDefaults (6.5.2): + - GoogleUtilities/UserDefaults (6.6.0): - GoogleUtilities/Logger - HexColors (2.3.0) - Highlightr (2.1.0) @@ -162,9 +164,9 @@ PODS: - Protobuf (3.11.4) - Quick (2.2.0) - Reveal-SDK (24) - - SDWebImage (5.7.1): - - SDWebImage/Core (= 5.7.1) - - SDWebImage/Core (5.7.1) + - SDWebImage (5.7.3): + - SDWebImage/Core (= 5.7.3) + - SDWebImage/Core (5.7.3) - SnapKit (5.0.1) - STRegex (2.1.1) - SVGKit (2.1.0): @@ -174,8 +176,8 @@ PODS: - SwiftLint (0.39.2) - SwiftyGif (5.2.0) - SwiftyJSON (5.0.0) - - Tabman (2.8.0): - - Pageboy (~> 3.5.0) + - Tabman (2.8.1): + - Pageboy (~> 3.5.1) - TSMessages (0.9.13): - HexColors (~> 2.3.0) - TTTAttributedLabel (2.0.0) @@ -191,7 +193,7 @@ PODS: DEPENDENCIES: - ActionSheetPicker-3.0 (= 2.4.0) - - Agrume (= 5.6.7) + - Agrume (= 5.6.8) - Alamofire (= 5.1.0) - Amplitude-iOS (= 4.9.3) - Atributika (= 4.9.5) @@ -206,10 +208,10 @@ DEPENDENCIES: - Fabric (= 1.10.2) - FBSDKCoreKit (= 5.13.0) - FBSDKLoginKit (= 5.13.0) - - Firebase/Analytics (= 6.22.0) - - Firebase/Core (= 6.22.0) - - Firebase/Messaging (= 6.22.0) - - Firebase/RemoteConfig (= 6.22.0) + - Firebase/Analytics (= 6.23.0) + - Firebase/Core (= 6.23.0) + - Firebase/Messaging (= 6.23.0) + - Firebase/RemoteConfig (= 6.23.0) - Highlightr (= 2.1.0) - IQKeyboardManagerSwift (= 6.5.4) - Kanna (= 5.2.2) @@ -222,7 +224,7 @@ DEPENDENCIES: - PromiseKit (= 6.13.0) - Quick (= 2.2.0) - Reveal-SDK - - SDWebImage (= 5.7.1) + - SDWebImage (= 5.7.3) - SnapKit (= 5.0.1) - STRegex (= 2.1.1) - SVGKit (from `https://github.com/SVGKit/SVGKit.git`, branch `2.x`) @@ -230,7 +232,7 @@ DEPENDENCIES: - SwiftDate (= 6.1.0) - SwiftLint (= 0.39.2) - SwiftyJSON (= 5.0.0) - - Tabman (= 2.8.0) + - Tabman (= 2.8.1) - TSMessages (from `https://github.com/KrauseFx/TSMessages.git`) - TTTAttributedLabel (= 2.0.0) - TUSafariActivity (= 1.0.4) @@ -321,7 +323,7 @@ CHECKOUT OPTIONS: SPEC CHECKSUMS: ActionSheetPicker-3.0: c68e1f8355828b4e1c823fa87185aacfef5e6fe3 - Agrume: 1440351654f1d8669f240b5e7f1c76bafa15cd02 + Agrume: 53c2be9b06caef4b4996a2886a56b9e901bcc6c0 Alamofire: 9d5c5f602928e512395b30950c5984eca840093c Amplitude-iOS: 122e026c44db8460e5efcf84859aa290a0ae9786 Atributika: 19429d34013b6d860feb5427c67bdbd219414e1f @@ -337,21 +339,21 @@ SPEC CHECKSUMS: Fabric: 706c8b8098fff96c33c0db69cbf81f9c551d0d74 FBSDKCoreKit: b1645dba3dfdba6102f2d4026fdbfca8114dc229 FBSDKLoginKit: 4aca55e93a63b1ff0cb9446208d49e32c13ab24a - Firebase: 32f9520684e87c7af3f0704f7f88042626d6b536 + Firebase: 585ae467b3edda6a5444e788fda6888f024d8d6f FirebaseABTesting: 821a3a3e4a145987e80fee3657c4de1cb9adf693 - FirebaseAnalytics: 83f822fd0d33a46f49f89b8c3ab16ab4d89df08a + FirebaseAnalytics: 558f7a03d19de451093032c806f39d5f9dff096e FirebaseAnalyticsInterop: 3f86269c38ae41f47afeb43ebf32a001f58fcdae - FirebaseCore: 9aca0f1fffb405176ba15311a5621fcde4106fcf - FirebaseCoreDiagnostics: 13a6564cd6d5375066bbc8940cc1753af24497f3 + FirebaseCore: a2788a0d5f6c1dff17b8f79b4a73654a8d4bfdbd + FirebaseCoreDiagnostics: b59c024493a409f8aecba02c99928d0d8431d159 FirebaseCoreDiagnosticsInterop: 296e2c5f5314500a850ad0b83e9e7c10b011a850 - FirebaseInstallations: acb3216eb9784d3b1d2d2d635ff74fa892cc0c44 - FirebaseInstanceID: 4b87a9a75ef628a53bb5de46e4d4cecfcce88c02 - FirebaseMessaging: 4ec33842d36b3319e062e51fb8b35a74f726950d + FirebaseInstallations: 2119fb3e46b0a88bfdbf12562f855ee3252462fa + FirebaseInstanceID: cef67c4967c7cecb56ea65d8acbb4834825c587b + FirebaseMessaging: 828e66eb357a893e3cebd9ee0f6bc1941447cc94 FirebaseRemoteConfig: 47abf7a04a9082091955ea555aa79cfdd249b19c - GoogleAppMeasurement: e49be3954045b17d046f271b9cc1ec052bad9702 - GoogleDataTransport: 6ffa4dd0b6d547f8d27b91bd92fa9e197a3f5f1f - GoogleDataTransportCCTSupport: 12f02e5c8f09c055615de90bcd5ba2c375546051 - GoogleUtilities: ad0f3b691c67909d03a3327cc205222ab8f42e0e + GoogleAppMeasurement: 2253e99c1f22638cf234c059144660c338ad76c3 + GoogleDataTransport: 061fe7d9b476710e3cd8ea51e8e07d8b67c2b420 + GoogleDataTransportCCTSupport: 0f39025e8cf51f168711bd3fb773938d7e62ddb5 + GoogleUtilities: 39530bc0ad980530298e9c4af8549e991fd033b1 HexColors: 6ad3947c3447a055a3aa8efa859def096351fe5f Highlightr: 595f3e100737c8de41113385da8bd0b5b65212c6 IQKeyboardManagerSwift: 2dde0fc70110e8eac7ccce2a46fdbec6a850b414 @@ -370,7 +372,7 @@ SPEC CHECKSUMS: Protobuf: 176220c526ad8bd09ab1fb40a978eac3fef665f7 Quick: 7fb19e13be07b5dfb3b90d4f9824c855a11af40e Reveal-SDK: 5d7e56b8f018c0a88b3a2c10bf68d598bbd3b071 - SDWebImage: 5d4cf6afccd9ac1d2ee701ed0c4917cbb1b80536 + SDWebImage: 97351f6582ceca541ea294ba66a1fcb342a331c2 SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb STRegex: d49e88d0fe58538d3175fdd989bc1243b9be2a07 SVGKit: 8a2fc74258bdb2abb54d3b65f3dd68b0277a9c4d @@ -379,7 +381,7 @@ SPEC CHECKSUMS: SwiftLint: 22ccbbe3b8008684be5955693bab135e0ed6a447 SwiftyGif: b85c6b33a9411859d9e1db998b6a8214aea942df SwiftyJSON: 36413e04c44ee145039d332b4f4e2d3e8d6c4db7 - Tabman: f62ad94ee54a7d96e3fbab34f677d9ea4d38ece6 + Tabman: b1d349045017c25d558aef052c2d7eeac1273c0c TSMessages: eb3cf27b6900684a21bad4fe9ea426e287b8c839 TTTAttributedLabel: 8cffe8e127e4e82ff3af1e5386d4cd0ad000b656 TUSafariActivity: afc55a00965377939107ce4fdc7f951f62454546 @@ -387,6 +389,6 @@ SPEC CHECKSUMS: VK-ios-sdk: 62a10b6571fbcda0657f455fedce7fedf55b4cd0 YandexMobileMetrica: 1030df2959c886a7be3bc3d274eb54c8e86afd6f -PODFILE CHECKSUM: d9a987678262cc1b050569e6115b50fbf9457879 +PODFILE CHECKSUM: 102d17c414cc3f0207677129a616c4df7481f7e3 COCOAPODS: 1.9.1 diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 5e05ca9ca3..953fa3519b 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -1,5 +1,5 @@ # This is the minimum version number required. -fastlane_version "2.145.0" +fastlane_version "2.146.1" default_platform :ios From 4b43b14dc01a47ceb5bff49ea7b2321836414bc4 Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Sun, 26 Apr 2020 23:54:08 +0300 Subject: [PATCH 02/11] Course info user courses actions (#702) * Parse is_archived * Persist isFavorite & isArchived * Add user-courses network service * Perform user course actions * Notify about changes * Fix present actions alert on unenrolled course --- Stepic.xcodeproj/project.pbxproj | 8 +- .../Course/Course+CoreDataProperties.swift | 20 + .../Legacy/Model/Entities/Course/Course.swift | 4 + .../Model.xcdatamodeld/.xccurrentversion | 2 +- .../contents | 354 ++++++++++++++++++ .../Network/Endpoints/UserCoursesAPI.swift | 27 ++ Stepic/Legacy/Model/UserCourse.swift | 47 ++- .../CourseInfo/CourseInfoAssembly.swift | 14 +- .../CourseInfo/CourseInfoDataFlow.swift | 21 ++ .../CourseInfoHeaderViewModel.swift | 2 + .../CourseInfo/CourseInfoInteractor.swift | 49 ++- .../CourseInfo/CourseInfoPresenter.swift | 19 +- .../CourseInfo/CourseInfoProvider.swift | 29 +- .../CourseInfo/CourseInfoViewController.swift | 43 ++- .../Provider/CourseListNetworkService.swift | 4 +- .../ContinueCourseProvider.swift | 2 +- .../Services/DataBackUpdateService.swift | 16 + .../Network/UserCoursesNetworkService.swift | 58 +++ Stepic/en.lproj/Localizable.strings | 4 + Stepic/ru.lproj/Localizable.strings | 4 + 20 files changed, 693 insertions(+), 34 deletions(-) create mode 100644 Stepic/Legacy/Model/Model.xcdatamodeld/Model_user_courses_actions_v51.xcdatamodel/contents create mode 100644 Stepic/Sources/Services/Models/Network/UserCoursesNetworkService.swift diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index 0b8e510cbe..1808877cc9 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -491,6 +491,7 @@ 2C6E9CDB1FF27543001821A2 /* AdaptiveStorageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6E9CDA1FF27543001821A2 /* AdaptiveStorageManager.swift */; }; 2C7024BA23D00762002A0246 /* SettingsRightDetailSwitchTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7024B923D00762002A0246 /* SettingsRightDetailSwitchTableViewCell.swift */; }; 2C7024BC23D00799002A0246 /* SettingsRightDetailSwitchCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7024BB23D00799002A0246 /* SettingsRightDetailSwitchCellView.swift */; }; + 2C746642245613E900BB0800 /* UserCoursesNetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C746641245613E900BB0800 /* UserCoursesNetworkService.swift */; }; 2C79F61821873CD9004CC082 /* NotificationsRequestOnlySettingsAlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C79F61721873CD9004CC082 /* NotificationsRequestOnlySettingsAlertPresenter.swift */; }; 2C7F641C23B0F5B7006C7648 /* PlayNextCircleControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7F641B23B0F5B7006C7648 /* PlayNextCircleControlView.swift */; }; 2C7F641E23B12208006C7648 /* AutoplayStorageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7F641D23B12208006C7648 /* AutoplayStorageManager.swift */; }; @@ -1730,6 +1731,8 @@ 2C6E9CDA1FF27543001821A2 /* AdaptiveStorageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveStorageManager.swift; sourceTree = ""; }; 2C7024B923D00762002A0246 /* SettingsRightDetailSwitchTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsRightDetailSwitchTableViewCell.swift; sourceTree = ""; }; 2C7024BB23D00799002A0246 /* SettingsRightDetailSwitchCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsRightDetailSwitchCellView.swift; sourceTree = ""; }; + 2C7466402456065F00BB0800 /* Model_user_courses_actions_v51.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_user_courses_actions_v51.xcdatamodel; sourceTree = ""; }; + 2C746641245613E900BB0800 /* UserCoursesNetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCoursesNetworkService.swift; sourceTree = ""; }; 2C79F61721873CD9004CC082 /* NotificationsRequestOnlySettingsAlertPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsRequestOnlySettingsAlertPresenter.swift; sourceTree = ""; }; 2C7F641B23B0F5B7006C7648 /* PlayNextCircleControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayNextCircleControlView.swift; sourceTree = ""; }; 2C7F641D23B12208006C7648 /* AutoplayStorageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoplayStorageManager.swift; sourceTree = ""; }; @@ -5305,6 +5308,7 @@ 62E98E41865820B1B8F7357D /* UnitsNetworkService.swift */, 2C16495822C10DD300DF18CA /* UserActivitiesNetworkService.swift */, 2CB1C3B024009339001DA83E /* UserCodeRunsNetworkService.swift */, + 2C746641245613E900BB0800 /* UserCoursesNetworkService.swift */, 62E98407592112A22E967DE1 /* UsersNetworkService.swift */, 2CB9529B229F29F000A6117A /* ViewsNetworkService.swift */, 2C85C6A422D38A3800FDBAFE /* VotesNetworkService.swift */, @@ -7678,6 +7682,7 @@ 62E9889E935597A0ED849B0C /* ContentLanguageSwitchViewController.swift in Sources */, 62E982DE03DC1E9967B8B43B /* ContentLanguageSwitchViewModel.swift in Sources */, 62E98561DFB8CBB54B0CA2F7 /* ContentLanguageSwitchButton.swift in Sources */, + 2C746642245613E900BB0800 /* UserCoursesNetworkService.swift in Sources */, 62E98C25078CB0ECB000FB71 /* ContentLanguageSwitchView.swift in Sources */, 62E98B25E6C3E23A223DC564 /* ContinueCourseAssembly.swift in Sources */, 62E98E830A3AC918F1CD339F /* ContinueCourseDataFlow.swift in Sources */, @@ -8360,6 +8365,7 @@ 08D1EF6E1BB5618700BE84E6 /* Model.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + 2C7466402456065F00BB0800 /* Model_user_courses_actions_v51.xcdatamodel */, 2C87A7A52446635E00933CA4 /* Model_user_activity_v50.xcdatamodel */, 2C04BA3724058AF100D74D4B /* Model_attempts_caching.xcdatamodel */, 2CB1C3B224009ACB001DA83E /* Model_ is_run_user_code_allowed.xcdatamodel */, @@ -8412,7 +8418,7 @@ 0802AC531C7222B200C4F3E6 /* Model_v2.xcdatamodel */, 08D1EF6F1BB5618700BE84E6 /* Model.xcdatamodel */, ); - currentVersion = 2C87A7A52446635E00933CA4 /* Model_user_activity_v50.xcdatamodel */; + currentVersion = 2C7466402456065F00BB0800 /* Model_user_courses_actions_v51.xcdatamodel */; path = Model.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/Stepic/Legacy/Model/Entities/Course/Course+CoreDataProperties.swift b/Stepic/Legacy/Model/Entities/Course/Course+CoreDataProperties.swift index 106ca8ccd4..91b4c0cde0 100644 --- a/Stepic/Legacy/Model/Entities/Course/Course+CoreDataProperties.swift +++ b/Stepic/Legacy/Model/Entities/Course/Course+CoreDataProperties.swift @@ -22,6 +22,8 @@ extension Course { @NSManaged var managedEnrolled: NSNumber? @NSManaged var managedFeatured: NSNumber? @NSManaged var managedPublic: NSNumber? + @NSManaged var managedIsFavorite: NSNumber? + @NSManaged var managedIsArchived: NSNumber? @NSManaged var managedLearnersCount: NSNumber? @NSManaged var managedReadiness: NSNumber? @@ -208,6 +210,24 @@ extension Course { } } + var isFavorite: Bool { + get { + self.managedIsFavorite?.boolValue ?? false + } + set { + self.managedIsFavorite = NSNumber(value: newValue) + } + } + + var isArchived: Bool { + get { + self.managedIsArchived?.boolValue ?? false + } + set { + self.managedIsArchived = NSNumber(value: newValue) + } + } + var isPublic: Bool { set(isPublic) { self.managedPublic = isPublic as NSNumber? diff --git a/Stepic/Legacy/Model/Entities/Course/Course.swift b/Stepic/Legacy/Model/Entities/Course/Course.swift index 9322214eff..9911c4f565 100644 --- a/Stepic/Legacy/Model/Entities/Course/Course.swift +++ b/Stepic/Legacy/Model/Entities/Course/Course.swift @@ -96,6 +96,8 @@ final class Course: NSManagedObject, IDFetchable { self.enrolled = json[JSONKey.enrollment.rawValue].int != nil self.featured = json[JSONKey.isFeatured.rawValue].boolValue self.isPublic = json[JSONKey.isPublic.rawValue].boolValue + self.isFavorite = json[JSONKey.isFavorite.rawValue].boolValue + self.isArchived = json[JSONKey.isArchived.rawValue].boolValue self.readiness = json[JSONKey.readiness.rawValue].float self.summary = json[JSONKey.summary.rawValue].stringValue @@ -405,6 +407,8 @@ final class Course: NSManagedObject, IDFetchable { case enrollment case isFeatured = "is_featured" case isPublic = "is_public" + case isFavorite = "is_favorite" + case isArchived = "is_archived" case readiness case summary case workload diff --git a/Stepic/Legacy/Model/Model.xcdatamodeld/.xccurrentversion b/Stepic/Legacy/Model/Model.xcdatamodeld/.xccurrentversion index 5ebe992446..f7943d6d03 100644 --- a/Stepic/Legacy/Model/Model.xcdatamodeld/.xccurrentversion +++ b/Stepic/Legacy/Model/Model.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - Model_user_activity_v50.xcdatamodel + Model_user_courses_actions_v51.xcdatamodel diff --git a/Stepic/Legacy/Model/Model.xcdatamodeld/Model_user_courses_actions_v51.xcdatamodel/contents b/Stepic/Legacy/Model/Model.xcdatamodeld/Model_user_courses_actions_v51.xcdatamodel/contents new file mode 100644 index 0000000000..4f9a575b1f --- /dev/null +++ b/Stepic/Legacy/Model/Model.xcdatamodeld/Model_user_courses_actions_v51.xcdatamodel/contents @@ -0,0 +1,354 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Stepic/Legacy/Model/Network/Endpoints/UserCoursesAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/UserCoursesAPI.swift index 87cdf4de90..b698dd6055 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/UserCoursesAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/UserCoursesAPI.swift @@ -31,4 +31,31 @@ final class UserCoursesAPI: APIEndpoint { } } } + + func retrieve(courseID: Course.IdType) -> Promise<([UserCourse], Meta)> { + Promise { seal in + var params = Parameters() + params["course"] = courseID + + self.retrieve.request( + requestEndpoint: self.name, + paramName: self.name, + params: params, + withManager: self.manager + ).done { userCourses, meta, _ in + seal.fulfill((userCourses, meta)) + }.catch { error in + seal.reject(error) + } + } + } + + func update(_ userCourse: UserCourse) -> Promise { + self.update.request( + requestEndpoint: self.name, + paramName: "userCourse", + updatingObject: userCourse, + withManager: self.manager + ) + } } diff --git a/Stepic/Legacy/Model/UserCourse.swift b/Stepic/Legacy/Model/UserCourse.swift index 603f46ad25..e34e22a5fa 100644 --- a/Stepic/Legacy/Model/UserCourse.swift +++ b/Stepic/Legacy/Model/UserCourse.swift @@ -10,25 +10,40 @@ import Foundation import SwiftyJSON final class UserCourse: JSONSerializable { - var id: Int - var userId: Int - var courseId: Int - var isFavorite: Bool - var lastViewed: Date + var id: Int = 0 + var userID: Int = 0 + var courseID: Int = 0 + var isFavorite: Bool = false + var isArchived: Bool = false + var lastViewed: Date = Date() - func update(json: JSON) { - self.id = json["id"].intValue - self.userId = json["user"].intValue - self.courseId = json["course"].intValue - self.isFavorite = json["is_favorite"].boolValue - self.lastViewed = Parser.shared.dateFromTimedateJSON(json["last_viewed"]) ?? Date() + var json: JSON { + [ + JSONKey.isFavorite.rawValue: self.isFavorite, + JSONKey.isArchived.rawValue: self.isArchived, + JSONKey.course.rawValue: self.courseID + ] } required init(json: JSON) { - self.id = json["id"].intValue - self.userId = json["user"].intValue - self.courseId = json["course"].intValue - self.isFavorite = json["is_favorite"].boolValue - self.lastViewed = Parser.shared.dateFromTimedateJSON(json["last_viewed"]) ?? Date() + self.update(json: json) + } + + func update(json: JSON) { + self.id = json[JSONKey.id.rawValue].intValue + self.userID = json[JSONKey.user.rawValue].intValue + self.courseID = json[JSONKey.course.rawValue].intValue + self.isFavorite = json[JSONKey.isFavorite.rawValue].boolValue + self.isArchived = json[JSONKey.isArchived.rawValue].boolValue + self.lastViewed = Parser.shared.dateFromTimedateJSON(json[JSONKey.lastViewed.rawValue]) ?? Date() + } + + enum JSONKey: String { + case id + case user + case course + case isFavorite = "is_favorite" + case isArchived = "is_archived" + case lastViewed = "last_viewed" } } diff --git a/Stepic/Sources/Modules/CourseInfo/CourseInfoAssembly.swift b/Stepic/Sources/Modules/CourseInfo/CourseInfoAssembly.swift index 932c2be310..14f4c6e8be 100644 --- a/Stepic/Sources/Modules/CourseInfo/CourseInfoAssembly.swift +++ b/Stepic/Sources/Modules/CourseInfo/CourseInfoAssembly.swift @@ -25,7 +25,8 @@ final class CourseInfoAssembly: Assembly { reviewSummariesPersistenceService: CourseReviewSummariesPersistenceService(), reviewSummariesNetworkService: CourseReviewSummariesNetworkService( courseReviewSummariesAPI: CourseReviewSummariesAPI() - ) + ), + userCoursesNetworkService: UserCoursesNetworkService(userCoursesAPI: UserCoursesAPI()) ) let presenter = CourseInfoPresenter() @@ -33,6 +34,14 @@ final class CourseInfoAssembly: Assembly { presenter: NotificationsRequestAlertPresenter(context: .courseSubscription), analytics: .init(source: .courseSubscription) ) + + let dataBackUpdateService = DataBackUpdateService( + unitsNetworkService: UnitsNetworkService(unitsAPI: UnitsAPI()), + sectionsNetworkService: SectionsNetworkService(sectionsAPI: SectionsAPI()), + coursesNetworkService: CoursesNetworkService(coursesAPI: CoursesAPI()), + progressesNetworkService: ProgressesNetworkService(progressesAPI: ProgressesAPI()) + ) + let interactor = CourseInfoInteractor( courseID: self.courseID, presenter: presenter, @@ -43,7 +52,8 @@ final class CourseInfoAssembly: Assembly { adaptiveStorageManager: AdaptiveStorageManager(), notificationSuggestionManager: NotificationSuggestionManager(), notificationsRegistrationService: notificationsRegistrationService, - spotlightIndexingService: SpotlightIndexingService.shared + spotlightIndexingService: SpotlightIndexingService.shared, + dataBackUpdateService: dataBackUpdateService ) notificationsRegistrationService.delegate = interactor diff --git a/Stepic/Sources/Modules/CourseInfo/CourseInfoDataFlow.swift b/Stepic/Sources/Modules/CourseInfo/CourseInfoDataFlow.swift index 244479fb6b..836f8eb912 100644 --- a/Stepic/Sources/Modules/CourseInfo/CourseInfoDataFlow.swift +++ b/Stepic/Sources/Modules/CourseInfo/CourseInfoDataFlow.swift @@ -130,11 +130,32 @@ enum CourseInfo { } } + /// Handle HUD status update + enum BlockingWaitingIndicatorStatusUpdate { + struct Response { + let isSuccessful: Bool + } + + struct ViewModel { + let isSuccessful: Bool + } + } + /// Drop course enum CourseUnenrollmentAction { struct Request {} } + /// Add/remove course to/from favorites + enum CourseFavoriteAction { + struct Request {} + } + + /// Move/remove course to/from archived + enum CourseArchiveAction { + struct Request {} + } + /// Do main action (continue, enroll, etc) enum MainCourseAction { struct Request {} diff --git a/Stepic/Sources/Modules/CourseInfo/CourseInfoHeaderViewModel.swift b/Stepic/Sources/Modules/CourseInfo/CourseInfoHeaderViewModel.swift index 21579edf94..3a4594cfb6 100644 --- a/Stepic/Sources/Modules/CourseInfo/CourseInfoHeaderViewModel.swift +++ b/Stepic/Sources/Modules/CourseInfo/CourseInfoHeaderViewModel.swift @@ -14,6 +14,8 @@ struct CourseInfoHeaderViewModel { let progress: CourseInfoProgressViewModel? let isVerified: Bool let isEnrolled: Bool + let isFavorite: Bool + let isArchived: Bool let buttonDescription: ButtonDescription struct ButtonDescription { diff --git a/Stepic/Sources/Modules/CourseInfo/CourseInfoInteractor.swift b/Stepic/Sources/Modules/CourseInfo/CourseInfoInteractor.swift index 84773d7762..17d00eba7f 100644 --- a/Stepic/Sources/Modules/CourseInfo/CourseInfoInteractor.swift +++ b/Stepic/Sources/Modules/CourseInfo/CourseInfoInteractor.swift @@ -5,6 +5,8 @@ protocol CourseInfoInteractorProtocol { func doCourseRefresh(request: CourseInfo.CourseLoad.Request) func doCourseShareAction(request: CourseInfo.CourseShareAction.Request) func doCourseUnenrollmentAction(request: CourseInfo.CourseUnenrollmentAction.Request) + func doCourseFavoriteAction(request: CourseInfo.CourseFavoriteAction.Request) + func doCourseArchiveAction(request: CourseInfo.CourseArchiveAction.Request) func doMainCourseAction(request: CourseInfo.MainCourseAction.Request) func doOnlineModeReset(request: CourseInfo.OnlineModeReset.Request) func doRegistrationForRemoteNotifications(request: CourseInfo.RemoteNotificationsRegistration.Request) @@ -23,6 +25,8 @@ final class CourseInfoInteractor: CourseInfoInteractorProtocol { private let notificationsRegistrationService: NotificationsRegistrationServiceProtocol private let spotlightIndexingService: SpotlightIndexingServiceProtocol + private let dataBackUpdateService: DataBackUpdateServiceProtocol + private let courseID: Course.IdType private var currentCourse: Course? { didSet { @@ -76,7 +80,8 @@ final class CourseInfoInteractor: CourseInfoInteractorProtocol { adaptiveStorageManager: AdaptiveStorageManagerProtocol, notificationSuggestionManager: NotificationSuggestionManager, notificationsRegistrationService: NotificationsRegistrationServiceProtocol, - spotlightIndexingService: SpotlightIndexingServiceProtocol + spotlightIndexingService: SpotlightIndexingServiceProtocol, + dataBackUpdateService: DataBackUpdateServiceProtocol ) { self.presenter = presenter self.provider = provider @@ -87,6 +92,7 @@ final class CourseInfoInteractor: CourseInfoInteractorProtocol { self.notificationSuggestionManager = notificationSuggestionManager self.notificationsRegistrationService = notificationsRegistrationService self.spotlightIndexingService = spotlightIndexingService + self.dataBackUpdateService = dataBackUpdateService self.courseID = courseID } @@ -160,6 +166,14 @@ final class CourseInfoInteractor: CourseInfoInteractorProtocol { } } + func doCourseFavoriteAction(request: CourseInfo.CourseFavoriteAction.Request) { + self.doUserCourseUpdateAction { $0.isFavorite.toggle() } + } + + func doCourseArchiveAction(request: CourseInfo.CourseArchiveAction.Request) { + self.doUserCourseUpdateAction { $0.isArchived.toggle() } + } + func doMainCourseAction(request: CourseInfo.MainCourseAction.Request) { guard let course = self.currentCourse else { return @@ -262,6 +276,39 @@ final class CourseInfoInteractor: CourseInfoInteractorProtocol { } } + private func doUserCourseUpdateAction(_ updateBlock: @escaping (UserCourse) -> Void) { + guard let course = self.currentCourse, course.enrolled else { + return + } + + self.presenter.presentWaitingState(response: .init(shouldDismiss: false)) + + firstly { + self.provider + .fetchUserCourse(courseID: course.id) + .compactMap { $0 } + }.then { userCourse -> Promise in + updateBlock(userCourse) + return self.provider.updateUserCourse(userCourse: userCourse) + }.done { userCourse in + if let course = self.currentCourse { + if course.isFavorite != userCourse.isFavorite { + course.isFavorite = userCourse.isFavorite + self.dataBackUpdateService.triggerCourseIsFavoriteUpdate(retrievedCourse: course) + } + if course.isArchived != userCourse.isArchived { + course.isArchived = userCourse.isArchived + self.dataBackUpdateService.triggerCourseIsArchivedUpdate(retrievedCourse: course) + } + self.presenter.presentCourse(response: .init(result: .success(course))) + } + self.presenter.presentWaitingStatus(response: .init(isSuccessful: true)) + }.catch { error in + print("course info interactor: user course action error = \(error)") + self.presenter.presentWaitingStatus(response: .init(isSuccessful: false)) + } + } + enum Error: Swift.Error { case cachedFetchFailed case networkFetchFailed diff --git a/Stepic/Sources/Modules/CourseInfo/CourseInfoPresenter.swift b/Stepic/Sources/Modules/CourseInfo/CourseInfoPresenter.swift index dcd222be4d..a95c165691 100644 --- a/Stepic/Sources/Modules/CourseInfo/CourseInfoPresenter.swift +++ b/Stepic/Sources/Modules/CourseInfo/CourseInfoPresenter.swift @@ -8,8 +8,9 @@ protocol CourseInfoPresenterProtocol { func presentCourseSharing(response: CourseInfo.CourseShareAction.Response) func presentLastStep(response: CourseInfo.LastStepPresentation.Response) func presentAuthorization(response: CourseInfo.AuthorizationPresentation.Response) - func presentWaitingState(response: CourseInfo.BlockingWaitingIndicatorUpdate.Response) func presentPaidCourseBuying(response: CourseInfo.PaidCourseBuyingPresentation.Response) + func presentWaitingState(response: CourseInfo.BlockingWaitingIndicatorUpdate.Response) + func presentWaitingStatus(response: CourseInfo.BlockingWaitingIndicatorStatusUpdate.Response) } final class CourseInfoPresenter: CourseInfoPresenterProtocol { @@ -55,10 +56,6 @@ final class CourseInfoPresenter: CourseInfoPresenterProtocol { self.viewController?.displayCourseSharing(viewModel: viewModel) } - func presentWaitingState(response: CourseInfo.BlockingWaitingIndicatorUpdate.Response) { - self.viewController?.displayBlockingLoadingIndicator(viewModel: .init(shouldDismiss: response.shouldDismiss)) - } - func presentLastStep(response: CourseInfo.LastStepPresentation.Response) { self.viewController?.displayLastStep( viewModel: .init( @@ -77,6 +74,16 @@ final class CourseInfoPresenter: CourseInfoPresenterProtocol { self.viewController?.displayPaidCourseBuying(viewModel: .init(urlPath: path)) } + func presentWaitingState(response: CourseInfo.BlockingWaitingIndicatorUpdate.Response) { + self.viewController?.displayBlockingLoadingIndicator(viewModel: .init(shouldDismiss: response.shouldDismiss)) + } + + func presentWaitingStatus(response: CourseInfo.BlockingWaitingIndicatorStatusUpdate.Response) { + self.viewController?.displayBlockingLoadingIndicatorStatus( + viewModel: .init(isSuccessful: response.isSuccessful) + ) + } + private func makeProgressViewModel(progress: Progress) -> CourseInfoProgressViewModel { var normalizedPercent = progress.percentPassed normalizedPercent.round(.up) @@ -112,6 +119,8 @@ final class CourseInfoPresenter: CourseInfoPresenterProtocol { progress: progress, isVerified: (course.readiness ?? 0) > 0.9, isEnrolled: course.enrolled, + isFavorite: course.isFavorite, + isArchived: course.isArchived, buttonDescription: self.makeButtonDescription(course: course) ) } diff --git a/Stepic/Sources/Modules/CourseInfo/CourseInfoProvider.swift b/Stepic/Sources/Modules/CourseInfo/CourseInfoProvider.swift index 3f7baa2adb..ed001ae60b 100644 --- a/Stepic/Sources/Modules/CourseInfo/CourseInfoProvider.swift +++ b/Stepic/Sources/Modules/CourseInfo/CourseInfoProvider.swift @@ -4,6 +4,9 @@ import PromiseKit protocol CourseInfoProviderProtocol { func fetchCached() -> Promise func fetchRemote() -> Promise + + func fetchUserCourse(courseID: Course.IdType) -> Promise + func updateUserCourse(userCourse: UserCourse) -> Promise } final class CourseInfoProvider: CourseInfoProviderProtocol { @@ -18,6 +21,8 @@ final class CourseInfoProvider: CourseInfoProviderProtocol { private let reviewSummariesPersistenceService: CourseReviewSummariesPersistenceServiceProtocol private let reviewSummariesNetworkService: CourseReviewSummariesNetworkServiceProtocol + private let userCoursesNetworkService: UserCoursesNetworkServiceProtocol + init( courseID: Course.IdType, coursesPersistenceService: CoursesPersistenceServiceProtocol, @@ -25,7 +30,8 @@ final class CourseInfoProvider: CourseInfoProviderProtocol { progressesPersistenceService: ProgressesPersistenceServiceProtocol, progressesNetworkService: ProgressesNetworkServiceProtocol, reviewSummariesPersistenceService: CourseReviewSummariesPersistenceServiceProtocol, - reviewSummariesNetworkService: CourseReviewSummariesNetworkServiceProtocol + reviewSummariesNetworkService: CourseReviewSummariesNetworkServiceProtocol, + userCoursesNetworkService: UserCoursesNetworkServiceProtocol ) { self.courseID = courseID self.coursesNetworkService = coursesNetworkService @@ -34,6 +40,7 @@ final class CourseInfoProvider: CourseInfoProviderProtocol { self.progressesPersistenceService = progressesPersistenceService self.reviewSummariesNetworkService = reviewSummariesNetworkService self.reviewSummariesPersistenceService = reviewSummariesPersistenceService + self.userCoursesNetworkService = userCoursesNetworkService } func fetchCached() -> Promise { @@ -64,6 +71,26 @@ final class CourseInfoProvider: CourseInfoProviderProtocol { } } + func fetchUserCourse(courseID: Course.IdType) -> Promise { + Promise { seal in + self.userCoursesNetworkService.fetch(courseID: courseID).done { userCourse in + seal.fulfill(userCourse) + }.catch { _ in + seal.reject(Error.networkFetchFailed) + } + } + } + + func updateUserCourse(userCourse: UserCourse) -> Promise { + Promise { seal in + self.userCoursesNetworkService.update(userCourse: userCourse).done { userCourse in + seal.fulfill(userCourse) + }.catch { _ in + seal.reject(Error.networkFetchFailed) + } + } + } + private func fetchAndMergeCourse( courseFetchMethod: @escaping (Course.IdType) -> Promise, progressFetchMethod: @escaping (Progress.IdType) -> Promise, diff --git a/Stepic/Sources/Modules/CourseInfo/CourseInfoViewController.swift b/Stepic/Sources/Modules/CourseInfo/CourseInfoViewController.swift index 378f428947..e855992426 100644 --- a/Stepic/Sources/Modules/CourseInfo/CourseInfoViewController.swift +++ b/Stepic/Sources/Modules/CourseInfo/CourseInfoViewController.swift @@ -18,8 +18,9 @@ protocol CourseInfoViewControllerProtocol: AnyObject { func displayCourseSharing(viewModel: CourseInfo.CourseShareAction.ViewModel) func displayLastStep(viewModel: CourseInfo.LastStepPresentation.ViewModel) func displayAuthorization(viewModel: CourseInfo.AuthorizationPresentation.ViewModel) - func displayBlockingLoadingIndicator(viewModel: CourseInfo.BlockingWaitingIndicatorUpdate.ViewModel) func displayPaidCourseBuying(viewModel: CourseInfo.PaidCourseBuyingPresentation.ViewModel) + func displayBlockingLoadingIndicator(viewModel: CourseInfo.BlockingWaitingIndicatorUpdate.ViewModel) + func displayBlockingLoadingIndicatorStatus(viewModel: CourseInfo.BlockingWaitingIndicatorStatusUpdate.ViewModel) } final class CourseInfoViewController: UIViewController { @@ -49,7 +50,7 @@ final class CourseInfoViewController: UIViewController { // Element is nil when view controller was not initialized yet private var submodulesControllers: [UIViewController?] = [] - private var shouldShowDropCourseAction = false + private var storedViewModel: CourseInfoHeaderViewModel? private let didJustSubscribe: Bool init( @@ -217,7 +218,33 @@ final class CourseInfoViewController: UIViewController { ) ) - if self.shouldShowDropCourseAction { + if let viewModel = self.storedViewModel, viewModel.isEnrolled { + let favoriteActionTitle = viewModel.isFavorite + ? NSLocalizedString("CourseInfoCourseActionRemoveFromFavoritesAlertTitle", comment: "") + : NSLocalizedString("CourseInfoCourseActionAddToFavoritesAlertTitle", comment: "") + alert.addAction( + UIAlertAction( + title: favoriteActionTitle, + style: .default, + handler: { [weak self] _ in + self?.interactor.doCourseFavoriteAction(request: .init()) + } + ) + ) + + let archivedActionTitle = viewModel.isArchived + ? NSLocalizedString("CourseInfoCourseActionRemoveFromArchivedAlertTitle", comment: "") + : NSLocalizedString("CourseInfoCourseActionMoveToArchivedAlertTitle", comment: "") + alert.addAction( + UIAlertAction( + title: archivedActionTitle, + style: .default, + handler: { [weak self] _ in + self?.interactor.doCourseArchiveAction(request: .init()) + } + ) + ) + alert.addAction( UIAlertAction( title: NSLocalizedString("DropCourse", comment: ""), @@ -416,8 +443,8 @@ extension CourseInfoViewController: CourseInfoViewControllerProtocol { func displayCourse(viewModel: CourseInfo.CourseLoad.ViewModel) { switch viewModel.state { case .result(let data): + self.storedViewModel = data self.courseInfoView?.configure(viewModel: data) - self.shouldShowDropCourseAction = data.isEnrolled case .loading: break } @@ -470,6 +497,14 @@ extension CourseInfoViewController: CourseInfoViewControllerProtocol { } } + func displayBlockingLoadingIndicatorStatus(viewModel: CourseInfo.BlockingWaitingIndicatorStatusUpdate.ViewModel) { + if viewModel.isSuccessful { + SVProgressHUD.showSuccess(withStatus: nil) + } else { + SVProgressHUD.showError(withStatus: nil) + } + } + func displayLastStep(viewModel: CourseInfo.LastStepPresentation.ViewModel) { guard let navigationController = self.navigationController else { return diff --git a/Stepic/Sources/Modules/CourseList/Provider/CourseListNetworkService.swift b/Stepic/Sources/Modules/CourseList/Provider/CourseListNetworkService.swift index ede4f6f0f0..fb3fdd4e2e 100644 --- a/Stepic/Sources/Modules/CourseList/Provider/CourseListNetworkService.swift +++ b/Stepic/Sources/Modules/CourseList/Provider/CourseListNetworkService.swift @@ -41,11 +41,11 @@ final class EnrolledCourseListNetworkService: BaseCourseListNetworkService, Cour } return self.coursesAPI - .retrieve(ids: userCoursesInfo.0.map { $0.courseId }) + .retrieve(ids: userCoursesInfo.0.map { $0.courseID }) .map { ($0, userCoursesInfo.0, userCoursesInfo.1) } }.done { courses, info, meta in let orderedCourses = courses.reordered( - order: info.map { $0.courseId }, + order: info.map { $0.courseID }, transform: { $0.id } ) seal.fulfill((orderedCourses, meta)) diff --git a/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseProvider.swift b/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseProvider.swift index a2354f1dbb..d91e7454fa 100644 --- a/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseProvider.swift +++ b/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseProvider.swift @@ -27,7 +27,7 @@ final class ContinueCourseProvider: ContinueCourseProviderProtocol { .sorted(by: { $0.lastViewed > $1.lastViewed }) .prefix(1) // [] or [id] - let coursesIDs = lastCourse.compactMap { $0.courseId } + let coursesIDs = lastCourse.compactMap { $0.courseID } return self.coursesAPI.retrieve(ids: coursesIDs) }.then { courses -> Promise<(Course?, Progress?)> in if let course = courses.first, diff --git a/Stepic/Sources/Services/DataBackUpdateService.swift b/Stepic/Sources/Services/DataBackUpdateService.swift index 227e205cb8..2de89f886e 100644 --- a/Stepic/Sources/Services/DataBackUpdateService.swift +++ b/Stepic/Sources/Services/DataBackUpdateService.swift @@ -20,6 +20,8 @@ struct DataBackUpdateDescription: OptionSet { static let profileLastName = DataBackUpdateDescription(rawValue: 4) static let profileShortBio = DataBackUpdateDescription(rawValue: 5) static let profileDetails = DataBackUpdateDescription(rawValue: 6) + static let courseIsFavorite = DataBackUpdateDescription(rawValue: 7) + static let courseIsArchived = DataBackUpdateDescription(rawValue: 8) } protocol DataBackUpdateServiceDelegate: AnyObject { @@ -63,6 +65,10 @@ protocol DataBackUpdateServiceProtocol: AnyObject { func triggerEnrollmentUpdate(retrievedCourse: Course) /// Report about profile update func triggerProfileUpdate(updatedProfile: Profile) + /// Report about `isFavorite` state update with already retrieved course + func triggerCourseIsFavoriteUpdate(retrievedCourse: Course) + /// Report about `isArchived` state update with already retrieved course + func triggerCourseIsArchivedUpdate(retrievedCourse: Course) } final class DataBackUpdateService: DataBackUpdateServiceProtocol { @@ -192,6 +198,16 @@ final class DataBackUpdateService: DataBackUpdateServiceProtocol { self.postNotification(target: .profile(updatedProfile)) } + func triggerCourseIsFavoriteUpdate(retrievedCourse: Course) { + self.postNotification(description: [.courseIsFavorite], target: .course(retrievedCourse)) + self.postNotification(target: .course(retrievedCourse)) + } + + func triggerCourseIsArchivedUpdate(retrievedCourse: Course) { + self.postNotification(description: [.courseIsArchived], target: .course(retrievedCourse)) + self.postNotification(target: .course(retrievedCourse)) + } + // MARK: Private methods private func postNotification(description: DataBackUpdateDescription? = nil, target: DataBackUpdateTarget) { diff --git a/Stepic/Sources/Services/Models/Network/UserCoursesNetworkService.swift b/Stepic/Sources/Services/Models/Network/UserCoursesNetworkService.swift new file mode 100644 index 0000000000..95548ee48c --- /dev/null +++ b/Stepic/Sources/Services/Models/Network/UserCoursesNetworkService.swift @@ -0,0 +1,58 @@ +import Foundation +import PromiseKit + +protocol UserCoursesNetworkServiceProtocol: AnyObject { + func fetch(page: Int) -> Promise<([UserCourse], Meta)> + func fetch(courseID: Course.IdType) -> Promise + + func update(userCourse: UserCourse) -> Promise +} + +extension UserCoursesNetworkServiceProtocol { + func fetch() -> Promise<([UserCourse], Meta)> { + self.fetch(page: 1) + } +} + +final class UserCoursesNetworkService: UserCoursesNetworkServiceProtocol { + private let userCoursesAPI: UserCoursesAPI + + init(userCoursesAPI: UserCoursesAPI) { + self.userCoursesAPI = userCoursesAPI + } + + func fetch(page: Int) -> Promise<([UserCourse], Meta)> { + Promise { seal in + self.userCoursesAPI.retrieve(page: page).done { userCourses, meta in + seal.fulfill((userCourses, meta)) + }.catch { _ in + seal.reject(Error.fetchFailed) + } + } + } + + func fetch(courseID: Course.IdType) -> Promise { + Promise { seal in + self.userCoursesAPI.retrieve(courseID: courseID).done { userCourses, _ in + seal.fulfill(userCourses.first) + }.catch { _ in + seal.reject(Error.fetchFailed) + } + } + } + + func update(userCourse: UserCourse) -> Promise { + Promise { seal in + self.userCoursesAPI.update(userCourse).done { userCourse in + seal.fulfill(userCourse) + }.catch { _ in + seal.reject(Error.updateFailed) + } + } + } + + enum Error: Swift.Error { + case fetchFailed + case updateFailed + } +} diff --git a/Stepic/en.lproj/Localizable.strings b/Stepic/en.lproj/Localizable.strings index aad75686f5..a9d6d77b4e 100644 --- a/Stepic/en.lproj/Localizable.strings +++ b/Stepic/en.lproj/Localizable.strings @@ -445,6 +445,10 @@ ContinueCourseCourseCurrentProgressTitle = "Current progress: %@/%@ points"; /* Course info */ CourseInfoTitle = "About course"; +CourseInfoCourseActionAddToFavoritesAlertTitle = "Add to Favorites"; +CourseInfoCourseActionRemoveFromFavoritesAlertTitle = "Remove from Favorites"; +CourseInfoCourseActionMoveToArchivedAlertTitle = "Move to Archived"; +CourseInfoCourseActionRemoveFromArchivedAlertTitle = "Remove from Archived"; CourseInfoTabInfo = "Info"; CourseInfoTabSyllabus = "Syllabus"; CourseInfoTabSyllabusSectionProgressTitle = "%@/%@ points"; diff --git a/Stepic/ru.lproj/Localizable.strings b/Stepic/ru.lproj/Localizable.strings index 63504bdab3..8964ba1cef 100644 --- a/Stepic/ru.lproj/Localizable.strings +++ b/Stepic/ru.lproj/Localizable.strings @@ -447,6 +447,10 @@ ContinueCourseCourseCurrentProgressTitle = "Текущий прогресс: %@/ /* Course info */ CourseInfoTitle = "О курсе"; +CourseInfoCourseActionAddToFavoritesAlertTitle = "Добавить в избранное"; +CourseInfoCourseActionRemoveFromFavoritesAlertTitle = "Убрать из избранного"; +CourseInfoCourseActionMoveToArchivedAlertTitle = "Переместить в архив"; +CourseInfoCourseActionRemoveFromArchivedAlertTitle = "Убрать из архива"; CourseInfoTabInfo = "Инфо"; CourseInfoTabSyllabus = "Модули"; CourseInfoTabSyllabusSectionProgressTitle = "%@/%@ баллов"; From b209d554b5eb79bba2a2c4f843b1c4b66913d87e Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Tue, 28 Apr 2020 13:45:51 +0300 Subject: [PATCH 03/11] User courses (#703) * Bump Tabman from 2.8.1 to 2.8.2 Bumps [Tabman](https://github.com/uias/Tabman) from 2.8.1 to 2.8.2 - [Release notes](https://github.com/uias/Tabman/releases/tag/2.8.2) - [Commits](https://github.com/uias/Tabman/compare/2.8.1...2.8.2) * Show user courses tabs * Show user courses * Fix OptionSet conformings * Fix DataBackUpdateServiceDelegate conformings * Handle data back updates * Delete unused code --- Podfile | 2 +- Podfile.lock | 8 +- Stepic.xcodeproj/project.pbxproj | 54 ++++- .../Profile/Profile/ProfilePresenter.swift | 11 +- .../Network/Endpoints/UserCoursesAPI.swift | 13 +- .../CourseList/CourseListAssembly.swift | 6 +- .../CourseListDataBackUpdateService.swift | 101 +++++++++ .../CourseListInteractor.swift | 64 ++++-- .../Provider/CourseListNetworkService.swift | 21 +- .../CourseList/Provider/CourseListTypes.swift | 24 ++- .../ContinueCourseInteractor.swift | 7 +- .../Modules/Home/HomeViewController.swift | 8 +- .../UserCourses/UserCoursesAssembly.swift | 17 ++ .../UserCourses/UserCoursesDataFlow.swift | 31 +++ .../UserCourses/UserCoursesInteractor.swift | 18 ++ .../UserCourses/UserCoursesPresenter.swift | 13 ++ .../UserCourses/UserCoursesView.swift | 35 ++++ .../UserCoursesViewController.swift | 196 ++++++++++++++++++ .../Views/NewSortingQuizElementView.swift | 4 +- .../Services/DataBackUpdateService.swift | 29 +-- Stepic/en.lproj/Localizable.strings | 6 + Stepic/ru.lproj/Localizable.strings | 6 + 22 files changed, 610 insertions(+), 64 deletions(-) create mode 100644 Stepic/Sources/Modules/CourseList/Interactor/CourseListDataBackUpdateService.swift rename Stepic/Sources/Modules/CourseList/{ => Interactor}/CourseListInteractor.swift (87%) create mode 100644 Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesAssembly.swift create mode 100644 Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesDataFlow.swift create mode 100644 Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesInteractor.swift create mode 100644 Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesPresenter.swift create mode 100644 Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesView.swift create mode 100644 Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesViewController.swift diff --git a/Podfile b/Podfile index d3ea6e574d..8b074b0776 100644 --- a/Podfile +++ b/Podfile @@ -63,7 +63,7 @@ def all_pods pod 'ActionSheetPicker-3.0', '2.4.0' pod 'Nuke', '8.4.1' pod 'STRegex', '2.1.1' - pod 'Tabman', '2.8.1' + pod 'Tabman', '2.8.2' pod 'SwiftDate', '6.1.0' end diff --git a/Podfile.lock b/Podfile.lock index 418dc13f7d..e43c59ec72 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -176,7 +176,7 @@ PODS: - SwiftLint (0.39.2) - SwiftyGif (5.2.0) - SwiftyJSON (5.0.0) - - Tabman (2.8.1): + - Tabman (2.8.2): - Pageboy (~> 3.5.1) - TSMessages (0.9.13): - HexColors (~> 2.3.0) @@ -232,7 +232,7 @@ DEPENDENCIES: - SwiftDate (= 6.1.0) - SwiftLint (= 0.39.2) - SwiftyJSON (= 5.0.0) - - Tabman (= 2.8.1) + - Tabman (= 2.8.2) - TSMessages (from `https://github.com/KrauseFx/TSMessages.git`) - TTTAttributedLabel (= 2.0.0) - TUSafariActivity (= 1.0.4) @@ -381,7 +381,7 @@ SPEC CHECKSUMS: SwiftLint: 22ccbbe3b8008684be5955693bab135e0ed6a447 SwiftyGif: b85c6b33a9411859d9e1db998b6a8214aea942df SwiftyJSON: 36413e04c44ee145039d332b4f4e2d3e8d6c4db7 - Tabman: b1d349045017c25d558aef052c2d7eeac1273c0c + Tabman: 05f32e419b07411b5ec74516212ffbff2a9dfc0e TSMessages: eb3cf27b6900684a21bad4fe9ea426e287b8c839 TTTAttributedLabel: 8cffe8e127e4e82ff3af1e5386d4cd0ad000b656 TUSafariActivity: afc55a00965377939107ce4fdc7f951f62454546 @@ -389,6 +389,6 @@ SPEC CHECKSUMS: VK-ios-sdk: 62a10b6571fbcda0657f455fedce7fedf55b4cd0 YandexMobileMetrica: 1030df2959c886a7be3bc3d274eb54c8e86afd6f -PODFILE CHECKSUM: 102d17c414cc3f0207677129a616c4df7481f7e3 +PODFILE CHECKSUM: 90b5bb417626b8d84cd9f2e1ab3eba1529b4b7c2 COCOAPODS: 1.9.1 diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index 1808877cc9..10d46e7a59 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -351,6 +351,7 @@ 08FEFC1E1F117257005CA0FB /* CodeSuggestionTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 08FEFC1B1F117257005CA0FB /* CodeSuggestionTableViewCell.xib */; }; 08FEFC211F127470005CA0FB /* AutocompleteWords.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08FEFC201F127470005CA0FB /* AutocompleteWords.swift */; }; 245107514E8F81CF9A8C4C44 /* DownloadARQuickLookView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FDC926BE23A9131D45DFF0 /* DownloadARQuickLookView.swift */; }; + 2A7E1C8409B12A6CB09F0393 /* UserCoursesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A044A06FA4B2AA6C9F984A3 /* UserCoursesViewController.swift */; }; 2AFCCAAEBDBC4C05F4D5E5F0 /* DownloadARQuickLookDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A65C1DFD5A2533C506D297B /* DownloadARQuickLookDataFlow.swift */; }; 2C01BB68233CD92C00C8DCF0 /* Require.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C01BB67233CD92C00C8DCF0 /* Require.swift */; }; 2C01D3AA22DDB7EA00C84CEE /* DefaultsContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C01D3A922DDB7EA00C84CEE /* DefaultsContainer.swift */; }; @@ -426,6 +427,7 @@ 2C2F0BE92186EF87007DCA0A /* CommonNotificationsRequestAlertDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2F0BE82186EF87007DCA0A /* CommonNotificationsRequestAlertDataSource.swift */; }; 2C2F0BEC2186F0C3007DCA0A /* NotificationsRequestAlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2F0BEB2186F0C3007DCA0A /* NotificationsRequestAlertPresenter.swift */; }; 2C2F0BEE2186F196007DCA0A /* NotificationsRequestAlertDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2F0BED2186F196007DCA0A /* NotificationsRequestAlertDataSource.swift */; }; + 2C30505B24574B1F005DCD81 /* CourseListDataBackUpdateService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C30505A24574B1F005DCD81 /* CourseListDataBackUpdateService.swift */; }; 2C32E71320FF6C1D008BB909 /* Auth.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2CC351841F6827BE004255B6 /* Auth.storyboard */; }; 2C331C5721734764007B0D01 /* NotificationService+Streak.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C331C5621734764007B0D01 /* NotificationService+Streak.swift */; }; 2C3ABA8923D1DA8D00E90439 /* AboutAppViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3ABA8823D1DA8D00E90439 /* AboutAppViewController.swift */; }; @@ -671,6 +673,7 @@ 2CFC5ABC228ADFC400B5248A /* StepsPersistenceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFC5ABB228ADFC400B5248A /* StepsPersistenceService.swift */; }; 2CFDB1241F559F9A00B8035C /* AvatarImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFDB1231F559F9A00B8035C /* AvatarImageView.swift */; }; 3203AD6A1594995EDE114EA0 /* Pods_StepicTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 938533EAAD61D57EF139C60C /* Pods_StepicTests.framework */; }; + 39AB2324EB824124A83D7534 /* UserCoursesInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09AF7B8FAACF8A4D31AF7583 /* UserCoursesInteractor.swift */; }; 62E980270B31473DE5E16CDD /* SubmissionsAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98C9C16C33663FD3D7021 /* SubmissionsAssembly.swift */; }; 62E98034865DC79E2B5C5D67 /* BaseExploreDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E9838546792D675BC7EA54 /* BaseExploreDataFlow.swift */; }; 62E98042B4F232FCD9788187 /* CodeQuizFullscreenProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98398BC9BB107755305A1 /* CodeQuizFullscreenProvider.swift */; }; @@ -1135,8 +1138,11 @@ 62E98FB9AFF029A44C3A1AAE /* CourseInfoTabSyllabusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98C1FE5E4C87C3199EC01 /* CourseInfoTabSyllabusProvider.swift */; }; 62E98FE1CE19810D6930DAE6 /* SubmissionsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E9845417AC50C4D61B42C6 /* SubmissionsProvider.swift */; }; 62E98FFD1EEDB775EE381221 /* CourseListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98F968FBB3ED0E6309407 /* CourseListPresenter.swift */; }; + 6670CEAE1D677A5EADD37531 /* UserCoursesAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC26997F83B299C9E3FCC5D /* UserCoursesAssembly.swift */; }; + 7C948DD5EBB144CE0658E454 /* UserCoursesDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1A4BED02A9D05CB0787EBE1 /* UserCoursesDataFlow.swift */; }; 861B96371FE1DF7F00773EDA /* CAGradientLayer+Init.swift in Sources */ = {isa = PBXBuildFile; fileRef = 861B96361FE1DF7F00773EDA /* CAGradientLayer+Init.swift */; }; 8622056B2055561F00F14255 /* PinsMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8622056A2055561F00F14255 /* PinsMapView.swift */; }; + 86356B693AF04C13CE3C856F /* UserCoursesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAF219462BE0AB704DED8077 /* UserCoursesView.swift */; }; 86624A731FC76578008E7E6C /* NotificationStatusesAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86624A721FC76578008E7E6C /* NotificationStatusesAPI.swift */; }; 86624A751FC76682008E7E6C /* NotificationsStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86624A741FC76682008E7E6C /* NotificationsStatus.swift */; }; 866AD0D4206145DC0004C2B2 /* StepikPlaceholderStyle+Placeholders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 866AD0D3206145DC0004C2B2 /* StepikPlaceholderStyle+Placeholders.swift */; }; @@ -1144,6 +1150,7 @@ 86BB7C022019538100063538 /* CongratsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86BB7C012019538000063538 /* CongratsView.swift */; }; 86BB7C07201953AF00063538 /* CongratulationAlertManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86BB7C04201953AE00063538 /* CongratulationAlertManager.swift */; }; 86BB7C09201953AF00063538 /* CongratulationViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 86BB7C06201953AF00063538 /* CongratulationViewController.xib */; }; + 942F44578E193384B1E4DAD9 /* UserCoursesPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582946156484A5655EF72E43 /* UserCoursesPresenter.swift */; }; AAB4F45B011C05D59E6EEEEB /* DownloadARQuickLookPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DCDAA39E6747A12CC239E3 /* DownloadARQuickLookPresenter.swift */; }; C2537B043956035AEFCABAB5 /* DownloadARQuickLookOutputProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD2CFBDE7B71C461F988B1B5 /* DownloadARQuickLookOutputProtocol.swift */; }; C95BEF69AB5100E263ED66F6 /* DownloadARQuickLookAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = B259C3826C752171E0305872 /* DownloadARQuickLookAssembly.swift */; }; @@ -1574,6 +1581,8 @@ 08FEFC1A1F117257005CA0FB /* CodeSuggestionTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CodeSuggestionTableViewCell.swift; sourceTree = ""; }; 08FEFC1B1F117257005CA0FB /* CodeSuggestionTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CodeSuggestionTableViewCell.xib; sourceTree = ""; }; 08FEFC201F127470005CA0FB /* AutocompleteWords.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutocompleteWords.swift; sourceTree = ""; }; + 09AF7B8FAACF8A4D31AF7583 /* UserCoursesInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UserCoursesInteractor.swift; sourceTree = ""; }; + 1A044A06FA4B2AA6C9F984A3 /* UserCoursesViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UserCoursesViewController.swift; sourceTree = ""; }; 1A65C1DFD5A2533C506D297B /* DownloadARQuickLookDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DownloadARQuickLookDataFlow.swift; sourceTree = ""; }; 2C0176C02188953700DDB9D0 /* NotificationAlertsAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationAlertsAnalytics.swift; sourceTree = ""; }; 2C01BB67233CD92C00C8DCF0 /* Require.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Require.swift; sourceTree = ""; }; @@ -1655,6 +1664,7 @@ 2C2F0BE82186EF87007DCA0A /* CommonNotificationsRequestAlertDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonNotificationsRequestAlertDataSource.swift; sourceTree = ""; }; 2C2F0BEB2186F0C3007DCA0A /* NotificationsRequestAlertPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsRequestAlertPresenter.swift; sourceTree = ""; }; 2C2F0BED2186F196007DCA0A /* NotificationsRequestAlertDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsRequestAlertDataSource.swift; sourceTree = ""; }; + 2C30505A24574B1F005DCD81 /* CourseListDataBackUpdateService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseListDataBackUpdateService.swift; sourceTree = ""; }; 2C331C5621734764007B0D01 /* NotificationService+Streak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationService+Streak.swift"; sourceTree = ""; }; 2C35C4C61F4DA462002F3BF4 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = ru; path = LeaderboardNames/ru.lproj/adjectives_f.plist; sourceTree = ""; }; 2C35C4C81F4DA462002F3BF4 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = ru; path = LeaderboardNames/ru.lproj/adjectives_m.plist; sourceTree = ""; }; @@ -1919,6 +1929,7 @@ 3BCD8888462BD21ECF82C602 /* DownloadARQuickLookViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DownloadARQuickLookViewController.swift; sourceTree = ""; }; 3C1049108DA5FB2370275330 /* Pods-StepicTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-StepicTests.release.xcconfig"; path = "Target Support Files/Pods-StepicTests/Pods-StepicTests.release.xcconfig"; sourceTree = ""; }; 49B8797DC84D64C5BAA84E76 /* Pods-Stepic.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Stepic.debug.xcconfig"; path = "Target Support Files/Pods-Stepic/Pods-Stepic.debug.xcconfig"; sourceTree = ""; }; + 582946156484A5655EF72E43 /* UserCoursesPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UserCoursesPresenter.swift; sourceTree = ""; }; 61DCDAA39E6747A12CC239E3 /* DownloadARQuickLookPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DownloadARQuickLookPresenter.swift; sourceTree = ""; }; 62E9800E79AB72A13F894B73 /* CourseInfoTabSyllabusInputProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseInfoTabSyllabusInputProtocol.swift; sourceTree = ""; }; 62E98021576A447517397409 /* ContinueCourseView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContinueCourseView.swift; sourceTree = ""; }; @@ -2395,9 +2406,12 @@ 86BB7C05201953AF00063538 /* CongratulationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CongratulationViewController.swift; sourceTree = ""; }; 86BB7C06201953AF00063538 /* CongratulationViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CongratulationViewController.xib; sourceTree = ""; }; 938533EAAD61D57EF139C60C /* Pods_StepicTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_StepicTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9EC26997F83B299C9E3FCC5D /* UserCoursesAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UserCoursesAssembly.swift; sourceTree = ""; }; 9F98579F526E5A4D162C3356 /* Pods_Stepic.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Stepic.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B259C3826C752171E0305872 /* DownloadARQuickLookAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DownloadARQuickLookAssembly.swift; sourceTree = ""; }; B3FDC926BE23A9131D45DFF0 /* DownloadARQuickLookView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DownloadARQuickLookView.swift; sourceTree = ""; }; + BAF219462BE0AB704DED8077 /* UserCoursesView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UserCoursesView.swift; sourceTree = ""; }; + C1A4BED02A9D05CB0787EBE1 /* UserCoursesDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UserCoursesDataFlow.swift; sourceTree = ""; }; CC129A0368739A641E6ED26A /* DownloadARQuickLookInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DownloadARQuickLookInteractor.swift; sourceTree = ""; }; CD2CFBDE7B71C461F988B1B5 /* DownloadARQuickLookOutputProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DownloadARQuickLookOutputProtocol.swift; sourceTree = ""; }; D109E72D69B31373C97237F8 /* Pods-Stepic.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Stepic.release.xcconfig"; path = "Target Support Files/Pods-Stepic/Pods-Stepic.release.xcconfig"; sourceTree = ""; }; @@ -2548,6 +2562,14 @@ path = Analytics; sourceTree = ""; }; + 2C03627F2456DE0F00551807 /* HomeSubmodules */ = { + isa = PBXGroup; + children = ( + C5254161C4484A82273DE3C7 /* UserCourses */, + ); + path = HomeSubmodules; + sourceTree = ""; + }; 2C04BA2E24058A4B00D74D4B /* Attempt */ = { isa = PBXGroup; children = ( @@ -2649,6 +2671,15 @@ path = Repository; sourceTree = ""; }; + 2C30505924574A82005DCD81 /* Interactor */ = { + isa = PBXGroup; + children = ( + 2C30505A24574B1F005DCD81 /* CourseListDataBackUpdateService.swift */, + 62E9846FAD1E23AB87024093 /* CourseListInteractor.swift */, + ); + path = Interactor; + sourceTree = ""; + }; 2C37968421ABED9500BC7E62 /* ActiveTests */ = { isa = PBXGroup; children = ( @@ -5094,6 +5125,7 @@ 62E9854F0F8A7B3F51F4BC7F /* ExploreSubmodules */, 62E98209A197353446CA6EB4 /* FullscreenCourseList */, 62E98467D5F10A9715F7FABA /* Home */, + 2C03627F2456DE0F00551807 /* HomeSubmodules */, 62E981F9A38A8FB18756836C /* Lesson */, 62E9888B323F8D7D6B0F33B7 /* ProfileEdit */, 62E98EE82D8C9EB6D4AB5223 /* Quizzes */, @@ -5183,11 +5215,11 @@ children = ( 62E984D225AB3EB455A51F98 /* CourseListAssembly.swift */, 62E98A2F0861B457FAABF490 /* CourseListDataFlow.swift */, - 62E9846FAD1E23AB87024093 /* CourseListInteractor.swift */, 62E98F968FBB3ED0E6309407 /* CourseListPresenter.swift */, 62E98C2D7CF9196EC220CE32 /* CourseListViewController.swift */, 62E98C3E78674AF597721978 /* CourseWidgetViewModel.swift */, 62E98E455B3841471584F9BC /* InputOutput */, + 2C30505924574A82005DCD81 /* Interactor */, 62E98DCEFD7DB9F89C8D75AF /* Provider */, 62E980A63F3F8952B27F10D8 /* Views */, ); @@ -6287,6 +6319,19 @@ path = InputOutput; sourceTree = ""; }; + C5254161C4484A82273DE3C7 /* UserCourses */ = { + isa = PBXGroup; + children = ( + 9EC26997F83B299C9E3FCC5D /* UserCoursesAssembly.swift */, + C1A4BED02A9D05CB0787EBE1 /* UserCoursesDataFlow.swift */, + 09AF7B8FAACF8A4D31AF7583 /* UserCoursesInteractor.swift */, + 582946156484A5655EF72E43 /* UserCoursesPresenter.swift */, + BAF219462BE0AB704DED8077 /* UserCoursesView.swift */, + 1A044A06FA4B2AA6C9F984A3 /* UserCoursesViewController.swift */, + ); + path = UserCourses; + sourceTree = ""; + }; DCA78145E487F3C8F1848CF6 /* Pods */ = { isa = PBXGroup; children = ( @@ -7339,6 +7384,7 @@ 087387DA1BB9B768003CFAD1 /* ErrorEnums.swift in Sources */, 2CD8464B1F25FE8B00E8153C /* ProfilesAPI.swift in Sources */, 088E73F020619C8F00D458E3 /* DeleteRequestMaker.swift in Sources */, + 2C30505B24574B1F005DCD81 /* CourseListDataBackUpdateService.swift in Sources */, 0885F8521BA9D64400F2A188 /* Constants.swift in Sources */, 2C16ED3523D0BC620017C113 /* SettingsRightDetailCellView.swift in Sources */, 08BC47061CD9F424009A1D25 /* DeleteDeviceExecutableTask.swift in Sources */, @@ -7921,6 +7967,12 @@ 245107514E8F81CF9A8C4C44 /* DownloadARQuickLookView.swift in Sources */, 008437078C67C7149BE6DE53 /* DownloadARQuickLookViewController.swift in Sources */, C2537B043956035AEFCABAB5 /* DownloadARQuickLookOutputProtocol.swift in Sources */, + 6670CEAE1D677A5EADD37531 /* UserCoursesAssembly.swift in Sources */, + 7C948DD5EBB144CE0658E454 /* UserCoursesDataFlow.swift in Sources */, + 39AB2324EB824124A83D7534 /* UserCoursesInteractor.swift in Sources */, + 942F44578E193384B1E4DAD9 /* UserCoursesPresenter.swift in Sources */, + 86356B693AF04C13CE3C856F /* UserCoursesView.swift in Sources */, + 2A7E1C8409B12A6CB09F0393 /* UserCoursesViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Stepic/Legacy/Controllers/Profile/Profile/ProfilePresenter.swift b/Stepic/Legacy/Controllers/Profile/Profile/ProfilePresenter.swift index 300e3de9f2..4fffa3586b 100644 --- a/Stepic/Legacy/Controllers/Profile/Profile/ProfilePresenter.swift +++ b/Stepic/Legacy/Controllers/Profile/Profile/ProfilePresenter.swift @@ -295,7 +295,16 @@ extension ProfilePresenter: ProfileAchievementsPresenterDelegate { } extension ProfilePresenter: DataBackUpdateServiceDelegate { - func dataBackUpdateService(_ dataBackUpdateService: DataBackUpdateService, didReport refreshedTarget: DataBackUpdateTarget) { + func dataBackUpdateService( + _ dataBackUpdateService: DataBackUpdateService, + didReport update: DataBackUpdateDescription, + for target: DataBackUpdateTarget + ) {} + + func dataBackUpdateService( + _ dataBackUpdateService: DataBackUpdateService, + didReport refreshedTarget: DataBackUpdateTarget + ) { if case .profile(let profile) = refreshedTarget { self.headerInfoPresenter?.update(with: profile) self.descriptionPresenter?.update(with: profile) diff --git a/Stepic/Legacy/Model/Network/Endpoints/UserCoursesAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/UserCoursesAPI.swift index b698dd6055..2b126d58b4 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/UserCoursesAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/UserCoursesAPI.swift @@ -13,11 +13,22 @@ import PromiseKit final class UserCoursesAPI: APIEndpoint { override var name: String { "user-courses" } - func retrieve(page: Int = 1) -> Promise<([UserCourse], Meta)> { + func retrieve( + page: Int = 1, + isArchived: Bool? = nil, + isFavorite: Bool? = nil + ) -> Promise<([UserCourse], Meta)> { Promise { seal in var params = Parameters() params["page"] = page + if let isArchived = isArchived { + params[UserCourse.JSONKey.isArchived.rawValue] = String(isArchived) + } + if let isFavorite = isFavorite { + params[UserCourse.JSONKey.isFavorite.rawValue] = String(isFavorite) + } + self.retrieve.request( requestEndpoint: self.name, paramName: self.name, diff --git a/Stepic/Sources/Modules/CourseList/CourseListAssembly.swift b/Stepic/Sources/Modules/CourseList/CourseListAssembly.swift index 16f845218b..5d0568e598 100644 --- a/Stepic/Sources/Modules/CourseList/CourseListAssembly.swift +++ b/Stepic/Sources/Modules/CourseList/CourseListAssembly.swift @@ -54,6 +54,10 @@ class CourseListAssembly: Assembly { coursesNetworkService: CoursesNetworkService(coursesAPI: CoursesAPI()), progressesNetworkService: ProgressesNetworkService(progressesAPI: ProgressesAPI()) ) + let courseListDataBackUpdateService = CourseListDataBackUpdateService( + courseListType: self.type, + dataBackUpdateService: dataBackUpdateService + ) let interactor = CourseListInteractor( presenter: presenter, @@ -62,7 +66,7 @@ class CourseListAssembly: Assembly { courseSubscriber: CourseSubscriber(), userAccountService: UserAccountService(), personalDeadlinesService: PersonalDeadlinesService(), - dataBackUpdateService: dataBackUpdateService + courseListDataBackUpdateService: courseListDataBackUpdateService ) self.moduleInput = interactor diff --git a/Stepic/Sources/Modules/CourseList/Interactor/CourseListDataBackUpdateService.swift b/Stepic/Sources/Modules/CourseList/Interactor/CourseListDataBackUpdateService.swift new file mode 100644 index 0000000000..09663a72fa --- /dev/null +++ b/Stepic/Sources/Modules/CourseList/Interactor/CourseListDataBackUpdateService.swift @@ -0,0 +1,101 @@ +import Foundation + +protocol CourseListDataBackUpdateServiceDelegate: AnyObject { + /// Tells the delegate that the specified course is updated. + func courseListDataBackUpdateService( + _ service: CourseListDataBackUpdateServiceProtocol, + didUpdateCourse course: Course + ) + /// Tells the delegate that the specified course is deleted. + func courseListDataBackUpdateService( + _ service: CourseListDataBackUpdateServiceProtocol, + didDeleteCourse course: Course + ) + /// Tells the delegate that the specified course is inserted. + func courseListDataBackUpdateService( + _ service: CourseListDataBackUpdateServiceProtocol, + didInsertCourse course: Course + ) +} + +protocol CourseListDataBackUpdateServiceProtocol: AnyObject { + var delegate: CourseListDataBackUpdateServiceDelegate? { get set } +} + +final class CourseListDataBackUpdateService: CourseListDataBackUpdateServiceProtocol { + private let courseListType: CourseListType + private let dataBackUpdateService: DataBackUpdateServiceProtocol + + weak var delegate: CourseListDataBackUpdateServiceDelegate? + + init( + courseListType: CourseListType, + dataBackUpdateService: DataBackUpdateServiceProtocol + ) { + self.courseListType = courseListType + self.dataBackUpdateService = dataBackUpdateService + self.dataBackUpdateService.delegate = self + } +} + +extension CourseListDataBackUpdateService: DataBackUpdateServiceDelegate { + func dataBackUpdateService( + _ dataBackUpdateService: DataBackUpdateService, + didReport update: DataBackUpdateDescription, + for target: DataBackUpdateTarget + ) { + guard case .course(let course) = target else { + return + } + + // If progress or enrollment state was updated then refresh course with data + if update.contains(.progress) || update.contains(.enrollment) { + self.delegate?.courseListDataBackUpdateService(self, didUpdateCourse: course) + } + + // If isArchived or isFavorite state was updated then handle specified update and refresh course list + if update.contains(.courseIsArchived) || update.contains(.courseIsFavorite) { + self.handleUserCoursesCourseUpdate(update: update, course: course) + } + } + + func dataBackUpdateService( + _ dataBackUpdateService: DataBackUpdateService, + didReport refreshedTarget: DataBackUpdateTarget + ) {} + + private func handleUserCoursesCourseUpdate(update: DataBackUpdateDescription, course: Course) { + let isUserCoursesCourseListType = self.courseListType is EnrolledCourseListType + || self.courseListType is FavoriteCourseListType + || self.courseListType is ArchivedCourseListType + guard isUserCoursesCourseListType else { + return + } + + if update.contains(.courseIsArchived) { + if self.courseListType is EnrolledCourseListType { + if course.isArchived { + self.delegate?.courseListDataBackUpdateService(self, didDeleteCourse: course) + } else { + self.delegate?.courseListDataBackUpdateService(self, didInsertCourse: course) + } + } else if self.courseListType is ArchivedCourseListType { + if course.isArchived { + self.delegate?.courseListDataBackUpdateService(self, didInsertCourse: course) + } else { + self.delegate?.courseListDataBackUpdateService(self, didDeleteCourse: course) + } + } + } + + if update.contains(.courseIsFavorite) { + if self.courseListType is FavoriteCourseListType { + if course.isFavorite { + self.delegate?.courseListDataBackUpdateService(self, didInsertCourse: course) + } else { + self.delegate?.courseListDataBackUpdateService(self, didDeleteCourse: course) + } + } + } + } +} diff --git a/Stepic/Sources/Modules/CourseList/CourseListInteractor.swift b/Stepic/Sources/Modules/CourseList/Interactor/CourseListInteractor.swift similarity index 87% rename from Stepic/Sources/Modules/CourseList/CourseListInteractor.swift rename to Stepic/Sources/Modules/CourseList/Interactor/CourseListInteractor.swift index e7c785656b..fd81c1ffaa 100644 --- a/Stepic/Sources/Modules/CourseList/CourseListInteractor.swift +++ b/Stepic/Sources/Modules/CourseList/Interactor/CourseListInteractor.swift @@ -23,7 +23,7 @@ final class CourseListInteractor: CourseListInteractorProtocol { private let courseSubscriber: CourseSubscriberProtocol private let userAccountService: UserAccountServiceProtocol private let personalDeadlinesService: PersonalDeadlinesServiceProtocol - private let dataBackUpdateService: DataBackUpdateServiceProtocol + private let courseListDataBackUpdateService: CourseListDataBackUpdateServiceProtocol private var isOnline = false private var didLoadFromCache = false @@ -37,7 +37,7 @@ final class CourseListInteractor: CourseListInteractorProtocol { courseSubscriber: CourseSubscriberProtocol, userAccountService: UserAccountServiceProtocol, personalDeadlinesService: PersonalDeadlinesServiceProtocol, - dataBackUpdateService: DataBackUpdateServiceProtocol + courseListDataBackUpdateService: CourseListDataBackUpdateServiceProtocol ) { self.presenter = presenter self.provider = provider @@ -46,8 +46,8 @@ final class CourseListInteractor: CourseListInteractorProtocol { self.userAccountService = userAccountService self.personalDeadlinesService = personalDeadlinesService - self.dataBackUpdateService = dataBackUpdateService - self.dataBackUpdateService.delegate = self + self.courseListDataBackUpdateService = courseListDataBackUpdateService + self.courseListDataBackUpdateService.delegate = self } // MARK: - Public methods @@ -160,7 +160,7 @@ final class CourseListInteractor: CourseListInteractorProtocol { ) self.presenter.presentNextCourses(response: response) - self.provider.cache(courses: self.currentCourses.map { $0.1 }) + self.cacheCurrentCourses() }.catch { error in let response = CourseList.NextCoursesLoad.Response( isAuthorized: self.userAccountService.isAuthorized, @@ -269,6 +269,24 @@ final class CourseListInteractor: CourseListInteractorProtocol { self.currentCourses[targetIndex] = (self.getUniqueIdentifierForCourse(course), course) } + private func deleteCourseInCurrentCourses(_ course: Course) { + self.currentCourses.removeAll { $0.0 == self.getUniqueIdentifierForCourse(course) } + } + + private func insertCourseInCurrentCourses(_ course: Course) { + let newElement = (self.getUniqueIdentifierForCourse(course), course) + + if let targetIndex = self.currentCourses.firstIndex(where: { $0.1 == course }) { + self.currentCourses[targetIndex] = newElement + } else { + self.currentCourses.insert(newElement, at: 0) + } + } + + private func cacheCurrentCourses() { + self.provider.cache(courses: self.currentCourses.map { $0.1 }) + } + /// Just present current data again private func refreshCourseList() { let courses = CourseList.AvailableCourses( @@ -310,20 +328,30 @@ extension CourseListInteractor: CourseListInputProtocol { } } -extension CourseListInteractor: DataBackUpdateServiceDelegate { - func dataBackUpdateService( - _ dataBackUpdateService: DataBackUpdateService, - reportsUpdate update: DataBackUpdateDescription, - for target: DataBackUpdateTarget +extension CourseListInteractor: CourseListDataBackUpdateServiceDelegate { + func courseListDataBackUpdateService( + _ service: CourseListDataBackUpdateServiceProtocol, + didUpdateCourse course: Course ) { - guard case .course(let course) = target else { - return - } + self.updateCourseInCurrentCourses(course) + self.refreshCourseList() + } - // If progress or enrollment state was updated then refresh course with data - if update.contains(.progress) || update.contains(.enrollment) { - self.updateCourseInCurrentCourses(course) - self.refreshCourseList() - } + func courseListDataBackUpdateService( + _ service: CourseListDataBackUpdateServiceProtocol, + didDeleteCourse course: Course + ) { + self.deleteCourseInCurrentCourses(course) + self.cacheCurrentCourses() + self.refreshCourseList() + } + + func courseListDataBackUpdateService( + _ service: CourseListDataBackUpdateServiceProtocol, + didInsertCourse course: Course + ) { + self.insertCourseInCurrentCourses(course) + self.cacheCurrentCourses() + self.refreshCourseList() } } diff --git a/Stepic/Sources/Modules/CourseList/Provider/CourseListNetworkService.swift b/Stepic/Sources/Modules/CourseList/Provider/CourseListNetworkService.swift index fb3fdd4e2e..2fe0419665 100644 --- a/Stepic/Sources/Modules/CourseList/Provider/CourseListNetworkService.swift +++ b/Stepic/Sources/Modules/CourseList/Provider/CourseListNetworkService.swift @@ -17,12 +17,24 @@ class BaseCourseListNetworkService { } } -final class EnrolledCourseListNetworkService: BaseCourseListNetworkService, CourseListNetworkServiceProtocol { - let type: EnrolledCourseListType +final class UserCoursesCourseListNetworkService: BaseCourseListNetworkService, CourseListNetworkServiceProtocol { + let type: CourseListType private let userCoursesAPI: UserCoursesAPI + private var fetchParams: (isArchived: Bool?, isFavorite: Bool?) { + if self.type is EnrolledCourseListType { + return (false, nil) + } else if self.type is FavoriteCourseListType { + return (nil, true) + } else if self.type is ArchivedCourseListType { + return (true, nil) + } else { + fatalError("Unsupported course list type") + } + } + init( - type: EnrolledCourseListType, + type: CourseListType, coursesAPI: CoursesAPI, userCoursesAPI: UserCoursesAPI ) { @@ -33,7 +45,8 @@ final class EnrolledCourseListNetworkService: BaseCourseListNetworkService, Cour func fetch(page: Int = 1) -> Promise<([Course], Meta)> { Promise { seal in - self.userCoursesAPI.retrieve(page: page).then { + let (isArchived, isFavorite) = self.fetchParams + self.userCoursesAPI.retrieve(page: page, isArchived: isArchived, isFavorite: isFavorite).then { userCoursesInfo -> Promise<([Course], [UserCourse], Meta)> in // Cause we can't pass empty ids list to courses endpoint if userCoursesInfo.0.isEmpty { diff --git a/Stepic/Sources/Modules/CourseList/Provider/CourseListTypes.swift b/Stepic/Sources/Modules/CourseList/Provider/CourseListTypes.swift index fb78571980..95ab4a1141 100644 --- a/Stepic/Sources/Modules/CourseList/Provider/CourseListTypes.swift +++ b/Stepic/Sources/Modules/CourseList/Provider/CourseListTypes.swift @@ -12,6 +12,10 @@ struct PopularCourseListType: CourseListType { struct EnrolledCourseListType: CourseListType {} +struct FavoriteCourseListType: CourseListType {} + +struct ArchivedCourseListType: CourseListType {} + struct TagCourseListType: CourseListType { let id: Int let language: ContentLanguage @@ -53,6 +57,18 @@ final class CourseListServicesFactory { cacheID: "MyCoursesInfo" ) ) + } else if self.type is FavoriteCourseListType { + return CourseListPersistenceService( + storage: DefaultsCourseListPersistenceStorage( + cacheID: "MyCoursesInfoFavorite" + ) + ) + } else if self.type is ArchivedCourseListType { + return CourseListPersistenceService( + storage: DefaultsCourseListPersistenceStorage( + cacheID: "MyCoursesInfoArchived" + ) + ) } else if let type = self.type as? PopularCourseListType { return CourseListPersistenceService( storage: DefaultsCourseListPersistenceStorage( @@ -75,9 +91,11 @@ final class CourseListServicesFactory { } func makeNetworkService() -> CourseListNetworkServiceProtocol { - if let type = self.type as? EnrolledCourseListType { - return EnrolledCourseListNetworkService( - type: type, + if self.type is EnrolledCourseListType + || self.type is FavoriteCourseListType + || self.type is ArchivedCourseListType { + return UserCoursesCourseListNetworkService( + type: self.type, coursesAPI: self.coursesAPI, userCoursesAPI: self.userCoursesAPI ) diff --git a/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseInteractor.swift b/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseInteractor.swift index 9b4b534541..306285d76d 100644 --- a/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseInteractor.swift +++ b/Stepic/Sources/Modules/ExploreSubmodules/ContinueCourse/ContinueCourseInteractor.swift @@ -82,7 +82,7 @@ final class ContinueCourseInteractor: ContinueCourseInteractorProtocol { extension ContinueCourseInteractor: DataBackUpdateServiceDelegate { func dataBackUpdateService( _ dataBackUpdateService: DataBackUpdateService, - reportsUpdate update: DataBackUpdateDescription, + didReport update: DataBackUpdateDescription, for target: DataBackUpdateTarget ) { guard case .course(let course) = target, @@ -93,4 +93,9 @@ extension ContinueCourseInteractor: DataBackUpdateServiceDelegate { self.currentCourse = course self.presenter.presentLastCourse(response: .init(result: course)) } + + func dataBackUpdateService( + _ dataBackUpdateService: DataBackUpdateService, + didReport refreshedTarget: DataBackUpdateTarget + ) {} } diff --git a/Stepic/Sources/Modules/Home/HomeViewController.swift b/Stepic/Sources/Modules/Home/HomeViewController.swift index 034a723b2f..f9d9f17868 100644 --- a/Stepic/Sources/Modules/Home/HomeViewController.swift +++ b/Stepic/Sources/Modules/Home/HomeViewController.swift @@ -124,12 +124,8 @@ final class HomeViewController: BaseExploreViewController { // MARK: - Fullscreen displaying private func displayFullscreenEnrolledCourseList() { - self.interactor.doFullscreenCourseListPresentation( - request: .init( - presentationDescription: nil, - courseListType: EnrolledCourseListType() - ) - ) + let assembly = UserCoursesAssembly() + self.push(module: assembly.makeModule()) } private func displayFullscreenPopularCourseList(contentLanguage: ContentLanguage) { diff --git a/Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesAssembly.swift b/Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesAssembly.swift new file mode 100644 index 0000000000..7930dfbd6f --- /dev/null +++ b/Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesAssembly.swift @@ -0,0 +1,17 @@ +import UIKit + +final class UserCoursesAssembly: Assembly { + func makeModule() -> UIViewController { + let presenter = UserCoursesPresenter() + let interactor = UserCoursesInteractor(presenter: presenter) + let viewController = UserCoursesViewController( + interactor: interactor, + availableTabs: UserCourses.Tab.allCases, + initialTab: .allCourses + ) + + presenter.viewController = viewController + + return viewController + } +} diff --git a/Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesDataFlow.swift b/Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesDataFlow.swift new file mode 100644 index 0000000000..fba14da691 --- /dev/null +++ b/Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesDataFlow.swift @@ -0,0 +1,31 @@ +import Foundation + +enum UserCourses { + enum Tab: CaseIterable { + case allCourses + case favorites + case archived + + var title: String { + switch self { + case .allCourses: + return NSLocalizedString("UserCoursesTabAllCoursesTitle", comment: "") + case .favorites: + return NSLocalizedString("UserCoursesTabFavoritesTitle", comment: "") + case .archived: + return NSLocalizedString("UserCoursesTabArchivedTitle", comment: "") + } + } + } + + // MARK: Use cases + + /// Show user courses course lists + enum UserCoursesLoad { + struct Request {} + + struct Response {} + + struct ViewModel {} + } +} diff --git a/Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesInteractor.swift b/Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesInteractor.swift new file mode 100644 index 0000000000..e85e7f57ad --- /dev/null +++ b/Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesInteractor.swift @@ -0,0 +1,18 @@ +import Foundation +import PromiseKit + +protocol UserCoursesInteractorProtocol { + func doUserCoursesFetch(request: UserCourses.UserCoursesLoad.Request) +} + +final class UserCoursesInteractor: UserCoursesInteractorProtocol { + private let presenter: UserCoursesPresenterProtocol + + init(presenter: UserCoursesPresenterProtocol) { + self.presenter = presenter + } + + func doUserCoursesFetch(request: UserCourses.UserCoursesLoad.Request) { + self.presenter.presentUserCourses(response: .init()) + } +} diff --git a/Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesPresenter.swift b/Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesPresenter.swift new file mode 100644 index 0000000000..bb03d10308 --- /dev/null +++ b/Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesPresenter.swift @@ -0,0 +1,13 @@ +import UIKit + +protocol UserCoursesPresenterProtocol { + func presentUserCourses(response: UserCourses.UserCoursesLoad.Response) +} + +final class UserCoursesPresenter: UserCoursesPresenterProtocol { + weak var viewController: UserCoursesViewControllerProtocol? + + func presentUserCourses(response: UserCourses.UserCoursesLoad.Response) { + self.viewController?.displayUserCourses(viewModel: .init()) + } +} diff --git a/Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesView.swift b/Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesView.swift new file mode 100644 index 0000000000..4bcbee5865 --- /dev/null +++ b/Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesView.swift @@ -0,0 +1,35 @@ +import SnapKit +import UIKit + +extension UserCoursesView { + struct Appearance {} +} + +final class UserCoursesView: UIView { + let appearance: Appearance + + init( + frame: CGRect = .zero, + appearance: Appearance = Appearance() + ) { + self.appearance = appearance + super.init(frame: frame) + + self.setupView() + self.addSubviews() + self.makeConstraints() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension UserCoursesView: ProgrammaticallyInitializableViewProtocol { + func setupView() {} + + func addSubviews() {} + + func makeConstraints() {} +} diff --git a/Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesViewController.swift b/Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesViewController.swift new file mode 100644 index 0000000000..9974bd134c --- /dev/null +++ b/Stepic/Sources/Modules/HomeSubmodules/UserCourses/UserCoursesViewController.swift @@ -0,0 +1,196 @@ +import Pageboy +import SnapKit +import Tabman +import UIKit + +protocol UserCoursesViewControllerProtocol: AnyObject { + func displayUserCourses(viewModel: UserCourses.UserCoursesLoad.ViewModel) +} + +final class UserCoursesViewController: TabmanViewController { + enum Appearance { + static let backgroundColor = UIColor.stepikBackground + + static let barTintColor = UIColor.stepikAccent + static let barBackgroundColor = UIColor.stepikNavigationBarBackground + static let barSeparatorColor = UIColor.stepikOpaqueSeparator + static let barButtonTitleFontNormal = UIFont.systemFont(ofSize: 15, weight: .light) + static let barButtonTitleFontSelected = UIFont.systemFont(ofSize: 15) + static let barButtonTitleColor = UIColor.stepikPrimaryText + + static var navigationBarAppearance: StyledNavigationController.NavigationBarAppearanceState { + .init(shadowViewAlpha: 0.0) + } + } + + private lazy var tabBarView: TMBar = { + let bar = TMBarView() + bar.layout.transitionStyle = .snap + bar.tintColor = Appearance.barTintColor + bar.backgroundView.style = .flat(color: Appearance.barBackgroundColor) + bar.indicator.tintColor = Appearance.barTintColor + bar.indicator.weight = .light + bar.layout.interButtonSpacing = 0 + bar.layout.contentMode = .fit + + bar.buttons.customize { labelBarButton in + labelBarButton.font = Appearance.barButtonTitleFontNormal + labelBarButton.selectedFont = Appearance.barButtonTitleFontSelected + labelBarButton.tintColor = Appearance.barButtonTitleColor + labelBarButton.selectedTintColor = Appearance.barButtonTitleColor + } + + let separatorView = UIView() + separatorView.backgroundColor = Appearance.barSeparatorColor + bar.backgroundView.addSubview(separatorView) + separatorView.translatesAutoresizingMaskIntoConstraints = false + separatorView.snp.makeConstraints { make in + make.height.equalTo(1.0 / UIScreen.main.nativeScale) + make.leading.trailing.bottom.equalToSuperview() + } + + return bar + }() + + private let interactor: UserCoursesInteractorProtocol + + private let availableTabs: [UserCourses.Tab] + private let initialTabIndex: Int + private var tabViewControllers: [UIViewController?] = [] + + init( + interactor: UserCoursesInteractorProtocol, + availableTabs: [UserCourses.Tab], + initialTab: UserCourses.Tab + ) { + self.interactor = interactor + + self.availableTabs = availableTabs + self.tabViewControllers = Array(repeating: nil, count: availableTabs.count) + + if let initialTabIndex = self.availableTabs.firstIndex(of: initialTab) { + self.initialTabIndex = initialTabIndex + } else { + self.initialTabIndex = 0 + } + + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + let view = UserCoursesView(frame: UIScreen.main.bounds) + self.view = view + } + + override func viewDidLoad() { + super.viewDidLoad() + + self.setup() + self.interactor.doUserCoursesFetch(request: .init()) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if let styledNavigationController = self.navigationController as? StyledNavigationController { + styledNavigationController.changeShadowViewAlpha( + Appearance.navigationBarAppearance.shadowViewAlpha, + sender: self + ) + } + } + + // MARK: Private API + + private func setup() { + self.title = NSLocalizedString("UserCoursesTitle", comment: "") + self.view.backgroundColor = Appearance.backgroundColor + + self.dataSource = self + self.addBar(self.tabBarView, dataSource: self, at: .top) + } + + private func loadTabViewControllerIfNeeded(at index: Int) { + guard self.tabViewControllers.count > index else { + fatalError("Invalid controllers initialization") + } + + guard self.tabViewControllers[index] == nil else { + return + } + + guard let tab = self.availableTabs[safe: index] else { + return + } + + let assembly = FullscreenCourseListAssembly( + presentationDescription: nil, + courseListType: tab.courseListType + ) + + self.tabViewControllers[index] = assembly.makeModule() + } +} + +private extension UserCourses.Tab { + var courseListType: CourseListType { + switch self { + case .allCourses: + return EnrolledCourseListType() + case .favorites: + return FavoriteCourseListType() + case .archived: + return ArchivedCourseListType() + } + } +} + +// MARK: - UserCoursesViewController: UserCoursesViewControllerProtocol - + +extension UserCoursesViewController: UserCoursesViewControllerProtocol { + func displayUserCourses(viewModel: UserCourses.UserCoursesLoad.ViewModel) { + self.reloadData() + } +} + +// MARK: - UserCoursesViewController: PageboyViewControllerDataSource - + +extension UserCoursesViewController: PageboyViewControllerDataSource { + func numberOfViewControllers(in pageboyViewController: PageboyViewController) -> Int { + self.availableTabs.count + } + + func viewController( + for pageboyViewController: PageboyViewController, + at index: PageboyViewController.PageIndex + ) -> UIViewController? { + self.loadTabViewControllerIfNeeded(at: index) + return self.tabViewControllers[index] + } + + func defaultPage(for pageboyViewController: PageboyViewController) -> PageboyViewController.Page? { + .at(index: self.initialTabIndex) + } +} + +// MARK: - UserCoursesViewController: TMBarDataSource - + +extension UserCoursesViewController: TMBarDataSource { + func barItem(for bar: TMBar, at index: Int) -> TMBarItemable { + let title = self.availableTabs[safe: index]?.title ?? "" + return TMBarItem(title: title) + } +} + +// MARK: - UserCoursesViewController: StyledNavigationControllerPresentable - + +extension UserCoursesViewController: StyledNavigationControllerPresentable { + var navigationBarAppearanceOnFirstPresentation: StyledNavigationController.NavigationBarAppearanceState { + Appearance.navigationBarAppearance + } +} diff --git a/Stepic/Sources/Modules/Quizzes/NewSortingQuiz/Views/NewSortingQuizElementView.swift b/Stepic/Sources/Modules/Quizzes/NewSortingQuiz/Views/NewSortingQuizElementView.swift index 91cf040fe5..a05600df66 100644 --- a/Stepic/Sources/Modules/Quizzes/NewSortingQuiz/Views/NewSortingQuizElementView.swift +++ b/Stepic/Sources/Modules/Quizzes/NewSortingQuiz/Views/NewSortingQuizElementView.swift @@ -216,8 +216,8 @@ final class NewSortingQuizElementView: UIView { struct Direction: OptionSet { let rawValue: Int - static let top = Direction(rawValue: 1) - static let bottom = Direction(rawValue: 2) + static let top = Direction(rawValue: 1 << 0) + static let bottom = Direction(rawValue: 1 << 1) } } diff --git a/Stepic/Sources/Services/DataBackUpdateService.swift b/Stepic/Sources/Services/DataBackUpdateService.swift index 2de89f886e..4d93f274d1 100644 --- a/Stepic/Sources/Services/DataBackUpdateService.swift +++ b/Stepic/Sources/Services/DataBackUpdateService.swift @@ -14,14 +14,14 @@ enum DataBackUpdateTarget { struct DataBackUpdateDescription: OptionSet { let rawValue: Int - static let progress = DataBackUpdateDescription(rawValue: 1) - static let enrollment = DataBackUpdateDescription(rawValue: 2) - static let profileFirstName = DataBackUpdateDescription(rawValue: 3) - static let profileLastName = DataBackUpdateDescription(rawValue: 4) - static let profileShortBio = DataBackUpdateDescription(rawValue: 5) - static let profileDetails = DataBackUpdateDescription(rawValue: 6) - static let courseIsFavorite = DataBackUpdateDescription(rawValue: 7) - static let courseIsArchived = DataBackUpdateDescription(rawValue: 8) + static let progress = DataBackUpdateDescription(rawValue: 1 << 0) + static let enrollment = DataBackUpdateDescription(rawValue: 1 << 1) + static let profileFirstName = DataBackUpdateDescription(rawValue: 1 << 2) + static let profileLastName = DataBackUpdateDescription(rawValue: 1 << 3) + static let profileShortBio = DataBackUpdateDescription(rawValue: 1 << 4) + static let profileDetails = DataBackUpdateDescription(rawValue: 1 << 5) + static let courseIsFavorite = DataBackUpdateDescription(rawValue: 1 << 6) + static let courseIsArchived = DataBackUpdateDescription(rawValue: 1 << 7) } protocol DataBackUpdateServiceDelegate: AnyObject { @@ -39,19 +39,6 @@ protocol DataBackUpdateServiceDelegate: AnyObject { ) } -extension DataBackUpdateServiceDelegate { - func dataBackUpdateService( - _ dataBackUpdateService: DataBackUpdateService, - didReport update: DataBackUpdateDescription, - for target: DataBackUpdateTarget - ) {} - - func dataBackUpdateService( - _ dataBackUpdateService: DataBackUpdateService, - didReport refreshedTarget: DataBackUpdateTarget - ) {} -} - protocol DataBackUpdateServiceProtocol: AnyObject { var delegate: DataBackUpdateServiceDelegate? { get set } diff --git a/Stepic/en.lproj/Localizable.strings b/Stepic/en.lproj/Localizable.strings index a9d6d77b4e..49a7035db3 100644 --- a/Stepic/en.lproj/Localizable.strings +++ b/Stepic/en.lproj/Localizable.strings @@ -476,6 +476,12 @@ CourseInfoTitleLanguage = "Language"; CourseInfoTitleCertificate = "Certificate"; CourseInfoTitleCertificateDetails = "Certificate details"; +/* User Courses */ +UserCoursesTitle = "My Courses"; +UserCoursesTabAllCoursesTitle = "All"; +UserCoursesTabFavoritesTitle = "Favorite"; +UserCoursesTabArchivedTitle = "Archive"; + RetentionNotificationOnNextDayTitle = "Don't slow down"; RetentionNotificationOnNextDayText = "You've done a great job learning yesterday, come back today and become smarter! 🚀"; RetentionNotificationOnThirdDayTitle = "Lost your thirst to knowledge? 🤔"; diff --git a/Stepic/ru.lproj/Localizable.strings b/Stepic/ru.lproj/Localizable.strings index 8964ba1cef..6a71e29bb3 100644 --- a/Stepic/ru.lproj/Localizable.strings +++ b/Stepic/ru.lproj/Localizable.strings @@ -478,6 +478,12 @@ CourseInfoTitleLanguage = "Язык"; CourseInfoTitleCertificate = "Сертификат"; CourseInfoTitleCertificateDetails = "Подробнее о сертификате"; +/* User Courses */ +UserCoursesTitle = "Мои курсы"; +UserCoursesTabAllCoursesTitle = "Все"; +UserCoursesTabFavoritesTitle = "Избранные"; +UserCoursesTabArchivedTitle = "Архив"; + RetentionNotificationOnNextDayTitle = "Не сбавляем темп"; RetentionNotificationOnNextDayText = "Удалось поучиться вчера - это хороший повод зайти сегодня и стать еще умнее! 🚀"; RetentionNotificationOnThirdDayTitle = "Иссякла тяга к знаниям? 🤔"; From 83432b576b34b7fc127cdc086a0f033752b45dc7 Mon Sep 17 00:00:00 2001 From: Stepik Bot Date: Wed, 29 Apr 2020 03:04:33 +0000 Subject: [PATCH 04/11] "Set version to 1.122 & bump build" --- Stepic.xcodeproj/project.pbxproj | 4 ++-- Stepic/Info.plist | 4 ++-- StepicTests/Info.plist | 4 ++-- StickerPackExtension/Info.plist | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index 10d46e7a59..1d240c3760 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -8325,7 +8325,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 203; + CURRENT_PROJECT_VERSION = 204; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = Stepic/Info.plist; @@ -8355,7 +8355,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 203; + CURRENT_PROJECT_VERSION = 204; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = Stepic/Info.plist; diff --git a/Stepic/Info.plist b/Stepic/Info.plist index e115028e06..ed0f34c256 100644 --- a/Stepic/Info.plist +++ b/Stepic/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.121 + 1.122 CFBundleSignature ???? CFBundleURLTypes @@ -54,7 +54,7 @@ CFBundleVersion - 203 + 204 Fabric APIKey diff --git a/StepicTests/Info.plist b/StepicTests/Info.plist index 1449a3b311..ddece6cb0f 100644 --- a/StepicTests/Info.plist +++ b/StepicTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.121 + 1.122 CFBundleSignature ???? CFBundleVersion - 203 + 204 diff --git a/StickerPackExtension/Info.plist b/StickerPackExtension/Info.plist index 36b6e50b4f..89fe885d2c 100644 --- a/StickerPackExtension/Info.plist +++ b/StickerPackExtension/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.121 + 1.122 CFBundleVersion - 203 + 204 NSExtension NSExtensionPointIdentifier From c8046e4815ba1d19721daff2a2534e98a77453bc Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Wed, 29 Apr 2020 11:02:10 +0300 Subject: [PATCH 05/11] Add release notes file for beta testers --- fastlane/release-notes.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastlane/release-notes.txt b/fastlane/release-notes.txt index 902333b449..568091abe2 100644 --- a/fastlane/release-notes.txt +++ b/fastlane/release-notes.txt @@ -1,3 +1,3 @@ Что тестировать: -- Поддержка AR контента в шагах APPS-2817. -- Бейджики 'Команда курса' для ассистента, 'Модератор курса' для модераторов в комментариях APPS-2823. \ No newline at end of file +- Поддержка операций со списками курсов (добавление в избранное и перемещение в архив) на странице курса APPS-2759. +- Экран "Мои курсы", который состоит 3-х табов: "Все", "Избранные", "Архив" APPS-2771. \ No newline at end of file From 3ea1d38e360c1a002ad4f73669a4545416f9248a Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Wed, 29 Apr 2020 16:40:26 +0300 Subject: [PATCH 06/11] Show user course action result with status and localized message --- .../CourseInfo/CourseInfoDataFlow.swift | 31 +++++++---- .../CourseInfo/CourseInfoInteractor.swift | 51 +++++++++++-------- .../CourseInfo/CourseInfoPresenter.swift | 31 +++++++++-- .../CourseInfo/CourseInfoViewController.swift | 8 +-- Stepic/en.lproj/Localizable.strings | 11 ++++ Stepic/ru.lproj/Localizable.strings | 11 ++++ 6 files changed, 104 insertions(+), 39 deletions(-) diff --git a/Stepic/Sources/Modules/CourseInfo/CourseInfoDataFlow.swift b/Stepic/Sources/Modules/CourseInfo/CourseInfoDataFlow.swift index 836f8eb912..baec82cd2a 100644 --- a/Stepic/Sources/Modules/CourseInfo/CourseInfoDataFlow.swift +++ b/Stepic/Sources/Modules/CourseInfo/CourseInfoDataFlow.swift @@ -18,6 +18,13 @@ enum CourseInfo { } } + enum UserCourseAction { + case favoriteAdd + case favoriteRemove + case archiveAdd + case archiveRemove + } + // MARK: Use cases /// Load & show info about course @@ -130,17 +137,6 @@ enum CourseInfo { } } - /// Handle HUD status update - enum BlockingWaitingIndicatorStatusUpdate { - struct Response { - let isSuccessful: Bool - } - - struct ViewModel { - let isSuccessful: Bool - } - } - /// Drop course enum CourseUnenrollmentAction { struct Request {} @@ -156,6 +152,19 @@ enum CourseInfo { struct Request {} } + /// Present HUD with status and localized message + enum UserCourseActionPresentation { + struct Response { + let userCourseAction: UserCourseAction + let isSuccessful: Bool + } + + struct ViewModel { + let isSuccessful: Bool + let message: String + } + } + /// Do main action (continue, enroll, etc) enum MainCourseAction { struct Request {} diff --git a/Stepic/Sources/Modules/CourseInfo/CourseInfoInteractor.swift b/Stepic/Sources/Modules/CourseInfo/CourseInfoInteractor.swift index 17d00eba7f..53f73a5ff8 100644 --- a/Stepic/Sources/Modules/CourseInfo/CourseInfoInteractor.swift +++ b/Stepic/Sources/Modules/CourseInfo/CourseInfoInteractor.swift @@ -167,11 +167,15 @@ final class CourseInfoInteractor: CourseInfoInteractorProtocol { } func doCourseFavoriteAction(request: CourseInfo.CourseFavoriteAction.Request) { - self.doUserCourseUpdateAction { $0.isFavorite.toggle() } + if let currentCourse = self.currentCourse, currentCourse.enrolled { + self.doUserCourseAction(currentCourse.isFavorite ? .favoriteRemove : .favoriteAdd) + } } func doCourseArchiveAction(request: CourseInfo.CourseArchiveAction.Request) { - self.doUserCourseUpdateAction { $0.isArchived.toggle() } + if let currentCourse = self.currentCourse, currentCourse.enrolled { + self.doUserCourseAction(currentCourse.isArchived ? .archiveRemove : .archiveAdd) + } } func doMainCourseAction(request: CourseInfo.MainCourseAction.Request) { @@ -276,36 +280,43 @@ final class CourseInfoInteractor: CourseInfoInteractorProtocol { } } - private func doUserCourseUpdateAction(_ updateBlock: @escaping (UserCourse) -> Void) { - guard let course = self.currentCourse, course.enrolled else { - return - } + private func doUserCourseAction(_ action: CourseInfo.UserCourseAction) { + let currentCourse = self.currentCourse.require() self.presenter.presentWaitingState(response: .init(shouldDismiss: false)) firstly { self.provider - .fetchUserCourse(courseID: course.id) + .fetchUserCourse(courseID: currentCourse.id) .compactMap { $0 } }.then { userCourse -> Promise in - updateBlock(userCourse) + switch action { + case .favoriteAdd: + userCourse.isFavorite = true + case .favoriteRemove: + userCourse.isFavorite = false + case .archiveAdd: + userCourse.isArchived = true + case .archiveRemove: + userCourse.isArchived = false + } + return self.provider.updateUserCourse(userCourse: userCourse) }.done { userCourse in - if let course = self.currentCourse { - if course.isFavorite != userCourse.isFavorite { - course.isFavorite = userCourse.isFavorite - self.dataBackUpdateService.triggerCourseIsFavoriteUpdate(retrievedCourse: course) - } - if course.isArchived != userCourse.isArchived { - course.isArchived = userCourse.isArchived - self.dataBackUpdateService.triggerCourseIsArchivedUpdate(retrievedCourse: course) - } - self.presenter.presentCourse(response: .init(result: .success(course))) + if currentCourse.isFavorite != userCourse.isFavorite { + currentCourse.isFavorite = userCourse.isFavorite + self.dataBackUpdateService.triggerCourseIsFavoriteUpdate(retrievedCourse: currentCourse) + } + if currentCourse.isArchived != userCourse.isArchived { + currentCourse.isArchived = userCourse.isArchived + self.dataBackUpdateService.triggerCourseIsArchivedUpdate(retrievedCourse: currentCourse) } - self.presenter.presentWaitingStatus(response: .init(isSuccessful: true)) + + self.presenter.presentCourse(response: .init(result: .success(currentCourse))) + self.presenter.presentUserCourseActionResult(response: .init(userCourseAction: action, isSuccessful: true)) }.catch { error in print("course info interactor: user course action error = \(error)") - self.presenter.presentWaitingStatus(response: .init(isSuccessful: false)) + self.presenter.presentUserCourseActionResult(response: .init(userCourseAction: action, isSuccessful: false)) } } diff --git a/Stepic/Sources/Modules/CourseInfo/CourseInfoPresenter.swift b/Stepic/Sources/Modules/CourseInfo/CourseInfoPresenter.swift index a95c165691..3f8dbb5967 100644 --- a/Stepic/Sources/Modules/CourseInfo/CourseInfoPresenter.swift +++ b/Stepic/Sources/Modules/CourseInfo/CourseInfoPresenter.swift @@ -10,7 +10,7 @@ protocol CourseInfoPresenterProtocol { func presentAuthorization(response: CourseInfo.AuthorizationPresentation.Response) func presentPaidCourseBuying(response: CourseInfo.PaidCourseBuyingPresentation.Response) func presentWaitingState(response: CourseInfo.BlockingWaitingIndicatorUpdate.Response) - func presentWaitingStatus(response: CourseInfo.BlockingWaitingIndicatorStatusUpdate.Response) + func presentUserCourseActionResult(response: CourseInfo.UserCourseActionPresentation.Response) } final class CourseInfoPresenter: CourseInfoPresenterProtocol { @@ -78,9 +78,32 @@ final class CourseInfoPresenter: CourseInfoPresenterProtocol { self.viewController?.displayBlockingLoadingIndicator(viewModel: .init(shouldDismiss: response.shouldDismiss)) } - func presentWaitingStatus(response: CourseInfo.BlockingWaitingIndicatorStatusUpdate.Response) { - self.viewController?.displayBlockingLoadingIndicatorStatus( - viewModel: .init(isSuccessful: response.isSuccessful) + func presentUserCourseActionResult(response: CourseInfo.UserCourseActionPresentation.Response) { + let isSuccessful = response.isSuccessful + + let message: String = { + switch response.userCourseAction { + case .favoriteAdd: + return isSuccessful + ? NSLocalizedString("CourseInfoCourseActionAddToFavoritesSuccessMessage", comment: "") + : NSLocalizedString("CourseInfoCourseActionAddToFavoritesFailureMessage", comment: "") + case .favoriteRemove: + return isSuccessful + ? NSLocalizedString("CourseInfoCourseActionRemoveFromFavoritesSuccessMessage", comment: "") + : NSLocalizedString("CourseInfoCourseActionRemoveFromFavoritesFailureMessage", comment: "") + case .archiveAdd: + return isSuccessful + ? NSLocalizedString("CourseInfoCourseActionMoveToArchivedSuccessMessage", comment: "") + : NSLocalizedString("CourseInfoCourseActionMoveToArchivedFailureMessage", comment: "") + case .archiveRemove: + return isSuccessful + ? NSLocalizedString("CourseInfoCourseActionRemoveFromArchivedSuccessMessage", comment: "") + : NSLocalizedString("CourseInfoCourseActionRemoveFromArchivedFailureMessage", comment: "") + } + }() + + self.viewController?.displayUserCourseActionResult( + viewModel: .init(isSuccessful: response.isSuccessful, message: message) ) } diff --git a/Stepic/Sources/Modules/CourseInfo/CourseInfoViewController.swift b/Stepic/Sources/Modules/CourseInfo/CourseInfoViewController.swift index e855992426..db13fca11e 100644 --- a/Stepic/Sources/Modules/CourseInfo/CourseInfoViewController.swift +++ b/Stepic/Sources/Modules/CourseInfo/CourseInfoViewController.swift @@ -20,7 +20,7 @@ protocol CourseInfoViewControllerProtocol: AnyObject { func displayAuthorization(viewModel: CourseInfo.AuthorizationPresentation.ViewModel) func displayPaidCourseBuying(viewModel: CourseInfo.PaidCourseBuyingPresentation.ViewModel) func displayBlockingLoadingIndicator(viewModel: CourseInfo.BlockingWaitingIndicatorUpdate.ViewModel) - func displayBlockingLoadingIndicatorStatus(viewModel: CourseInfo.BlockingWaitingIndicatorStatusUpdate.ViewModel) + func displayUserCourseActionResult(viewModel: CourseInfo.UserCourseActionPresentation.ViewModel) } final class CourseInfoViewController: UIViewController { @@ -497,11 +497,11 @@ extension CourseInfoViewController: CourseInfoViewControllerProtocol { } } - func displayBlockingLoadingIndicatorStatus(viewModel: CourseInfo.BlockingWaitingIndicatorStatusUpdate.ViewModel) { + func displayUserCourseActionResult(viewModel: CourseInfo.UserCourseActionPresentation.ViewModel) { if viewModel.isSuccessful { - SVProgressHUD.showSuccess(withStatus: nil) + SVProgressHUD.showSuccess(withStatus: viewModel.message) } else { - SVProgressHUD.showError(withStatus: nil) + SVProgressHUD.showError(withStatus: viewModel.message) } } diff --git a/Stepic/en.lproj/Localizable.strings b/Stepic/en.lproj/Localizable.strings index 49a7035db3..d8140cf239 100644 --- a/Stepic/en.lproj/Localizable.strings +++ b/Stepic/en.lproj/Localizable.strings @@ -445,10 +445,21 @@ ContinueCourseCourseCurrentProgressTitle = "Current progress: %@/%@ points"; /* Course info */ CourseInfoTitle = "About course"; +// Favorite CourseInfoCourseActionAddToFavoritesAlertTitle = "Add to Favorites"; CourseInfoCourseActionRemoveFromFavoritesAlertTitle = "Remove from Favorites"; +CourseInfoCourseActionAddToFavoritesSuccessMessage = "Course Successfully Added to Favorites"; +CourseInfoCourseActionRemoveFromFavoritesSuccessMessage = "Course Successfully Removed from Favorites"; +CourseInfoCourseActionAddToFavoritesFailureMessage = "Failed to Add Course to Favorites"; +CourseInfoCourseActionRemoveFromFavoritesFailureMessage = "Failed to Remove Course from Favorites"; +// Archive CourseInfoCourseActionMoveToArchivedAlertTitle = "Move to Archived"; CourseInfoCourseActionRemoveFromArchivedAlertTitle = "Remove from Archived"; +CourseInfoCourseActionMoveToArchivedSuccessMessage = "Course Successfully Moved to Archived"; +CourseInfoCourseActionRemoveFromArchivedSuccessMessage = "Course Successfully Removed from Archived"; +CourseInfoCourseActionMoveToArchivedFailureMessage = "Failed to Move Course to Archived"; +CourseInfoCourseActionRemoveFromArchivedFailureMessage = "Failed to Remove Course from Archived"; + CourseInfoTabInfo = "Info"; CourseInfoTabSyllabus = "Syllabus"; CourseInfoTabSyllabusSectionProgressTitle = "%@/%@ points"; diff --git a/Stepic/ru.lproj/Localizable.strings b/Stepic/ru.lproj/Localizable.strings index 6a71e29bb3..2fdb900343 100644 --- a/Stepic/ru.lproj/Localizable.strings +++ b/Stepic/ru.lproj/Localizable.strings @@ -447,10 +447,21 @@ ContinueCourseCourseCurrentProgressTitle = "Текущий прогресс: %@/ /* Course info */ CourseInfoTitle = "О курсе"; +// Favorite CourseInfoCourseActionAddToFavoritesAlertTitle = "Добавить в избранное"; CourseInfoCourseActionRemoveFromFavoritesAlertTitle = "Убрать из избранного"; +CourseInfoCourseActionAddToFavoritesSuccessMessage = "Курс успешно добавлен в избранное"; +CourseInfoCourseActionRemoveFromFavoritesSuccessMessage = "Курс успешно убран из избранного"; +CourseInfoCourseActionAddToFavoritesFailureMessage = "Не удалось добавить курс в избранное"; +CourseInfoCourseActionRemoveFromFavoritesFailureMessage = "Не удалось убрать курс из избранного"; +// Archive CourseInfoCourseActionMoveToArchivedAlertTitle = "Переместить в архив"; CourseInfoCourseActionRemoveFromArchivedAlertTitle = "Убрать из архива"; +CourseInfoCourseActionMoveToArchivedSuccessMessage = "Курс успешно перемещён в архив"; +CourseInfoCourseActionRemoveFromArchivedSuccessMessage = "Курс успешно убран из архива"; +CourseInfoCourseActionMoveToArchivedFailureMessage = "Не удалось переместить курс в архив"; +CourseInfoCourseActionRemoveFromArchivedFailureMessage = "Не удалось убрать курс из архива"; + CourseInfoTabInfo = "Инфо"; CourseInfoTabSyllabus = "Модули"; CourseInfoTabSyllabusSectionProgressTitle = "%@/%@ баллов"; From 93d6dd446da70530faad6461ee19d3c358d727c8 Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Wed, 29 Apr 2020 16:40:47 +0300 Subject: [PATCH 07/11] Bump build --- Stepic.xcodeproj/project.pbxproj | 4 ++-- Stepic/Info.plist | 2 +- StepicTests/Info.plist | 2 +- StickerPackExtension/Info.plist | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index 1d240c3760..f10d84936e 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -8325,7 +8325,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 204; + CURRENT_PROJECT_VERSION = 205; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = Stepic/Info.plist; @@ -8355,7 +8355,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 204; + CURRENT_PROJECT_VERSION = 205; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = Stepic/Info.plist; diff --git a/Stepic/Info.plist b/Stepic/Info.plist index ed0f34c256..343854763d 100644 --- a/Stepic/Info.plist +++ b/Stepic/Info.plist @@ -54,7 +54,7 @@ CFBundleVersion - 204 + 205 Fabric APIKey diff --git a/StepicTests/Info.plist b/StepicTests/Info.plist index ddece6cb0f..09e99ed99e 100644 --- a/StepicTests/Info.plist +++ b/StepicTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 204 + 205 diff --git a/StickerPackExtension/Info.plist b/StickerPackExtension/Info.plist index 89fe885d2c..5c24b1b1eb 100644 --- a/StickerPackExtension/Info.plist +++ b/StickerPackExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.122 CFBundleVersion - 204 + 205 NSExtension NSExtensionPointIdentifier From 2e34c1e26c62156fd51d6c39439d05b0b103b599 Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Wed, 29 Apr 2020 19:23:16 +0300 Subject: [PATCH 08/11] Fix crash on next/previous lesson navigation Reverts bump of Tabman. --- Podfile | 2 +- Podfile.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Podfile b/Podfile index 8b074b0776..f97cd9cae2 100644 --- a/Podfile +++ b/Podfile @@ -63,7 +63,7 @@ def all_pods pod 'ActionSheetPicker-3.0', '2.4.0' pod 'Nuke', '8.4.1' pod 'STRegex', '2.1.1' - pod 'Tabman', '2.8.2' + pod 'Tabman', '2.8.0' pod 'SwiftDate', '6.1.0' end diff --git a/Podfile.lock b/Podfile.lock index e43c59ec72..ff55fba378 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -176,8 +176,8 @@ PODS: - SwiftLint (0.39.2) - SwiftyGif (5.2.0) - SwiftyJSON (5.0.0) - - Tabman (2.8.2): - - Pageboy (~> 3.5.1) + - Tabman (2.8.0): + - Pageboy (~> 3.5.0) - TSMessages (0.9.13): - HexColors (~> 2.3.0) - TTTAttributedLabel (2.0.0) @@ -232,7 +232,7 @@ DEPENDENCIES: - SwiftDate (= 6.1.0) - SwiftLint (= 0.39.2) - SwiftyJSON (= 5.0.0) - - Tabman (= 2.8.2) + - Tabman (= 2.8.0) - TSMessages (from `https://github.com/KrauseFx/TSMessages.git`) - TTTAttributedLabel (= 2.0.0) - TUSafariActivity (= 1.0.4) @@ -381,7 +381,7 @@ SPEC CHECKSUMS: SwiftLint: 22ccbbe3b8008684be5955693bab135e0ed6a447 SwiftyGif: b85c6b33a9411859d9e1db998b6a8214aea942df SwiftyJSON: 36413e04c44ee145039d332b4f4e2d3e8d6c4db7 - Tabman: 05f32e419b07411b5ec74516212ffbff2a9dfc0e + Tabman: f62ad94ee54a7d96e3fbab34f677d9ea4d38ece6 TSMessages: eb3cf27b6900684a21bad4fe9ea426e287b8c839 TTTAttributedLabel: 8cffe8e127e4e82ff3af1e5386d4cd0ad000b656 TUSafariActivity: afc55a00965377939107ce4fdc7f951f62454546 @@ -389,6 +389,6 @@ SPEC CHECKSUMS: VK-ios-sdk: 62a10b6571fbcda0657f455fedce7fedf55b4cd0 YandexMobileMetrica: 1030df2959c886a7be3bc3d274eb54c8e86afd6f -PODFILE CHECKSUM: 90b5bb417626b8d84cd9f2e1ab3eba1529b4b7c2 +PODFILE CHECKSUM: 1b08a26ef5c257e8ed979b60d324befae1fcbb14 COCOAPODS: 1.9.1 From b91f8e923410e80af4d5f79979183befcedc0232 Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Wed, 29 Apr 2020 19:23:37 +0300 Subject: [PATCH 09/11] Bump build --- Stepic.xcodeproj/project.pbxproj | 4 ++-- Stepic/Info.plist | 2 +- StepicTests/Info.plist | 2 +- StickerPackExtension/Info.plist | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index f10d84936e..7389517268 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -8325,7 +8325,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 205; + CURRENT_PROJECT_VERSION = 206; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = Stepic/Info.plist; @@ -8355,7 +8355,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 205; + CURRENT_PROJECT_VERSION = 206; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = Stepic/Info.plist; diff --git a/Stepic/Info.plist b/Stepic/Info.plist index 343854763d..fb4afba9cd 100644 --- a/Stepic/Info.plist +++ b/Stepic/Info.plist @@ -54,7 +54,7 @@ CFBundleVersion - 205 + 206 Fabric APIKey diff --git a/StepicTests/Info.plist b/StepicTests/Info.plist index 09e99ed99e..71fec76880 100644 --- a/StepicTests/Info.plist +++ b/StepicTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 205 + 206 diff --git a/StickerPackExtension/Info.plist b/StickerPackExtension/Info.plist index 5c24b1b1eb..1128dd83ba 100644 --- a/StickerPackExtension/Info.plist +++ b/StickerPackExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.122 CFBundleVersion - 205 + 206 NSExtension NSExtensionPointIdentifier From 0f97e3a46c9fbb1faa6892416ca3ed4eb21cb91f Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Wed, 29 Apr 2020 22:27:11 +0300 Subject: [PATCH 10/11] Update GitHub logo APPS-2844 --- .../github.imageset/Contents.json | 43 ++++++++++++++++-- .../github.imageset/GitHub-Mark-Light.png | Bin 0 -> 2219 bytes .../github.imageset/GitHub-Mark-Light@2x.png | Bin 0 -> 3819 bytes .../github.imageset/GitHub-Mark-Light@3x.png | Bin 0 -> 5519 bytes .../github.imageset/GitHub-Mark.png | Bin 0 -> 2732 bytes .../github.imageset/GitHub-Mark@2x.png | Bin 0 -> 4777 bytes .../github.imageset/GitHub-Mark@3x.png | Bin 0 -> 6831 bytes .../Social icons/github.imageset/github.png | Bin 1179 -> 0 bytes .../github.imageset/github@2x.png | Bin 2372 -> 0 bytes .../github.imageset/github@3x.png | Bin 3640 -> 0 bytes 10 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 Stepic/Images.xcassets/Social icons/github.imageset/GitHub-Mark-Light.png create mode 100644 Stepic/Images.xcassets/Social icons/github.imageset/GitHub-Mark-Light@2x.png create mode 100644 Stepic/Images.xcassets/Social icons/github.imageset/GitHub-Mark-Light@3x.png create mode 100644 Stepic/Images.xcassets/Social icons/github.imageset/GitHub-Mark.png create mode 100644 Stepic/Images.xcassets/Social icons/github.imageset/GitHub-Mark@2x.png create mode 100644 Stepic/Images.xcassets/Social icons/github.imageset/GitHub-Mark@3x.png delete mode 100644 Stepic/Images.xcassets/Social icons/github.imageset/github.png delete mode 100644 Stepic/Images.xcassets/Social icons/github.imageset/github@2x.png delete mode 100644 Stepic/Images.xcassets/Social icons/github.imageset/github@3x.png diff --git a/Stepic/Images.xcassets/Social icons/github.imageset/Contents.json b/Stepic/Images.xcassets/Social icons/github.imageset/Contents.json index a083698e96..c17ab205f6 100644 --- a/Stepic/Images.xcassets/Social icons/github.imageset/Contents.json +++ b/Stepic/Images.xcassets/Social icons/github.imageset/Contents.json @@ -1,23 +1,56 @@ { "images" : [ { + "filename" : "GitHub-Mark.png", "idiom" : "universal", - "filename" : "github.png", "scale" : "1x" }, { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "GitHub-Mark-Light.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "GitHub-Mark@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "GitHub-Mark-Light@2x.png", "idiom" : "universal", - "filename" : "github@2x.png", "scale" : "2x" }, { + "filename" : "GitHub-Mark@3x.png", + "idiom" : "universal", + "scale" : "3x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "GitHub-Mark-Light@3x.png", "idiom" : "universal", - "filename" : "github@3x.png", "scale" : "3x" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } } diff --git a/Stepic/Images.xcassets/Social icons/github.imageset/GitHub-Mark-Light.png b/Stepic/Images.xcassets/Social icons/github.imageset/GitHub-Mark-Light.png new file mode 100644 index 0000000000000000000000000000000000000000..ad09bef65533a63cac5862d6a2a82115ee6076b0 GIT binary patch literal 2219 zcmah~2~ZPf6yAUcat#O~YS1;*8bz{6Fvv!Tv*aRbP?2J&X9x?Vm~6-f15zv=hzDS) z2cW32D%#qj=pa*(!z*J@@x-QzC5Wx!LA9V*>e1a0jt;hN=FfZk^1koA@9qC@HqMO{ zyE%_`1^~cK5+R5pER|eNLy2##Tk|%;Q1B>mI8fF=Z6j_DpfX8Qcd?&zxG90ic zQ-}{h84WmCeE^tA8Pn%SQCNT-5eJF&d5#hoV#`@cSn`S>7J=@uNIVvYf^(BF9fl`i z8jwFf7^F+(3Z0FSHX;F~Fn95+%c+MaQhHB#zxKRR;4NlyJ(UNpJ4Hts+dObrQ#K5$P5DSK3h{=Z7 z>_8$T5KmX@Yc;jqPFphJ~m!KKXgD*aszPZXqAKzIk}s8tWaoBY(PHosJMJBD%WA! zXbfA<`^bC2D(YVV8&@bq9-9HjsOOAYqr@TQITOg<1J83_n3?}4@rqe35g^eKZU=W% zB}XIPIg7{9<8B#Z5M-l|l2bl6HaE|F7?f<1{SC^x9g|RnpNHod&j6KRd1DsjfjoD+ zbJ%cR^rCuIa}!)>O4#1`#!~t4a&bpdaYlQS>hhDUm?IU2>q&2P&ebE!9onRG>`Ui_Tt|$650{hJ&-4p3Gb$caG+Qmy1>tN$_AC#8=0z4JqQy#Z*%c_p_c6q(kb?F0H&X>1kzfI1u41KhDPfVV-%i9*AdQuhAvMUbH zg|Ddc#_O5I3l!oD?dos4e6!x{^?T)^w|B-x;yF8++j@@ArAu$=oD6CO8LCbe!~ z+Er_;)5Eh)yQdUjl?JG7_tWQ2>Ap*@Dxl`%=N?V2g%yRylXmKC@xBz}2D(q-#y>J% ziVV4rDT_>_9=%;TVn>>(?tL(W?T6oX9F;LgM7h;%jM}nwbK=h7Qxx})pBUAC<7l1z zqpsN{*)MXRmfo6u$0DRREshOQmj#%Q+(b5hvg*eIwel z=6>>o8<=;-l_W4y)aEI)M0R$WsvEVR-L+`VF0Du9n@lO2k56$qb&mdql9~8Q!l}2= z@0>S5KNYPHKFd%0ArOeDKC^_8SuY5=;C}8sb>>%9 zwXLyrtQDu6?)ldM7l!RFP|iD;|LlOfWq6Z=q9tfcN=(Ia&`{%dt+0+4@?>ppAh%H; jI}Um}q?0Q)Z>y!uh;JUh<_zD7{704uBL!vQ%hvn@Tr(Q{ literal 0 HcmV?d00001 diff --git a/Stepic/Images.xcassets/Social icons/github.imageset/GitHub-Mark-Light@2x.png b/Stepic/Images.xcassets/Social icons/github.imageset/GitHub-Mark-Light@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..97047d5de59b53f433aa13121bf0d9b2e7d84d24 GIT binary patch literal 3819 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 G=6?Xv=EN@m literal 0 HcmV?d00001 diff --git a/Stepic/Images.xcassets/Social icons/github.imageset/GitHub-Mark.png b/Stepic/Images.xcassets/Social icons/github.imageset/GitHub-Mark.png new file mode 100644 index 0000000000000000000000000000000000000000..673a3095c9fd0427741e2b63df24439b7ba49a49 GIT binary patch literal 2732 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 E11n|=!~g&Q literal 0 HcmV?d00001 diff --git a/Stepic/Images.xcassets/Social icons/github.imageset/GitHub-Mark@3x.png b/Stepic/Images.xcassets/Social icons/github.imageset/GitHub-Mark@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..aec1c9a023022e040708748fb8b1a24453ef3e78 GIT binary patch literal 6831 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 literal 0 HcmV?d00001 diff --git a/Stepic/Images.xcassets/Social icons/github.imageset/github.png b/Stepic/Images.xcassets/Social icons/github.imageset/github.png deleted file mode 100644 index 5a00c9ec9b33e016d15ab2ba50cda530689c275f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1179 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 Date: Wed, 29 Apr 2020 22:27:56 +0300 Subject: [PATCH 11/11] Bump build --- Stepic.xcodeproj/project.pbxproj | 4 ++-- Stepic/Info.plist | 2 +- StepicTests/Info.plist | 2 +- StickerPackExtension/Info.plist | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index 7389517268..d7dd242145 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -8325,7 +8325,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 206; + CURRENT_PROJECT_VERSION = 207; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = Stepic/Info.plist; @@ -8355,7 +8355,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 206; + CURRENT_PROJECT_VERSION = 207; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = Stepic/Info.plist; diff --git a/Stepic/Info.plist b/Stepic/Info.plist index fb4afba9cd..94ecd45a1c 100644 --- a/Stepic/Info.plist +++ b/Stepic/Info.plist @@ -54,7 +54,7 @@ CFBundleVersion - 206 + 207 Fabric APIKey diff --git a/StepicTests/Info.plist b/StepicTests/Info.plist index 71fec76880..431fc9a4cb 100644 --- a/StepicTests/Info.plist +++ b/StepicTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 206 + 207 diff --git a/StickerPackExtension/Info.plist b/StickerPackExtension/Info.plist index 1128dd83ba..363839cd85 100644 --- a/StickerPackExtension/Info.plist +++ b/StickerPackExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.122 CFBundleVersion - 206 + 207 NSExtension NSExtensionPointIdentifier